Build Status Platforms Documentation Discord

Neon

一个用于高效、灵活的基于内容文本样式化的 Swift 库。

Neon 非常注重效率和灵活性。它位于您的文本系统和您获取语义 token 信息的位置之间。 Neon 是为语法高亮而开发的,它可以很好地满足这一需求。但是,它的通用性更强,可以用于任何需要管理基于范围的内容状态的系统。

很多人都在寻找一个开箱即用的编辑器 View 子类来完成所有工作。这是一个更底层的库。但是,您可以使用 Neon 来驱动此类视图的高亮显示。

注意

main 分支上的代码尚未完全准备好发布。但是,所有文档和开发工作都在那里,应该被认为是可用的。

安装

dependencies: [
    .package(url: "https://github.com/ChimeHQ/Neon", branch: "main")
],
targets: [
    .target(
        name: "MyTarget",
        dependencies: [
            "Neon",
            .product(name: "TreeSitterClient", package: "Neon"),
            .product(name: "RangeState", package: "Neon"),
        ]
    ),
]

概念

Neon 由三个部分组成:核心库、RangeStateTreeSitterClient

RangeState

Neon 的最低级别组件称为 RangeState。该模块包含用于系统其余部分的核心构建块。 RangeState 围绕混合同步/异步执行的思想构建。使一切异步容易得多,但这使得为小型文档提供低延迟路径变得不可能。它与内容无关。

其中许多支持可版本化的内容。如果您正在使用支持高效版本控制的后备存储结构,例如分片表,则向 RangeState 表达这一点可以提高其效率。

Neon

顶级模块包括用于管理文本样式的系统。它也与文本系统无关。它对文本的存储、显示或样式设置方式几乎不做任何假设。它还包括一些用于库存 AppKit 和 UIKit 系统的组件。提供这些是为了方便集成,而不是为了获得最佳性能。

还有一个 示例项目,演示了如何为 macOS 和 iOS 使用 TextViewHighlighter

TextKit 集成

在传统的 NSTextStorage 支持的系统(TextKit 1 和 2)中,实现无闪烁的按键高亮显示可能具有挑战性。您需要知道文本更改已被系统充分处理以至于可以进行样式设置的时间。文本更改生命周期中的这一点并非由 NSTextStorageNSLayoutManager 本身支持。它需要一个 NSTextStorage 子类。这样的子类 TSYTextStorageTextStory 中可用。

但是,即使这样还不够。您仍然需要精确控制失效和样式设置的时间。这就是 RangeInvalidationBuffer 的用武之地。

我尚未找到使用 TextKit 2 执行此操作的方法,如果没有新的 API,这可能是不可能的。

性能

Neon 的性能高度依赖于文本系统集成。每个方面都很重要,因为周围都存在性能瓶颈。但是,优先级范围计算(大多数文本视图的可见集)尤其重要。 使用 TextKit 1 正确执行此操作非常具有挑战性,而使用 TextKit 2 则极其困难。

底线:Neon 非常高效。瓶颈可能是您的解析系统和文本视图。它也可以很好地隐藏解析性能问题。但是,这并不意味着它是完美的。总有改进的余地,如果您怀疑有问题,请提出问题。

TreeSitterClient

该库是 SwiftTreeSitter 的混合同步/异步接口。 它的特点是

Tree-sitter 对每种语言使用单独的已编译解析器。 有多种方法可以将 tree-sitter 解析器与 SwiftTreeSitter 结合使用。 有关详细信息,请查看该项目。

Token 数据源

Neon 旨在同时接受和叠加来自多个源的 token 数据。 这是一个如何使用它的真实示例

您可能感兴趣的第一遍系统的一个示例是Lowlight

主题化

高亮主题实际上只是从语义标签到样式的映射。 Token 数据源应用语义标签,TextSystemInterface 使用这些标签来查找样式。

这种分离使您可以非常轻松地以对您想要支持的任何主题格式最有意义的方式进行此查找。 这也是一个方便的位置,可以将来自您的数据源的语义标签调整/修改为标准化形式。

如果您正在寻找一些帮助,请查看ThemePark

用法

TreeSitterClient

这是一个使用 TreeSitterClient 的最小示例。它很复杂,但应该让您了解需要做什么。

import Neon
import SwiftTreeSitter
import TreeSitterClient

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

// assume we have a text view available that has been loaded with some Swift source

let languageConfig = try LanguageConfiguration(
    tree_sitter_swift(),
    name: "Swift"
)

let clientConfig = TreeSitterClient.Configuration(
    languageProvider: { identifier in
        // look up nested languages by identifier here. If done
        // asynchronously, inform the client they are ready with
        // `languageConfigurationChanged(for:)`
        return nil
    },
    contentSnapshotProvider: { [textView] length in
        // given a maximum needed length, produce a `ContentSnapshot` structure
        // that will be used to access immutable text data

        // this can work for any system that efficiently produce a `String`
        return .init(string: textView.string)
    },
    lengthProvider: { [textView] in
        textView.string.utf16.count
    },
    invalidationHandler: { set in
        // take action on invalidated regions of the text
    },
    locationTransformer: { location in
        // optionally, use the UTF-16 location to produce a line-relative Point structure.
        return nil
    }
)

let client = try TreeSitterClient(
    rootLanguageConfig: languageConfig,
    configuration: clientConfig
)

let source = textView.string

let provider = source.predicateTextProvider

// this uses the synchronous query API, but with the `.required` mode, which will force the client
// to do all processing necessary to satisfy the request.
let highlights = try client.highlights(in: NSRange(0..<24), provider: provider, mode: .required)!

print("highlights:", highlights)

TreeSitterClient 还可以执行静态高亮显示。这对于将样式应用于字符串非常有用。

let languageConfig = try LanguageConfiguration(
    tree_sitter_swift(),
    name: "Swift"
)

let attrProvider: TokenAttributeProvider = { token in
    return [.foregroundColor: NSColor.red]
}

// produce an AttributedString
let highlightedSource = try await TreeSitterClient.highlight(
    string: source,
    attributeProvider: attrProvider,
    rootLanguageConfig: languageConfig,
    languageProvider: { _ in nil }
)

贡献和协作

我很乐意收到您的来信! 问题或 pull 请求效果很好。 一个Discord 服务器也可用于实时帮助,但我强烈倾向于以文档的形式回答。

我更喜欢协作,如果您有类似的项目,我很乐意找到合作的方式。

我更喜欢使用制表符进行缩进以提高可访问性。 但是,我宁愿您使用您想要的系统并提出 PR,也不愿因为空格而犹豫。

通过参与本项目,您同意遵守贡献者行为准则