Skip to content

Commit cf85789

Browse files
TreeSitter Tag Auto Close (#250)
### Description Fixes a few bugs with #247 and converts it to use tree-sitter rather than a regex-based implementation. This should be faster on larger documents and makes it more robust to edge cases in tag regexes. This also handles newlines correctly, as the old PR caused the editor to no longer be able to delete newlines Also fixes a small bug in the `TreeSitterClient` that caused *every* query to be dispatched to main asynchronously. This was the cause for a few visual oddities like flashing colors when changing themes. This also improves highlighting while scrolling fast as most highlights are processed synchronously. - Removes extensions on `NewlineProcessingFilter` - Cleans up `TagFilter` - Moves all newline processing to the one filter - Use tree-sitter for tag completion, supporting the following languages: HTML, JSX, TSX - Adds a few methods to `TreeSitterClient` for synchronously querying the tree sitter tree. - Adds a new `TreeSitterClientExecutor` class that the client uses to execute operations safely asynchronously and synchronously. - This is extremely useful for testing, as it allows the tests to force all operations to happen synchronously. - Adds a check to `dispatchMain` to see if the thread is already the main thread (meaning no async dispatch) ### Related Issues * #244 * Discussion on discord [Here](https://discord.com/channels/951544472238444645/1242238782653075537) ### Checklist - [x] I read and understood the [contributing guide](https://github.com/CodeEditApp/CodeEdit/blob/main/CONTRIBUTING.md) as well as the [code of conduct](https://github.com/CodeEditApp/CodeEdit/blob/main/CODE_OF_CONDUCT.md) - [x] The issues this PR addresses are related to each other - [x] My changes generate no new warnings - [x] My code builds and runs on my machine - [x] My changes are all related to the related issue above - [x] I documented my code ### Screenshots https://github.com/CodeEditApp/CodeEditSourceEditor/assets/35942988/8fc559a4-15c9-4b4e-a3aa-57c86c57f7c9 https://github.com/CodeEditApp/CodeEditSourceEditor/assets/35942988/a209b40f-7aa3-4105-aa37-5608e8b4bcdb
1 parent 1740482 commit cf85789

22 files changed

+893
-280
lines changed

Example/CodeEditSourceEditorExample/CodeEditSourceEditorExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved

+2-2
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@
1414
"kind" : "remoteSourceControl",
1515
"location" : "https://github.com/CodeEditApp/CodeEditTextView.git",
1616
"state" : {
17-
"revision" : "86b980464bcb67693e2053283c7a99bdc6f358bc",
18-
"version" : "0.7.3"
17+
"revision" : "80911be6bcdae5e35ef5ed351adf6dda9b57e555",
18+
"version" : "0.7.4"
1919
}
2020
},
2121
{
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<Scheme
3+
LastUpgradeVersion = "1540"
4+
version = "1.7">
5+
<BuildAction
6+
parallelizeBuildables = "YES"
7+
buildImplicitDependencies = "YES"
8+
buildArchitectures = "Automatic">
9+
<BuildActionEntries>
10+
<BuildActionEntry
11+
buildForTesting = "YES"
12+
buildForRunning = "YES"
13+
buildForProfiling = "YES"
14+
buildForArchiving = "YES"
15+
buildForAnalyzing = "YES">
16+
<BuildableReference
17+
BuildableIdentifier = "primary"
18+
BlueprintIdentifier = "6C1365292B8A7B94004A1D18"
19+
BuildableName = "CodeEditSourceEditorExample.app"
20+
BlueprintName = "CodeEditSourceEditorExample"
21+
ReferencedContainer = "container:CodeEditSourceEditorExample.xcodeproj">
22+
</BuildableReference>
23+
</BuildActionEntry>
24+
</BuildActionEntries>
25+
</BuildAction>
26+
<TestAction
27+
buildConfiguration = "Debug"
28+
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
29+
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
30+
shouldUseLaunchSchemeArgsEnv = "YES"
31+
shouldAutocreateTestPlan = "YES">
32+
</TestAction>
33+
<LaunchAction
34+
buildConfiguration = "Debug"
35+
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
36+
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
37+
launchStyle = "0"
38+
useCustomWorkingDirectory = "NO"
39+
ignoresPersistentStateOnLaunch = "NO"
40+
debugDocumentVersioning = "YES"
41+
debugServiceExtension = "internal"
42+
allowLocationSimulation = "YES">
43+
<BuildableProductRunnable
44+
runnableDebuggingMode = "0">
45+
<BuildableReference
46+
BuildableIdentifier = "primary"
47+
BlueprintIdentifier = "6C1365292B8A7B94004A1D18"
48+
BuildableName = "CodeEditSourceEditorExample.app"
49+
BlueprintName = "CodeEditSourceEditorExample"
50+
ReferencedContainer = "container:CodeEditSourceEditorExample.xcodeproj">
51+
</BuildableReference>
52+
</BuildableProductRunnable>
53+
</LaunchAction>
54+
<ProfileAction
55+
buildConfiguration = "Release"
56+
shouldUseLaunchSchemeArgsEnv = "YES"
57+
savedToolIdentifier = ""
58+
useCustomWorkingDirectory = "NO"
59+
debugDocumentVersioning = "YES">
60+
<BuildableProductRunnable
61+
runnableDebuggingMode = "0">
62+
<BuildableReference
63+
BuildableIdentifier = "primary"
64+
BlueprintIdentifier = "6C1365292B8A7B94004A1D18"
65+
BuildableName = "CodeEditSourceEditorExample.app"
66+
BlueprintName = "CodeEditSourceEditorExample"
67+
ReferencedContainer = "container:CodeEditSourceEditorExample.xcodeproj">
68+
</BuildableReference>
69+
</BuildableProductRunnable>
70+
</ProfileAction>
71+
<AnalyzeAction
72+
buildConfiguration = "Debug">
73+
</AnalyzeAction>
74+
<ArchiveAction
75+
buildConfiguration = "Release"
76+
revealArchiveInOrganizer = "YES">
77+
</ArchiveAction>
78+
</Scheme>

Example/CodeEditSourceEditorExample/CodeEditSourceEditorExample/Documents/CodeEditSourceEditorExampleDocument.swift

+1-4
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,7 @@ struct CodeEditSourceEditorExampleDocument: FileDocument {
1717

1818
static var readableContentTypes: [UTType] {
1919
[
20-
.sourceCode,
21-
.plainText,
22-
.delimitedText,
23-
.script
20+
.item
2421
]
2522
}
2623

Package.resolved

+2-2
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@
1414
"kind" : "remoteSourceControl",
1515
"location" : "https://github.com/CodeEditApp/CodeEditTextView.git",
1616
"state" : {
17-
"revision" : "86b980464bcb67693e2053283c7a99bdc6f358bc",
18-
"version" : "0.7.3"
17+
"revision" : "80911be6bcdae5e35ef5ed351adf6dda9b57e555",
18+
"version" : "0.7.4"
1919
}
2020
},
2121
{

Package.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ let package = Package(
1717
// A fast, efficient, text view for code.
1818
.package(
1919
url: "https://github.com/CodeEditApp/CodeEditTextView.git",
20-
from: "0.7.3"
20+
from: "0.7.4"
2121
),
2222
// tree-sitter languages
2323
.package(

Sources/CodeEditSourceEditor/Controller/CursorPosition.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ public struct CursorPosition: Sendable, Codable, Equatable {
5050
/// - range: The range of the position.
5151
/// - line: The line of the position.
5252
/// - column: The column of the position.
53-
init(range: NSRange, line: Int, column: Int) {
53+
package init(range: NSRange, line: Int, column: Int) {
5454
self.range = range
5555
self.line = line
5656
self.column = column

Sources/CodeEditSourceEditor/Controller/TextViewController+LoadView.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ extension TextViewController {
112112

113113
NSEvent.addLocalMonitorForEvents(matching: .keyDown) { event in
114114
guard self.view.window?.firstResponder == self.textView else { return event }
115-
let charactersIgnoringModifiers = event.charactersIgnoringModifiers
115+
// let charactersIgnoringModifiers = event.charactersIgnoringModifiers
116116
let commandKey = NSEvent.ModifierFlags.command.rawValue
117117
let modifierFlags = event.modifierFlags.intersection(.deviceIndependentFlagsMask).rawValue
118118
if modifierFlags == commandKey && event.charactersIgnoringModifiers == "/" {

Sources/CodeEditSourceEditor/Controller/TextViewController+TextFormation.swift

+16-34
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,10 @@ extension TextViewController {
3737
// Filters
3838

3939
setUpOpenPairFilters(pairs: BracketPairs.allValues)
40+
setUpTagFilter()
4041
setUpNewlineTabFilters(indentOption: indentOption)
4142
setUpDeletePairFilters(pairs: BracketPairs.allValues)
4243
setUpDeleteWhitespaceFilter(indentOption: indentOption)
43-
setUpTagFilter()
4444
}
4545

4646
/// Returns a `TextualIndenter` based on available language configuration.
@@ -92,15 +92,13 @@ extension TextViewController {
9292
}
9393

9494
private func setUpTagFilter() {
95-
let filter = TagFilter(language: self.language.tsName)
96-
textFilters.append(filter)
97-
}
98-
99-
func updateTagFilter() {
100-
textFilters.removeAll { $0 is TagFilter }
101-
102-
// Add new tagfilter with the updated language
103-
textFilters.append(TagFilter(language: self.language.tsName))
95+
guard let treeSitterClient, language.id.shouldProcessTags() else { return }
96+
textFilters.append(TagFilter(
97+
language: self.language,
98+
indentOption: indentOption,
99+
lineEnding: textView.layoutManager.detectedLineEnding,
100+
treeSitterClient: treeSitterClient
101+
))
104102
}
105103

106104
/// Determines whether or not a text mutation should be applied.
@@ -123,30 +121,14 @@ extension TextViewController {
123121
)
124122

125123
for filter in textFilters {
126-
if let newlineFilter = filter as? NewlineProcessingFilter {
127-
let action = mutation.applyWithTagProcessing(
128-
in: textView,
129-
using: newlineFilter,
130-
with: whitespaceProvider, indentOption: indentOption
131-
)
132-
switch action {
133-
case .none:
134-
continue
135-
case .stop:
136-
return true
137-
case .discard:
138-
return false
139-
}
140-
} else {
141-
let action = filter.processMutation(mutation, in: textView, with: whitespaceProvider)
142-
switch action {
143-
case .none:
144-
continue
145-
case .stop:
146-
return true
147-
case .discard:
148-
return false
149-
}
124+
let action = filter.processMutation(mutation, in: textView, with: whitespaceProvider)
125+
switch action {
126+
case .none:
127+
continue
128+
case .stop:
129+
return true
130+
case .discard:
131+
return false
150132
}
151133
}
152134

Sources/CodeEditSourceEditor/Controller/TextViewController.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ public class TextViewController: NSViewController {
3939
public var language: CodeLanguage {
4040
didSet {
4141
highlighter?.setLanguage(language: language)
42-
updateTagFilter()
42+
setUpTextFormation()
4343
}
4444
}
4545

Sources/CodeEditSourceEditor/Extensions/NewlineProcessingFilter+TagHandling.swift

-102
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
//
2+
// Node+filterChildren.swift
3+
// CodeEditSourceEditor
4+
//
5+
// Created by Khan Winter on 5/29/24.
6+
//
7+
8+
import SwiftTreeSitter
9+
10+
extension Node {
11+
func firstChild(`where` isMatch: (Node) -> Bool) -> Node? {
12+
for idx in 0..<childCount {
13+
guard let node = child(at: idx) else { continue }
14+
if isMatch(node) {
15+
return node
16+
}
17+
}
18+
19+
return nil
20+
}
21+
22+
func mapChildren<T>(_ callback: (Node) -> T) -> [T] {
23+
var retVal: [T] = []
24+
for idx in 0..<childCount {
25+
guard let node = child(at: idx) else { continue }
26+
retVal.append(callback(node))
27+
}
28+
return retVal
29+
}
30+
31+
func filterChildren(_ isIncluded: (Node) -> Bool) -> [Node] {
32+
var retVal: [Node] = []
33+
for idx in 0..<childCount {
34+
guard let node = child(at: idx) else { continue }
35+
if isIncluded(node) {
36+
retVal.append(node)
37+
}
38+
}
39+
40+
return retVal
41+
}
42+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
//
2+
// NewlineProcessingFilter+TagHandling.swift
3+
// CodeEditSourceEditor
4+
//
5+
// Created by Roscoe Rubin-Rottenberg on 5/19/24.
6+
//
7+
8+
import Foundation
9+
import TextStory
10+
import TextFormation
11+
12+
// Helper extension to extract capture groups
13+
extension String {
14+
func groups(for regexPattern: String) -> [String]? {
15+
guard let regex = try? NSRegularExpression(pattern: regexPattern) else { return nil }
16+
let nsString = self as NSString
17+
let results = regex.matches(in: self, range: NSRange(location: 0, length: nsString.length))
18+
return results.first.map { result in
19+
(1..<result.numberOfRanges).compactMap {
20+
result.range(at: $0).location != NSNotFound ? nsString.substring(with: result.range(at: $0)) : nil
21+
}
22+
}
23+
}
24+
}

0 commit comments

Comments
 (0)