Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds StringSyntaxHighlighter #363

Merged
merged 8 commits into from
Mar 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Sources/Runestone/Documentation.docc/Documentation.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,12 +61,14 @@ Syntax highlighting is based on GitHub's [Tree-sitter](https://github.com/tree-s

- <doc:Syntax-Highlighting-the-Text>
- <doc:AddingATreeSitterLanguage>
- <doc:SyntaxHighlightingAString>
- ``LanguageMode``
- ``PlainTextLanguageMode``
- ``TreeSitterLanguageMode``
- ``TreeSitterLanguage``
- ``TreeSitterLanguageProvider``
- ``SyntaxNode``
- ``StringSyntaxHighlighter``

### Indentation

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# ``StringSyntaxHighlighter``

## Example

Create a syntax highlighter by passing a theme and language, and then call the ``StringSyntaxHighlighter/syntaxHighlight(_:)`` method to syntax highlight the provided text.

```swift
let syntaxHighlighter = StringSyntaxHighlighter(
theme: TomorrowTheme(),
language: .javaScript
)
let attributedString = syntaxHighlighter.syntaxHighlight(
"""
function fibonacci(num) {
if (num <= 1) {
return 1
}
return fibonacci(num - 1) + fibonacci(num - 2)
}
"""
)
```

## Topics

### Essentials

- <doc:SyntaxHighlightingAString>
- ``StringSyntaxHighlighter/syntaxHighlight(_:)``

### Initialing the Syntax Highlighter

- ``StringSyntaxHighlighter/init(theme:language:languageProvider:)``

### Configuring the Appearance

- ``StringSyntaxHighlighter/theme``
- ``StringSyntaxHighlighter/kern``
- ``StringSyntaxHighlighter/lineHeightMultiplier``
- ``StringSyntaxHighlighter/tabLength``

### Specifying the Language

- ``StringSyntaxHighlighter/language``
- ``StringSyntaxHighlighter/languageProvider``
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Syntax Highlighting a String

Learn how to syntax hightlight a string without needing to create a TextView.

## Overview

The <doc:StringSyntaxHighlighter> can be used to syntax highlight a string without needing to create a <doc:TextView>.

Before reading this article, make sure that you have follow the guides on <doc:AddingATreeSitterLanguage> and <doc:CreatingATheme>.


## Creating an Attributed String

Create an instance of <doc:StringSyntaxHighlighter> by supplying the theme containing the colors and fonts to be used for syntax highlighting the text, as well as the language to use when parsing the text.

```swift
let syntaxHighlighter = StringSyntaxHighlighter(
theme: TomorrowTheme(),
language: .javaScript
)
```

If the language has any embedded languages, you will need to pass an object conforming to <doc:TreeSitterLanguageProvider>, which provides the syntax highlighter with additional languages.

Apply customizations to the syntax highlighter as needed.

```swift
syntaxHighlighter.kern = 0.3
syntaxHighlighter.lineHeightMultiplier = 1.2
syntaxHighlighter.tabLength = 2
```

With the syntax highlighter created and configured, we can syntax highlight the text.

```swift
let attributedString = syntaxHighlighter.syntaxHighlight(
"""
function fibonacci(num) {
if (num <= 1) {
return 1
}
return fibonacci(num - 1) + fibonacci(num - 2)
}
"""
)
```

The attributed string can be displayed using a UILabel or UITextView.
105 changes: 105 additions & 0 deletions Sources/Runestone/StringSyntaxHighlighter.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import UIKit

