Skip to content

Commit

Permalink
initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
fdgogogo committed Jan 30, 2024
0 parents commit e003916
Show file tree
Hide file tree
Showing 10 changed files with 592 additions and 0 deletions.
90 changes: 90 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# Xcode
#
# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore

## User settings
xcuserdata/

## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9)
*.xcscmblueprint
*.xccheckout

## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4)
build/
DerivedData/
*.moved-aside
*.pbxuser
!default.pbxuser
*.mode1v3
!default.mode1v3
*.mode2v3
!default.mode2v3
*.perspectivev3
!default.perspectivev3

## Obj-C/Swift specific
*.hmap

## App packaging
*.ipa
*.dSYM.zip
*.dSYM

## Playgrounds
timeline.xctimeline
playground.xcworkspace

# Swift Package Manager
#
# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
# Packages/
# Package.pins
Package.resolved
# *.xcodeproj

# Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata
# hence it is not needed unless you have added a package configuration file to your project
.swiftpm

.build/

# CocoaPods
#
# We recommend against adding the Pods directory to your .gitignore. However
# you should judge for yourself, the pros and cons are mentioned at:
# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
#
Pods/
#
# Add this line if you want to avoid checking in source code from the Xcode workspace
*.xcworkspace

# Carthage
#
# Add this line if you want to avoid checking in source code from Carthage dependencies.
# Carthage/Checkouts

Carthage/Build/

# Accio dependency management
Dependencies/
.accio/

# fastlane
#
# It is recommended to not store the screenshots in the git repo.
# Instead, use fastlane to re-generate the screenshots whenever they are needed.
# For more information about the recommended setup visit:
# https://docs.fastlane.tools/best-practices/source-control/#source-control

fastlane/report.xml
fastlane/Preview.html
fastlane/screenshots/**/*.png
fastlane/test_output

# Code Injection
#
# After new code Injection tools there's a generated folder /iOSInjectionProject
# https://github.com/johnno1962/injectionforxcode

