• Stars
    star
    320
  • Rank 131,126 (Top 3 %)
  • Language
    Swift
  • License
    BSD 3-Clause "New...
  • Created over 2 years ago
  • Updated 3 months ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

A Swift library for efficient, flexible content-based text styling

Build Status License Platforms Documentation

Neon

A Swift library for efficient, flexible content-based text styling.

  • Minimal text invalidation
  • Support for multiple sources of token data
  • A hybrid sync/async system for targeting flicker-free styling on keystrokes
  • tree-sitter integration
  • Compatibility with lazy text loading
  • Text-system agnostic

Neon has a strong focus on efficiency and flexibility. It sits in-between your text system and wherever you get your semantic token information. Neon was developed for syntax highlighting and it can serve that need very well. However, it is more general-purpose than that and could be used for any system that needs to style text based its content.

Many people are looking for a drop-in editor View subclass that does it all. This is a lower-level library. You could, however, use Neon to drive highlighting for a view like this.

Token Data Sources

Neon was designed to accept and overlay token data from multiple sources simultaneously. Here's a real-world example of how this is used:

  • First pass: pattern-matching system with ok quality and guaranteed low-latency
  • Second pass: tree-sitter, which has good quality and can often be low-latency
  • Third pass: Language Server Protocol's semantic tokens, which can augment existing highlighting, but is high-latency

Theming

A highlighting theme is really just a mapping from semantic labels to styles. Token data sources apply the semantic labels and the TextSystemInterface uses those labels to look up styling.

This separation makes it very easy for you to do this look-up in a way that makes the most sense for whatever theming formats you'd like to support. This is also a convenient spot to adapt/modify the semantic labels coming from your data sources into a normalized form.

Tree-Sitter Integration

Neon includes a tree-sitter token source, built around SwiftTreeSitter. There is an included standalone module called TreeSitterClient that abstracts almost all of the details. It includes support for running tree-sitter "highlights.scm" queries, among other features. Its hybrid synchronous/asynchronous API makes it possible to scale tree-sitter to large documents, where its parsing/queries can introduce too much latency.

Tree-sitter uses separate compiled parsers for each language. There are a variety of ways to use tree-sitter parsers with SwiftTreeSitter. Check out that project for details.

Text System Integration

Neon is text-system independent. It makes very few assumptions about how text is stored, displayed, and styled. And, it is built around parts that can be used together to build a full system. But, there are also some helper types for use-cases with simpler needs.

TextViewSystemInterface

An implementation of the TextSystemInterface protocol for NSTextView/UITextView. This takes care of the interface to the view and NSLayoutManager, but defers Token-style translation (themes) to an external AttributeProvider. This is compatible with both TextKit 1 and 2.

TextViewHighlighter

A more full-featured system that integrates NSTextView/UITextView with a TreeSitterClient. This is a good way to get going quickly, or just to see how the parts fit together. Also compatible with TextKit 1 and 2.

Will take over as the delegate of the text view's NSTextStorage.

There is also an example project that demonstrates how to use TextViewHighlighter for macOS and iOS.

Manual Integration

A minimal integration can be achieved by configuring a Highlighter to interface with an NSTextView:

func applicationDidFinishLaunching(_ aNotification: Notification) {
   let textInterface = TextViewSystemInterface(textView: textView, attributeProvider: self.attributeProvider)
   self.highlighter = Highlighter(textInterface: textInterface, tokenProvider: self.tokenProvider)
   textStorage.delegate = self
   self.highlighter.invalidate()
}

Attaching the highlighter to a text view interface tells it what to update, but not when. You have to notify the highlighter whenever the text view's content changes, and invalidate existing highlighting as needed. Such notifications can be conveyed by making yourself the delegate of your text view's textStorage, and implementing this delegate method:

func textStorage(_ textStorage: NSTextStorage, didProcessEditing editedMask: NSTextStorageEditActions, range editedRange: NSRange, changeInLength delta: Int) {
   // Map NSTextStorageDelegate editedRange to Neon's style of editedRange
   let adjustedRange = NSRange(location: editedRange.location, length: editedRange.length - delta)
   self.highlighter.didChangeContent(in: adjustedRange, delta: delta)

   DispatchQueue.main.async {
      self.highlighter.invalidate()
   }
}

