Skip to content
This repository was archived by the owner on Aug 27, 2023. It is now read-only.

Commit a512a5a

Browse files
committed
Firefly plugin
1 parent 964f057 commit a512a5a

7 files changed

Lines changed: 519 additions & 0 deletions
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
//
2+
// FireflyEditorInstance.swift
3+
//
4+
5+
import Foundation
6+
import AppMakerCore
7+
import SwiftUI
8+
import Firefly
9+
10+
/// The App Maker editor instance implementation for the Firefly editor.
11+
final class FireflyEditor: AppMakerEditorInstance, ObservableObject {
12+
13+
typealias Content = ProjectFileUTF8StringContent
14+
typealias EditorViewBody = FireflyViewRepresentable
15+
16+
let id: UUID = UUID()
17+
let debugAreaTopBarInfo: DebugAreaTopBarInfo = .init()
18+
19+
let liveProjectContent: LiveWritableProjectContent<ProjectFileUTF8StringContent>
20+
let context: AppMakerEditorInstanceContext<FireflyEditor>
21+
let sidebarConfiguration: FireflyEditorSidebarConfiguration
22+
let fireflyProjectTextSyncableDelegate: FireflyProjectTextSyncableDelegate
23+
24+
static func rightSidebarOptions() -> [RightSidebarOption<FireflyEditor>] {
25+
return [
26+
RightSidebarOption<FireflyEditor>(
27+
shortName: "Firefly",
28+
name: "Firefly Editor",
29+
render: { (editor: FireflyEditor) in
30+
return .valid(
31+
editor.sidebarConfiguration.makeView(),
32+
autoWrapInScrollView: false
33+
)
34+
}
35+
)
36+
]
37+
}
38+
39+
static var editorIcon: Image {
40+
Image(systemName: "doc.plaintext.fill")
41+
}
42+
43+
func getName() -> String {
44+
self.liveProjectContent.projectContentId.path.asRelativePath
45+
}
46+
47+
@MainActor
48+
func makeMainBodyView() -> FireflyViewRepresentable {
49+
FireflyViewRepresentable(
50+
editorInstance: self,
51+
config: self.sidebarConfiguration
52+
)
53+
}
54+
55+
@MainActor
56+
init?(
57+
liveProjectContent: LiveWritableProjectContent<ProjectFileUTF8StringContent>,
58+
context: AppMakerEditorInstanceContext<FireflyEditor>
59+
) async {
60+
self.context = context
61+
self.liveProjectContent = liveProjectContent
62+
let projectSyncedText = ProjectSyncedText(
63+
liveWritableString: liveProjectContent,
64+
autoFlushDebounceNanoseconds: 0
65+
)
66+
self.sidebarConfiguration = .init(
67+
projectSyncedText: projectSyncedText,
68+
desiredLanguageKey: liveProjectContent.projectContentId.path.fileExtension.lowercased()
69+
)
70+
let fireflyView = FireflyViewRepresentable.UIViewType()
71+
self.fireflyProjectTextSyncableDelegate = projectSyncedText.open(
72+
with: (
73+
fireflyView,
74+
self.sidebarConfiguration
75+
)
76+
)
77+
fireflyView.setOnSelectionChange { [weak self] (selectionRange: NSRange) in
78+
if let self = self {
79+
let newValue: String
80+
let dist = selectionRange.length
81+
if dist == 0 {
82+
newValue = ""
83+
} else if dist == 1 {
84+
newValue = "1 character"
85+
} else {
86+
newValue = "\(dist) characters"
87+
}
88+
if newValue != self.debugAreaTopBarInfo.text {
89+
self.debugAreaTopBarInfo.text = newValue
90+
}
91+
}
92+
}
93+
}
94+
95+
var isDestroyed: Bool = false
96+
func destroy() async {
97+
self.isDestroyed = true
98+
Task.init(priority: .background) {
99+
try? await Task.sleep(nanoseconds: 1_000_000_000)
100+
await self.fireflyProjectTextSyncableDelegate.projectSyncedText.destroy()
101+
await self.liveProjectContent.destroy()
102+
}
103+
}
104+
}
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
//
2+
// FireflyEditorSidebarConfiguration.swift
3+
//
4+
5+
import Foundation
6+
import SwiftUI
7+
import AppMakerCore
8+
import Firefly
9+
10+
11+
internal final class FireflyEditorSidebarConfiguration: ObservableObject {
12+
13+
@Published internal private(set) var showLineNumbers: Bool = true
14+
@Published internal private(set) var autoGutter: Bool = true
15+
@Published internal private(set) var gutterWidth: CGFloat = 20.0
16+
@Published internal private(set) var language: (key: String, displayName: String)?
17+
@Published internal private(set) var lightTheme: String = "Xcode Light"
18+
@Published internal private(set) var darkTheme: String = "Xcode Dark"
19+
20+
var autoFlushDebounceNanoseconds: UInt64 {
21+
get {
22+
projectSyncedText.autoFlushDebounceNanoseconds
23+
}
24+
set {
25+
projectSyncedText.autoFlushDebounceNanoseconds = newValue
26+
}
27+
}
28+
29+
let projectSyncedText: ProjectSyncedText
30+
31+
init(projectSyncedText: ProjectSyncedText, desiredLanguageKey: String) {
32+
self.projectSyncedText = projectSyncedText
33+
if let first = Firefly.fireflyLanguages.first(where: {$0.key == desiredLanguageKey}),
34+
let displayName: String = first.value["display_name"] as? String {
35+
self.language = (first.key, displayName)
36+
} else if let first = Firefly.fireflyLanguages.first(where: {$0.key == "default"}),
37+
let displayName: String = first.value["display_name"] as? String {
38+
self.language = (first.key, displayName)
39+
} else {
40+
self.language = nil
41+
}
42+
}
43+
44+
func makeView() -> AnyView {
45+
AnyView(SidebarView(config: self))
46+
}
47+
48+
private struct SidebarView: View {
49+
@Environment(\.colorScheme) var colorScheme: ColorScheme
50+
@ObservedObject var config: FireflyEditorSidebarConfiguration
51+
52+
var body: some View {
53+
List {
54+
Section("Appearance") {
55+
Toggle("Show Line Numbers", isOn: $config.showLineNumbers)
56+
if config.showLineNumbers {
57+
Toggle("Auto Gutter", isOn: $config.autoGutter)
58+
if !config.autoGutter {
59+
HStack {
60+
Text("Gutter Width")
61+
Slider(value: $config.gutterWidth, in: 10...40)
62+
}
63+
}
64+
}
65+
switch self.colorScheme {
66+
case .light :
67+
HStack {
68+
Text("Light Theme")
69+
Menu {
70+
let keys: [String] = fireflyThemes.keys.map({$0})
71+
ForEach(0..<keys.count) { (i: Int) in
72+
let key = keys[i]
73+
Button(key) {
74+
self.config.lightTheme = key
75+
}
76+
}
77+
} label: {
78+
HStack {
79+
Spacer()
80+
Text(config.lightTheme)
81+
}
82+
.frame(maxWidth: .infinity, maxHeight: .infinity)
83+
}
84+
}
85+
default:
86+
HStack {
87+
Text("Dark Theme")
88+
Menu {
89+
let keys: [String] = fireflyThemes.keys.map({$0})
90+
ForEach(0..<keys.count) { (i: Int) in
91+
let key = keys[i]
92+
Button(key) {
93+
self.config.darkTheme = key
94+
}
95+
}
96+
} label: {
97+
HStack {
98+
Spacer()
99+
Text(self.config.darkTheme)
100+
}
101+
.frame(maxWidth: .infinity, maxHeight: .infinity)
102+
}
103+
}
104+
}
105+
}
106+
Section("Syntax Highlighting") {
107+
HStack {
108+
Text("Language")
109+
Menu {
110+
let keys: [String] = fireflyLanguages.keys.map({$0})
111+
ForEach(0..<keys.count) { (i: Int) in
112+
let key = keys[i]
113+
if let displayName: String = fireflyLanguages[key]?["display_name"] as? String {
114+
Button(displayName) {
115+
self.config.language = (key, displayName)
116+
}
117+
}
118+
}
119+
} label: {
120+
HStack {
121+
Spacer()
122+
Text(config.language?.displayName ?? "None")
123+
}
124+
.frame(maxWidth: .infinity, maxHeight: .infinity)
125+
}
126+
}
127+
}
128+
Section("Behavior") {
129+
HStack {
130+
Text("Flush Debounce")
131+
Slider(
132+
value: Binding<Float>.init(get: {
133+
Float(config.autoFlushDebounceNanoseconds / 1_000_000)
134+
}, set: { float in
135+
config.autoFlushDebounceNanoseconds = UInt64(float) * 1_000_000
136+
}),
137+
in: 0...1_000,
138+
step: 1
139+
)
140+
}
141+
}
142+
}.listStyle(InsetGroupedListStyle())
143+
}
144+
}
145+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
//
2+
// FireflyLanguages.swift
3+
//
4+
5+
import Foundation
6+
import Firefly
7+
8+
func installExtraLanguages() {
9+
let luaLanguage: [String: Any] = [
10+
"display_name": "Lua",
11+
"identifier": [
12+
"regex": "(\\.[A-Za-z_]+\\w*)|((NS|UI)[A-Z][a-zA-Z]+)|((pcall|print|assert|collectgarbage|error|getfenv|getmetatable|ipairs|next|pairs|pcall|rawequal|rawget|rawset|select|setfenv|setmetatable|tonumber|tostring|type|unpack)(?=\\())|((ads|audio|composer|crypto|display|easing|facebook|gameNetwork|graphics|io|json|lfs|licensing|math|media|native|network|os|package|require|physics|socket|sqlite3|store|productList|event|storeTransaction|string|system|table|timer|transition|widget)(?=\\.))",
13+
"group": 0,
14+
"relevance": 1,
15+
"options": [],
16+
"multiline": false
17+
],
18+
"mult_string": [
19+
"regex": "\"\"\"(.*?)\"\"\"",
20+
"group": 0,
21+
"relevance": 4,
22+
"options": [NSRegularExpression.Options.dotMatchesLineSeparators],
23+
"multiline": true
24+
],
25+
"keyword": [
26+
"regex": "\\b(and|break|do|else|elseif|end|false|for|function|if|in|local|nil|not|or|repeat|return|then|true|until|while)\\b",
27+
"group": 0,
28+
"relevance": 1,
29+
"options": [],
30+
"multiline": false
31+
],
32+
"numbers": [
33+
"regex": "(?<=(\\s|\\[|,|:))(\\d|\\.|_)+",
34+
"group": 0,
35+
"relevance": 0,
36+
"options": [],
37+
"multiline": false
38+
],
39+
"string": [
40+
"regex": #"(?<!\\)".*?(?<!\\)""#,
41+
"group": 0,
42+
"relevance": 3,
43+
"options": [],
44+
"multiline": false
45+
],
46+
"comment": [
47+
"regex": "(?<!:)--.*?(\n|$)", // The regex used for highlighting
48+
"group": 0, // The regex group that should be highlighted
49+
"relevance": 5, // The relevance over other tokens
50+
"options": [], // Regular expression options
51+
"multiline": false // If the token is multiline
52+
],
53+
"multi_comment": [
54+
"regex": "--\\[\\[.*?--\\]\\]", // The regex used for highlighting
55+
"group": 0, // The regex group that should be highlighted
56+
"relevance": 5, // The relevance over other tokens
57+
"options": [NSRegularExpression.Options.dotMatchesLineSeparators], // Regular expression options
58+
"multiline": true // If the token is multiline
59+
],
60+
]
61+
62+
Firefly.fireflyLanguages["lua"] = luaLanguage
63+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
//
2+
// FireflyMain.swift
3+
//
4+
5+
import AppMakerCore
6+
import Combine
7+
import SwiftUI
8+
9+
public func firefly_main() {
10+
// install extra languages to be supported in the Firefly editor
11+
installExtraLanguages()
12+
13+
// created the App Maker editor for Firefly
14+
let fireflyEditor: AppMakerEditor = AppMakerEditor(
15+
named: "Firefly Editor",
16+
basePriority: 0,
17+
canMakeEditor: { validContent in
18+
if let validFileInfo = validContent as? ProjectFileInfoContent.ValidContent {
19+
return validFileInfo.fileType.isString ? .canMakeEditor : .cannotMakeEditor
20+
} else if validContent is ProjectDirectoryContent.ValidContent {
21+
return .cannotMakeEditor
22+
}
23+
return .notSure
24+
},
25+
editorInstanceType: FireflyEditor.self
26+
)
27+
try! fireflyEditor.install()
28+
}
29+
30+
31+
32+
33+
34+
35+
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
//
2+
// FireflyProjectTextSyncableDelegate.swift
3+
//
4+
5+
import Foundation
6+
import AppMakerCore
7+
import Firefly
8+
9+
final class FireflyProjectTextSyncableDelegate: ProjectTextSyncable {
10+
11+
let projectSyncedText: ProjectSyncedText
12+
let fireflySyntaxView: FireflySyntaxView
13+
14+
@MainActor
15+
init(
16+
projectSyncedText: ProjectSyncedText,
17+
startText: String,
18+
wrapped: (
19+
fireflySyntaxView: FireflySyntaxView,
20+
sidebarConfiguration: FireflyEditorSidebarConfiguration
21+
)
22+
) {
23+
self.projectSyncedText = projectSyncedText
24+
self.fireflySyntaxView = wrapped.fireflySyntaxView
25+
wrapped.fireflySyntaxView.text = startText
26+
wrapped.fireflySyntaxView.setOnTextChange { [weak projectSyncedText] oldText, location, newText in
27+
if let projectSyncedText = projectSyncedText {
28+
projectSyncedText.willReplace(oldText, at: location, to: newText)
29+
}
30+
}
31+
}
32+
33+
@MainActor
34+
func handleForeignTextReplacement(in range: NSRange, to newText: String) {
35+
self.fireflySyntaxView.replace(range: range, to: newText)
36+
}
37+
38+
@MainActor
39+
func handleForeignFullDocumentOverwrite(wholeDocumentNewText: String) {
40+
self.fireflySyntaxView.text = wholeDocumentNewText
41+
}
42+
}

0 commit comments

Comments
 (0)