/// Syntax highlights a string.
///
/// An instance of `StringSyntaxHighlighter` can be used to syntax highlight a string without needing to create a `TextView`.
public final class StringSyntaxHighlighter {
/// The theme to use when syntax highlighting the text.
public var theme: Theme
/// The language to use when parsing the text.
public var language: TreeSitterLanguage
/// Object that can provide embedded languages on demand. A strong reference will be stored to the language provider.
public var languageProvider: TreeSitterLanguageProvider?
/// The number of points by which to adjust kern.
///
/// The default value is 0 meaning that kerning is disabled.
public var kern: CGFloat = 0
/// The tab length determines the width of the tab measured in space characers.
///
/// The default value is 4 meaning that a tab is four spaces wide.
public var tabLength: Int = 4
/// The line-height is multiplied with the value.
public var lineHeightMultiplier: CGFloat = 1

/// Creates an object that can syntax highlight a text.
/// - Parameters:
/// - theme: The theme to use when syntax highlighting the text.
/// - language: The language to use when parsing the text
/// - languageProvider: Object that can provide embedded languages on demand. A strong reference will be stored to the language provider..
public init(
theme: Theme = DefaultTheme(),
language: TreeSitterLanguage,
languageProvider: TreeSitterLanguageProvider? = nil
) {
self.theme = theme
self.language = language
self.languageProvider = languageProvider
}

required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

/// Syntax highlights the text using the configured syntax highlighter.
/// - Parameter text: The text to be syntax highlighted.
/// - Returns: An attributed string containing the syntax highlighted text.
public func syntaxHighlight(_ text: String) -> NSAttributedString {
let mutableString = NSMutableString(string: text)
let stringView = StringView(string: mutableString)
let lineManager = LineManager(stringView: stringView)
lineManager.rebuild()
let languageMode = TreeSitterLanguageMode(language: language, languageProvider: languageProvider)
let internalLanguageMode = languageMode.makeInternalLanguageMode(
stringView: stringView,
lineManager: lineManager
)
internalLanguageMode.parse(mutableString)
let tabWidth = TabWidthMeasurer.tabWidth(tabLength: tabLength, font: theme.font)
let mutableAttributedString = NSMutableAttributedString(string: text)
let defaultAttributes = DefaultStringAttributes(
textColor: theme.textColor,
font: theme.font,
kern: kern,
tabWidth: tabWidth
)
defaultAttributes.apply(to: mutableAttributedString)
applyLineHeightMultiplier(to: mutableAttributedString)
let byteRange = ByteRange(from: 0, to: text.byteCount)
let syntaxHighlighter = internalLanguageMode.createLineSyntaxHighlighter()
syntaxHighlighter.theme = theme
let syntaxHighlighterInput = LineSyntaxHighlighterInput(
attributedString: mutableAttributedString,
byteRange: byteRange
)
syntaxHighlighter.syntaxHighlight(syntaxHighlighterInput)
return mutableAttributedString
}
}

private extension StringSyntaxHighlighter {
private func applyLineHeightMultiplier(to attributedString: NSMutableAttributedString) {
let scaledLineHeight = theme.font.totalLineHeight * lineHeightMultiplier
let mutableParagraphStyle = getMutableParagraphStyle(from: attributedString)
mutableParagraphStyle.lineSpacing = scaledLineHeight - theme.font.totalLineHeight
let range = NSRange(location: 0, length: attributedString.length)
attributedString.beginEditing()
attributedString.removeAttribute(.paragraphStyle, range: range)
attributedString.addAttribute(.paragraphStyle, value: mutableParagraphStyle, range: range)
attributedString.endEditing()
}

private func getMutableParagraphStyle(
from attributedString: NSMutableAttributedString
) -> NSMutableParagraphStyle {
guard let attributeValue = attributedString.attribute(.paragraphStyle, at: 0, effectiveRange: nil) else {
return NSMutableParagraphStyle()
}
guard let paragraphStyle = attributeValue as? NSParagraphStyle else {
fatalError("Expected .paragraphStyle attribute to be instance of NSParagraphStyle")
}
guard let mutableParagraphStyle = paragraphStyle.mutableCopy() as? NSMutableParagraphStyle else {
fatalError("Expected mutableCopy() to return an instance of NSMutableParagraphStyle")
}
return mutableParagraphStyle
}
}
Loading