Notice that the invalidate method is dispatched asynchronously to ensure the styles are not updated until after the underlying text storage is done being edited.

The initial configuration of highlighter included references to self.tokenProvider and self.attributeProvider, which are responsible for providing the logic behind what gets highlighted, and how it should be done. At a minimum, the TokenProvider generates and supplies named tokens that correspond to ranges of text:

let paintItBlackTokenName = "paintItBlack"

func tokenProvider(_ range: NSRange, completionHandler: @escaping (Result<TokenApplication, Error>) -> Void) {
   var tokens: [Token] = []
   guard let searchString = self.textView.textStorage?.string else {
      // Could also complete with .failure(...) here
      completionHandler(.success(TokenApplication(tokens: tokens, action: .replace)))
      return
   }

   if let regex = try? NSRegularExpression(pattern: "[^\\s]+\\s{0,1}") {
      regex.enumerateMatches(in: searchString, range: range) { regexResult, _, _ in
         guard let result = regexResult else { return }
         for rangeIndex in 0..<result.numberOfRanges {
            let tokenRange = result.range(at: rangeIndex)
            tokens.append(Token(name: paintItBlackTokenName, range: tokenRange))
         }
      }
   }

   completionHandler(.success(TokenApplication(tokens: tokens, action: .replace)))
}

In this trivial example, the "paint it black" token is unilaterally applied to every non-whitespace range of the text. It demonstrates how you use a token provider to associate named tokens with arbitrary ranges of text. It's important to understand though that supplying the token doesn't change anything about the appearance of the corresponding text. In order to achieve that, you need to implement the attribute provider, which effectively translates named tokens in to suitable attributes:

func attributeProvider(_ token: Token) -> [NSAttributedString.Key: Any]? {
   if token.name == paintItBlackTokenName {
      return [.foregroundColor: NSColor.red, .backgroundColor: NSColor.black]
   }
   return nil
}

Now our example achieves its goal of "painting black" any runs of non-whitespace characters, along with single whitespace characters between them:

Screenshot of 'Paint it Black' text window showing text with a black background and red text.

Using this basic structure you can annotate the text with tokens while separately determining the appropriate styling for those tokens.

Advanced Integration

Achieving better performance and guaranteed flicker-free highlighting is more challenging. Monitoring the visible rect of the text view will improve performance. You need to know when a text change has been processing by enough of the system that styling is possible. This point in the text change lifecycle is not natively supported by NSTextStorage or NSLayoutManager. It requires an NSTextStorage subclass. But, even that isn't quite enough unfortunately, as you still need to precisely control the timing of invalidation and styling. This is where HighlighterInvalidationBuffer comes in.

Relationship to TextStory

TextStory is a library that contains three very useful components when working with Neon.

  • TSYTextStorage gets you all the text change life cycle hooks without falling into the NSString/String bridging performance traps
  • TextMutationEventRouter makes it easier to route events to the components
  • LazyTextStoringMonitor allows for lazy content reading, which is essential to quickly open large documents

Components

Highlighter

This is the main component that coordinates the styling and invalidation of text.

  • Connects to a text view via TextSystemInterface
  • Monitors text changes and view visible state
  • Gets token-level information from a TokenProvider

Note that Highlighter is built to handle a TokenProvider calling its completion block more than one time, potentially replacing or merging with existing styling.

HighlighterInvalidationBuffer

In a traditional NSTextStorage/NSLayoutManager system (TextKit 1), it can be challenging to achieve flicker-free on-keypress highlighting. This class offers a mechanism for buffering invalidations, so you can precisely control how and when actual text style updates occur.

TreeSitterClient

This class is an asynchronous interface to tree-sitter. It provides an UTF-16 code-point (NSString-compatible) API for edits, invalidations, and queries. It can process edits of String objects, or raw bytes. Invalidations are translated to the current content state, even if a queue of edits are still being processed. It is fully-compatible with reading the document content lazily.

  • Monitors text changes
  • Can be used to build a TokenProvider