iOSInjectionProject/
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2024 Jiaan Fang

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
29 changes: 29 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// swift-tools-version: 5.8
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
name: "NeonCodeEditLanguagesPlugin",
platforms: [.macOS(.v13)],
products: [
.library(
name: "NeonCodeEditLanguagesPlugin",
targets: ["NeonCodeEditLanguagesPlugin"]),
],
dependencies: [
.package(url: "https://github.com/krzyzanowskim/STTextView", from: "0.8.13"),
.package(url: "https://github.com/ChimeHQ/Neon.git", from: "0.6.0"),
.package(url: "https://github.com/CodeEditApp/CodeEditLanguages", from: "0.1.18")
],
targets: [
.target(
name: "NeonCodeEditLanguagesPlugin",
dependencies: [
.product(name: "STTextView", package: "STTextView"),
"Neon",
.product(name: "CodeEditLanguages", package: "CodeEditLanguages")
]
)
]
)
51 changes: 51 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
[STTextView](https://github.com/krzyzanowskim/STTextView) Source Code Syntax Highlighting with [TreeSitter](https://tree-sitter.github.io/tree-sitter/), [Neon](https://github.com/ChimeHQ/Neon) and [CodeEditLanguages](https://github.com/CodeEditApp/CodeEditLanguages).

Most code in this repo is borrowed from [STTextView-Plugin-Neon](https://github.com/krzyzanowskim/STTextView-Plugin-Neon), with some adaptations to work with CodeEditLanguages.


## Installation

Add the plugin package as a dependency of your application, then register/add it to the STTextView instance:

```swift
import NeonCodeEditLanguagesPlugin

textView.addPlugin(
NeonCodeEditLanguagesPlugin(
theme: .default,
language: .go
)
)
```

SwiftUI:
```swift
import SwiftUI
import STTextViewUI
import NeonCodeEditLanguagesPlugin

struct ContentView: View {
@State private var text: AttributedString = ""
@State private var selection: NSRange?
var body: some View {
STTextViewUI.TextView(
text: $text,
selection: $selection,
options: [.wrapLines, .highlightSelectedLine],
plugins: [NeonCodeEditLanguagesPlugin(theme: .default, language: .go)]
)
.textViewFont(.monospacedDigitSystemFont(ofSize: NSFont.systemFontSize, weight: .regular))
.onAppear {
loadContent()
}
}

private func loadContent() {
// (....)
self.text = AttributedString(string)
}
}
```

<img width="612" alt="Default Theme" src="https://github.com/krzyzanowskim/STTextView-Plugin-Neon/assets/758033/03c35889-da7f-48c1-8982-77430eb69a20">

104 changes: 104 additions & 0 deletions Sources/NeonCodeEditLanguagesPlugin/Coordinator.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import Cocoa
import STTextView

import Neon
import TreeSitterClient
import SwiftTreeSitter

import CodeEditLanguages

public class Coordinator {
private(set) var highlighter: Neon.Highlighter?
private let language: CodeLanguage
private let tsLanguage: SwiftTreeSitter.Language
private let tsClient: TreeSitterClient
private var prevViewportRange: NSTextRange?

init(textView: STTextView, theme: Theme, language: CodeLanguage) {
tsLanguage = language.language!
self.language = language

tsClient = try! TreeSitterClient(language: tsLanguage) { codePointIndex in
guard let location = textView.textContentManager.location(at: codePointIndex),
let position = textView.textContentManager.position(location)
else {
return .zero
}

return Point(row: position.row, column: position.column)
}


tsClient.invalidationHandler = { [weak self] indexSet in
DispatchQueue.main.async {
self?.highlighter?.invalidate(.set(indexSet))
}
}

// set textview default font to theme default font
textView.font = theme.tokens[.default]?.font?.value ?? textView.font

DispatchQueue.main.async {
self.highlighter = Neon.Highlighter(textInterface: STTextViewSystemInterface(textView: textView) { neonToken in
var attributes: [NSAttributedString.Key: Any] = [:]
if let tvFont = textView.font {
attributes[.font] = tvFont
}

if let themeValue = theme.tokens[TokenName(neonToken.name)] {
attributes[.foregroundColor] = themeValue.color.value

if let font = themeValue.font?.value {
attributes[.font] = font
}
} else if let themeValue = theme.tokens[.default]{
attributes[.foregroundColor] = themeValue.color.value

if let font = themeValue.font?.value {
attributes[.font] = font
}
}

return !attributes.isEmpty ? attributes : nil
}, tokenProvider: self.tokenProvider(textContentManager: textView.textContentManager))
}

// initial parse of the whole content
tsClient.willChangeContent(in: NSRange(textView.textContentManager.documentRange, in: textView.textContentManager))
tsClient.didChangeContent(in: NSRange(textView.textContentManager.documentRange, in: textView.textContentManager), delta: textView.textContentManager.length, limit: textView.textContentManager.length, readHandler: Parser.readFunction(for: textView.textContentManager.attributedString(in: nil)?.string ?? ""), completionHandler: {})
}

private func tokenProvider(textContentManager: NSTextContentManager) -> Neon.TokenProvider? {
guard let highlightsQuery = try? tsLanguage.query(contentsOf: language.queryURL!) else {
return nil
}

return tsClient.tokenProvider(with: highlightsQuery) { range, _ in
guard range.isEmpty == false else { return nil }
return textContentManager.attributedString(in: NSTextRange(range, provider: textContentManager))?.string
}
}

func updateViewportRange(_ range: NSTextRange?) {
if range != prevViewportRange {
DispatchQueue.main.async {
self.highlighter?.visibleContentDidChange()
}
}
prevViewportRange = range
}

func willChangeContent(in range: NSRange) {
tsClient.willChangeContent(in: range)
}

func didChangeContent(_ textContentManager: NSTextContentManager, in range: NSRange, delta: Int, limit: Int) {
/// TODO: Instead get the *whole* string over and over (can be expensive for large documents)
/// implement maybe a reader function that read what needed only (is it possible?)
if let str = textContentManager.attributedString(in: nil)?.string {
let readFunction = Parser.readFunction(for: str)
tsClient.didChangeContent(in: range, delta: delta, limit: limit, readHandler: readFunction, completionHandler: {})
}

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import Cocoa

import STTextView

import CodeEditLanguages

public struct NeonCodeEditLanguagesPlugin: STPlugin {
private let theme: Theme
private let language: CodeLanguage

public init(theme: Theme = .default, language: CodeLanguage) {
self.theme = theme
self.language = language
}

public func setUp(context: any Context) {

context.events.onWillChangeText { affectedRange in
let range = NSRange(affectedRange, in: context.textView.textContentManager)
context.coordinator.willChangeContent(in: range)
}

context.events.onDidChangeText { affectedRange, replacementString in
guard let replacementString else { return }

let range = NSRange(affectedRange, in: context.textView.textContentManager)
context.coordinator.didChangeContent(context.textView.textContentManager, in: range, delta: replacementString.utf16.count - range.length, limit: context.textView.textContentManager.length)
}

context.events.onDidLayoutViewport { viewportRange in
context.coordinator.updateViewportRange(viewportRange)
}
}

public func makeCoordinator(context: CoordinatorContext) -> Coordinator {
Coordinator(textView: context.textView, theme: theme, language: language)
}

}

Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import Cocoa
import STTextView
import Neon

class STTextViewSystemInterface: TextSystemInterface {

typealias AttributeProvider = (Neon.Token) -> [NSAttributedString.Key: Any]?

private let textView: STTextView
private let attributeProvider: AttributeProvider

init(textView: STTextView, attributeProvider: @escaping AttributeProvider) {
self.textView = textView
self.attributeProvider = attributeProvider
}

func clearStyle(in range: NSRange) {
guard let textRange = NSTextRange(range, in: textView.textContentManager) else {
assertionFailure()
return
}

textView.textLayoutManager.removeRenderingAttribute(.foregroundColor, for: textRange)
if let defaultFont = textView.font {
textView.addAttributes([.font: defaultFont], range: range)
}
}

func applyStyle(to token: Neon.Token) {
print(token)
guard let attrs = attributeProvider(token),
let textRange = NSTextRange(token.range, in: textView.textContentManager)
else {
return
}

for attr in attrs {
textView.textLayoutManager.addRenderingAttribute(attr.key, value: attr.value, for: textRange)
}
}

var length: Int {
textView.textContentManager.length
}

var visibleRange: NSRange {
guard let viewportRange = textView.textLayoutManager.textViewportLayoutController.viewportRange else {
return .zero
}

return NSRange(viewportRange, provider: textView.textContentManager)
}
}
Loading

0 comments on commit e003916

Please sign in to comment.