TreeSitterClient provides APIs that can be both synchronous, asynchronous, or both depending on the state of the system. This kind of interface can be important when optimizing for flicker-free, low-latency highlighting live typing interactions like indenting.

Using it is quite involved - here's a little example:

import SwiftTreeSitter
import TreeSitterSwift // this parser is available via SPM (see SwiftTreeSitter's README.md)
import Neon

// step 1: setup

// construct the tree-sitter parser for the language you are interested in
let language = Language(language: tree_sitter_swift())

// construct your highlighting query
// remember, this is a one-time cost but can be expensive
let url = Bundle.main
              .resourceURL
              .appendingPathComponent("TreeSitterSwift_TreeSitterSwift.bundle")
              .appendingPathComponent("queries/highlights.scm")
let query = try! language.query(contentsOf: url)

// step 2: configure the client

let client = TreeSitterClient(language: language)

// this function will be called with a minimal set of text ranges
// that have become invalidated due to edits. These ranges
// always correspond to the *current* state of the text content,
// even if TreeSitterClient is currently processing edits in the
// background.
client.invalidationHandler = { indexSet in
    // highlighter.invalidate(.set(indexSet))
}

// step 3: inform it about content changes
// these APIs match up fairly closely with NSTextStorageDelegate,
// and are compatible with lazy evaluation of the text content

// call this *before* the content has been changed
client.willChangeContent(in: range)

// and call this *after*
client.didChangeContent(to: string, in: range, delta: delta, limit: limit)

// step 4: run queries
// you can execute these queries directly in the invalidationHandler, if desired

// Many tree-sitter highlight queries contain predicates. These are both expensive
// and complex to resolve. This is an optional feature - you can just skip it. Doing
// so makes the process both faster and simpler, but could result in lower-quality
// and even incorrect highlighting.

let provider: TreeSitterClient.TextProvider = { (range, _) -> String? in
   return nil
}

let range = NSMakeRange(0, 10) // for example
client.executeHighlightsQuery(query, in: range, textProvider: provider) { result in
   // Token values will tell you the highlights.scm name and range in your text
}

Suggestions or Feedback

We'd love to hear from you! Get in touch via twitter, an issue, or a pull request.

Please note that this project is released with a Contributor Code of Conduct. By participating in this project you agree to abide by its terms.

More Repositories

1

Impact

Crash capturing library for Apple platforms
C
431
star
2

Chime

An editor for macOS
Swift
303
star
3

SwiftTreeSitter

Swift API for the tree-sitter incremental parsing system
Swift
264
star
4

ConcurrencyPlus

Utilities for working with Swift Concurrency
Swift
253
star
5

Meter

Library for interacting with MetricKit
Swift
217
star
6

OperationPlus

NSOperation's missing pieces
Swift
128
star
7

SwiftLSPClient

A Swift library for interacting with Language Server Protocol implementations
Swift
103
star
8

LanguageClient

Language Server Protocol (LSP) client for Swift
Swift
96
star
9

TextStory

Happier, more flexible NSTextStorage
Swift
92
star
10

Rearrange

Swift library for working with ranges types: NSRange, IndexSet, and String.Index
Swift
69
star
11

WindowTreatment

Tools for happier work with NSWindow
Swift
63
star
12

LanguageServerProtocol

Swift library for working with Language Server Protocol (LSP)
Swift
56
star
13

ChimeKit

Framework for building Chime extensions
Swift
49
star
14

TextViewPlus

Make life better with NSTextView+TextKit 1/2
Swift
46
star
15

OAuthenticator

OAuth 2.0 request authentication
Swift
46
star
16

TextFormation

Rules system for live typing completions
Swift
46
star
17

Extendable

A set of utilities for more pleasant work with ExtensionKit
Swift
43
star
18

Wells

A lightweight diagnostics report submission system
Swift
40
star
19

AsyncXPCConnection

Concurrency support for NSXPCConnection
Swift
40
star
20

ProcessService

Host an executable within an XPC service
Swift
39
star
21

UITestingPlus

Utilities for working with XCUI testing
Swift
38
star
22

KeyCodes

AppKit Carbon key codes to UIKey-compatible enums
Swift
35
star
23

MeterReporter

Lightweight MetricKit-based diagnostics reporting
Swift
33
star
24

NicerTouchBar

Utilities for a more pleasant NSTouchBar development experience
Swift
33
star
25

TextViewBenchmark

A suite of performance tests for macOS text views
Swift
32
star
26

Flexer

Lexing library for Swift
Swift
31
star
27

ContainedDocument

Nested NSDocuments
Swift
26
star
28

JSONRPC

Swift library for JSON-RPC
Swift
25
star
29

ProcessEnv

Capture the shell environment of a Foundation app
Swift
25
star
30

ThemePark

A Swift library for working with syntax highlighting/IDE themes
Swift
25
star
31

Glyph

Make life with TextKit better
Swift
24
star
32

Dusk

Micro-framework to aid in supporting Dark Mode on macOS
Swift
21
star
33

ScrollViewPlus

A more pleasant NSScrollView experience
Swift
16
star
34

Background

Background Tasks and Networking
Swift
16
star
35

Welp

Tooling for macOS help books
Swift
15
star
36

XPCConnectionSession

Backwards-compatible implementation of XPCSession
Swift
13
star
37

EditorConfig

A Swift library for working with .editorconfig files
Swift
13
star
38

GlobPattern

Swift package to parse and evaluate glob patterns
Swift
13
star
39

Outline

Lazy SwiftUI wrapper for NSOutlineView
Swift
12
star
40

LanguageServer

Language Server Protocol server infrastructure for Swift
Swift
11
star
41

ImpactMeterAdapter

Impact crash reports as a Meter diagnostic source
Swift
11
star
42

Gramophone

Swift library for working with Extended Backus–Naur Form (EBNF) notation and grammars.
Swift
11
star
43

CoreSymbolication

Headers and package for CoreSymbolication
Swift
11
star
44

Ligature

Text selection, grouping, indentation, and manipulation in Swift
Swift
10
star
45

ViewPlus

Make working with NSView more pleasant
Swift
10
star
46

UnifiedLoggingPlus

Micro-framework for making OSLog more pleasant
Swift
10
star
47

Textbook

Easier text views and SwiftUI
Swift
10
star
48

chime-swift

A Chime extension for Swift
Swift
9
star
49

SwiftCoreSymbolication

Swift wrappers for CoreSymbolication
Swift
9
star
50

DebugAdapterProtocol

Swift library for working with Debug Adapter Protocol (DAP)
Swift
8
star
51

SourceView

A TextKit 2 `NSTextView` subclass built specifically to work with source code
Swift
7
star
52

RelativeCollections

Swift collection types that support efficient storage of order-relative values.
Swift
7
star
53

IBeam

A Swift library for multi-cursor support
Swift
7
star
54

paddleapi

Go implementation of the Paddle API
Go
6
star
55

MetricKitViewer

A really simple app for viewing MetricKit payloads
Swift
5
star
56

Lowlight

A simple syntax processing system that prioritizes latency over correctness
Swift
5
star
57

chime-ruby

A Chime extension for Ruby
Swift
4
star
58

chime-rust

A Chime extension for Rust
Swift
4
star
59

STTextView-Plugin-TextFormation

Source Code Typing Completions
Swift
4
star
60

gogsym

Go library for reading GSYM files
Go
3
star
61

chime-markdown

A Chime extension for Markdown
3
star
62

LanguageServerScripts

A collection of standardized scripts for managing LSP language servers with a Swift API
Shell
3
star
63

chime-elixir

A Chime extension for Elixir
Swift
2
star
64

binarycursor

Go binary data reader
Go
2
star
65

chime-python

A Chime extension for Rust
Swift
2
star
66

chime-go

A Chime extension for Go
Swift
1
star
67

chime-clojure

A Chime extension for Clojure
Swift
1
star
68

marpa-xcframework

XCFramework wrapper for libmarpa
C
1
star
69

Borderline

System for working with text line numbers in Swift
Swift
1
star