From 159badaafd009b41d700ba162f948031c961edff Mon Sep 17 00:00:00 2001 From: x86y Date: Tue, 17 Oct 2023 00:25:08 +0400 Subject: [PATCH] feat: add syntax highlightings --- Beacon.xcodeproj/project.pbxproj | 12 +++ Beacon/Models/History.swift | 5 +- Beacon/Utilities/LexBQN.swift | 95 ++++++++++++++++++++++ Beacon/Utilities/LexK.swift | 132 +++++++++++++++---------------- Beacon/Utilities/Tokenizer.swift | 92 +++++++++++++++++++++ Beacon/Utilities/Utils.swift | 2 +- Beacon/Views/ConfigView.swift | 2 +- Beacon/Views/ContentView.swift | 64 ++++++--------- 8 files changed, 291 insertions(+), 113 deletions(-) create mode 100644 Beacon/Utilities/LexBQN.swift create mode 100644 Beacon/Utilities/Tokenizer.swift diff --git a/Beacon.xcodeproj/project.pbxproj b/Beacon.xcodeproj/project.pbxproj index 19ad49a..a3ab6b5 100644 --- a/Beacon.xcodeproj/project.pbxproj +++ b/Beacon.xcodeproj/project.pbxproj @@ -34,6 +34,10 @@ F361BF0C2AD2A76700200F72 /* DashboardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3DF82892A4EF33A00F6CD19 /* DashboardView.swift */; }; F379CEC32ADD94BC00164F76 /* LexK.swift in Sources */ = {isa = PBXBuildFile; fileRef = F379CEC22ADD94BC00164F76 /* LexK.swift */; }; F379CEC42ADD94BC00164F76 /* LexK.swift in Sources */ = {isa = PBXBuildFile; fileRef = F379CEC22ADD94BC00164F76 /* LexK.swift */; }; + F379CEC62ADDB24D00164F76 /* LexBQN.swift in Sources */ = {isa = PBXBuildFile; fileRef = F379CEC52ADDB24D00164F76 /* LexBQN.swift */; }; + F379CEC72ADDB24D00164F76 /* LexBQN.swift in Sources */ = {isa = PBXBuildFile; fileRef = F379CEC52ADDB24D00164F76 /* LexBQN.swift */; }; + F379CEC92ADDD2A700164F76 /* Tokenizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = F379CEC82ADDD2A700164F76 /* Tokenizer.swift */; }; + F379CECA2ADDD2A700164F76 /* Tokenizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = F379CEC82ADDD2A700164F76 /* Tokenizer.swift */; }; F3877C8E29952A6600E2FCB5 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F3877C8D29952A6600E2FCB5 /* Assets.xcassets */; }; F38F03A92A323F8500F66354 /* libksim.a in Frameworks */ = {isa = PBXBuildFile; fileRef = F38F03A82A323F8500F66354 /* libksim.a */; }; F3AC4D262A4F6C3100B4FECD /* curl.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = F3AC4D252A4F6C3000B4FECD /* curl.xcframework */; }; @@ -107,6 +111,8 @@ F35FE0AD2A44387C00653849 /* WidgetKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WidgetKit.framework; path = System/Library/Frameworks/WidgetKit.framework; sourceTree = SDKROOT; }; F35FE0AF2A44387D00653849 /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = System/Library/Frameworks/SwiftUI.framework; sourceTree = SDKROOT; }; F379CEC22ADD94BC00164F76 /* LexK.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LexK.swift; sourceTree = ""; }; + F379CEC52ADDB24D00164F76 /* LexBQN.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LexBQN.swift; sourceTree = ""; }; + F379CEC82ADDD2A700164F76 /* Tokenizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tokenizer.swift; sourceTree = ""; }; F3877C8B29952A5400E2FCB5 /* ContentView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; F3877C8D29952A6600E2FCB5 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; F38F03A82A323F8500F66354 /* libksim.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libksim.a; path = Beacon/libs/libksim.a; sourceTree = ""; }; @@ -201,6 +207,8 @@ F3E1DFF62A3216C000B4A553 /* k.h */, F312CF8729951C49008EC197 /* Beacon-Bridging-Header.h */, F379CEC22ADD94BC00164F76 /* LexK.swift */, + F379CEC52ADDB24D00164F76 /* LexBQN.swift */, + F379CEC82ADDD2A700164F76 /* Tokenizer.swift */, ); path = Utilities; sourceTree = ""; @@ -449,10 +457,12 @@ F31BCE792A33B33E00A6116D /* Utils.swift in Sources */, F3E1DFF52A31F9D800B4A553 /* ConfigView.swift in Sources */, F32D9E8429966FCD007BC97C /* Beacon-Bridging-Header.h in Sources */, + F379CEC72ADDB24D00164F76 /* LexBQN.swift in Sources */, F379CEC42ADD94BC00164F76 /* LexK.swift in Sources */, F30A7B8B2A370B7800503966 /* BuffersView.swift in Sources */, F31BCE7F2A348B7500A6116D /* HelpView.swift in Sources */, F32D9E8529966FCD007BC97C /* ArraygroundApp.swift in Sources */, + F379CECA2ADDD2A700164F76 /* Tokenizer.swift in Sources */, F327CA1B2ADD0A2F00FD72D9 /* ReplInput.swift in Sources */, F32D9E8629966FCD007BC97C /* ContentView.swift in Sources */, ); @@ -467,10 +477,12 @@ F3DF828A2A4EF33A00F6CD19 /* DashboardView.swift in Sources */, F3E1DFF42A31F9D800B4A553 /* ConfigView.swift in Sources */, F312CF8829951C89008EC197 /* Beacon-Bridging-Header.h in Sources */, + F379CEC62ADDB24D00164F76 /* LexBQN.swift in Sources */, F379CEC32ADD94BC00164F76 /* LexK.swift in Sources */, F30A7B8A2A370B7800503966 /* BuffersView.swift in Sources */, F31BCE7E2A348B7500A6116D /* HelpView.swift in Sources */, F3B198CE299517DA00FE664F /* ArraygroundApp.swift in Sources */, + F379CEC92ADDD2A700164F76 /* Tokenizer.swift in Sources */, F327CA1A2ADD0A2F00FD72D9 /* ReplInput.swift in Sources */, F32D9E972996710A007BC97C /* ContentView.swift in Sources */, ); diff --git a/Beacon/Models/History.swift b/Beacon/Models/History.swift index d5f43e7..5ce3db9 100644 --- a/Beacon/Models/History.swift +++ b/Beacon/Models/History.swift @@ -11,6 +11,7 @@ struct Entry: Hashable, Codable, Identifiable { var id: String = UUID().uuidString var src: String var out: String + var lang: Language } enum Buffers { @@ -38,8 +39,8 @@ enum Buffers { class HistoryModel: ObservableObject { @Published var history: [String: [Entry]] = ["default": []] - func addMessage(with src: String, out: String, for key: String) { - let entry = Entry(src: src, out: out) + func addMessage(with src: String, out: String, lang: Language, for key: String) { + let entry = Entry(src: src, out: out, lang: lang) if var entries = history[key] { entries.append(entry) history[key] = entries diff --git a/Beacon/Utilities/LexBQN.swift b/Beacon/Utilities/LexBQN.swift new file mode 100644 index 0000000..821b254 --- /dev/null +++ b/Beacon/Utilities/LexBQN.swift @@ -0,0 +1,95 @@ +// +// LexBQN.swift +// Beacon +// + +import Foundation +import SwiftUI + +func parseBQN(code: String) -> [String] { + let regC = "0" + let fnsC = "1" + let fns = "!+-×÷⋆*√⌊⌈∧∨¬|=≠≤<>≥≡≢⊣⊢⥊∾≍⋈↑↓↕⌽⍉/⍋⍒⊏⊑⊐⊒∊⍷⊔«»⍎⍕" + let mopC = "2" + let mop = "`˜˘¨⁼⌜´˝˙" + let dopC = "3" + let dop = "∘⊸⟜○⌾⎉⚇⍟⊘◶⎊" + let namC = "4" + let nam = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_" + let digC = "5" + let dig = "0123456789π∞" + let digS = dig + "¯." + let digM = "eEiI" + let arrC = "6" + let arr = "·⍬‿⦃⦄⟨⟩[]@" + let dfnC = "7" + let dfn = "𝕨𝕩𝔽𝔾𝕎𝕏𝕗𝕘𝕣ℝ𝕤𝕊{}:" + let strC = "8" + let dmdC = "D" + let dmd = "←↩,⋄→⇐" + let comC = "C" + let newL = "E" + + var res = Array(repeating: "", count: code.count) + var i = code.startIndex + + while i < code.endIndex { + let c = code[i] + if digS.contains(c) { + res[i.utf16Offset(in: code)] = digC + i = code.index(after: i) + while dig.contains(code[safe: i] ?? "\0") || code[safe: i] == "." || digM.contains(code[safe: i] ?? "\0") && digS.contains(code[safe: code.index(after: i)] ?? "\0") { + i = code.index(after: i) + } + continue + } else if fns.contains(c) { + res[i.utf16Offset(in: code)] = fnsC + } else if mop.contains(c) { + res[i.utf16Offset(in: code)] = mopC + } else if dop.contains(c) { + res[i.utf16Offset(in: code)] = dopC + } else if dfn.contains(c) { + res[i.utf16Offset(in: code)] = dfnC + } else if arr.contains(c) { + res[i.utf16Offset(in: code)] = arrC + } else if dmd.contains(c) { + res[i.utf16Offset(in: code)] = dmdC + } else if nam.contains(c) || c == "•" { + let fst = i + if code[safe: i] == "•" { + i = code.index(after: i) + } + let cs = code[safe: i] ?? "\0" + while nam.contains(code[safe: i] ?? "\0") || dig.contains(code[safe: i] ?? "\0") { + i = code.index(after: i) + } + let ce = code[safe: code.index(before: i)] ?? "\0" + res[fst.utf16Offset(in: code)] = cs == "_" ? (ce == "_" ? dopC : mopC) : (cs >= "A" && cs <= "Z" ? fnsC : namC) + continue + } else if c == "'" || c == "\"" { + res[i.utf16Offset(in: code)] = strC + i = code.index(after: i) + let q = c + while code[safe: i] != nil, code[safe: i] != q, code[safe: i] != "\n" { + i = code.index(after: i) + } + } else if c == "#" { + res[i.utf16Offset(in: code)] = comC + while code[safe: i] != nil, code[safe: i] != "\n" { + i = code.index(after: i) + } + } else if !" \t".contains(c) { + res[i.utf16Offset(in: code)] = regC + } else if c == "\n" { + res[i.utf16Offset(in: code)] = newL + } + i = code.index(after: i) + } + return res +} + +extension String { + subscript(safe index: Index) -> Character? { + return indices.contains(index) ? self[index] : nil + } +} diff --git a/Beacon/Utilities/LexK.swift b/Beacon/Utilities/LexK.swift index f5a1d96..4388068 100644 --- a/Beacon/Utilities/LexK.swift +++ b/Beacon/Utilities/LexK.swift @@ -3,85 +3,81 @@ // Arrayground // +import Foundation import SwiftUI -enum TokenType { - case number, function, variable, string, statement, comment, other -} +func parseK(_ str: String) -> [String] { + let regC = "0" + let fnsC = "1" + let fns = "+-*%!&|<>=~,^#_?@." + let mopC = "2" + let mop = "/'\\" + let namC = "4" + let nam = "⎕⍞∆⍙ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" + let digC = "5" + let dig = "0123456789¯∞" + let parC = "6" + let dfnC = "7" + let strC = "8" + let dmdC = "D" + let dmd = ":$" + let comC = "C" + let endL = "E" -struct Token { - let value: String - let type: TokenType -} + var res = Array(repeating: "", count: str.count) + res[0] = regC -func tokenize(code: String) -> [Token] { - var tokens: [Token] = [] - var currentToken = "" - var isComment = false - var isString = false + var i = 0 - for char in code { - if isComment { - currentToken.append(char) - if char == "\n" { - tokens.append(Token(value: currentToken, type: .comment)) - currentToken = "" - isComment = false - } - continue - } + while i < str.count { + let p = i - 1 >= 0 ? String(str[str.index(str.startIndex, offsetBy: i - 1) ... str.index(str.startIndex, offsetBy: i - 1)]) : "\0" + let c = String(str[str.index(str.startIndex, offsetBy: i) ... str.index(str.startIndex, offsetBy: i)]) + let n = i + 1 < str.count ? String(str[str.index(str.startIndex, offsetBy: i + 1) ... str.index(str.startIndex, offsetBy: i + 1)]) : "\0" - if isString { - currentToken.append(char) - if char == "\"" { - tokens.append(Token(value: currentToken, type: .string)) - currentToken = "" - isString = false + if dig.contains(c) || (c == "." && dig.contains(n)) { + res[i] = digC + while i < str.count, dig.contains(String(str[str.index(str.startIndex, offsetBy: i) ... str.index(str.startIndex, offsetBy: i)])) || ".eEnNbiwW".contains(String(str[str.index(str.startIndex, offsetBy: i) ... str.index(str.startIndex, offsetBy: i)])) { + i += 1 } - continue - } - if char == "#" || char == "/" { - isComment = true - currentToken.append(char) continue - } - - if char == "\"" { - isString = true - currentToken.append(char) + } else if (c == " " && n == "/") || ((p == "\n" || p == "\0") && c == "/") { + res[i] = comC + while str[str.index(str.startIndex, offsetBy: i) ... str.index(str.startIndex, offsetBy: i)] != "\n" { + i += 1 + } + } else if fns.contains(c) || c.unicodeScalars.first!.value >= 0x80 { + res[i] = fnsC + } else if mop.contains(c) { + res[i] = mopC + if n == ":" { + i += 1 + res[i] = mopC + } + } else if "{xyz}".contains(c) { + res[i] = dfnC + } else if "([])".contains(c) { + res[i] = parC + } else if dmd.contains(c) { + res[i] = dmdC + } else if nam.contains(c) { + res[i] = namC + while nam.contains(String(str[str.index(str.startIndex, offsetBy: i) ... str.index(str.startIndex, offsetBy: i)])) || dig.contains(String(str[str.index(str.startIndex, offsetBy: i) ... str.index(str.startIndex, offsetBy: i)])) { + i += 1 + } continue - } - - if char.isWhitespace { - if !currentToken.isEmpty { - tokens.append(Token(value: currentToken, type: determineType(token: currentToken))) + } else if c == "\"" { + res[i] = strC + i += 1 + while str[str.index(str.startIndex, offsetBy: i) ... str.index(str.startIndex, offsetBy: i)] != "\"", str[str.index(str.startIndex, offsetBy: i) ... str.index(str.startIndex, offsetBy: i)] != "\n" { + i += String(str[str.index(str.startIndex, offsetBy: i) ... str.index(str.startIndex, offsetBy: i)]) == "\\" ? 2 : 1 } - currentToken = "" - } else { - currentToken.append(char) + } else if !" \t".contains(c) { + res[i] = regC + } else if c == "\n" { + res[i] = endL } + i += 1 } - - if !currentToken.isEmpty { - tokens.append(Token(value: currentToken, type: determineType(token: currentToken))) - } - - return tokens -} - -func determineType(token: String) -> TokenType { - if Double(token) != nil { - return .number - } - if token.last == ":" { - return .function - } - if token.first?.isLetter == true { - return .variable - } - if token == ":" { - return .statement - } - return .other + return res } diff --git a/Beacon/Utilities/Tokenizer.swift b/Beacon/Utilities/Tokenizer.swift new file mode 100644 index 0000000..3bdb22d --- /dev/null +++ b/Beacon/Utilities/Tokenizer.swift @@ -0,0 +1,92 @@ +// +// Tokenizer.swift +// Arrayground +// + +import Foundation +import SwiftUI + +func tokenToColor(_ type: TokenType) -> Color { + switch type { + case .regC: + return Color.gray // '#D2D2D2' + case .fnsC: + return Color.green // '#32E732' + case .mopC: + return Color.yellow // '#FFF455' + case .namC: + return Color.white // '#D2D2D2' + case .digC: + return Color.purple // '#FF80F4' + case .parC: + return Color.blue // '#89A7DC' + case .dfnC: + return Color.red // '#E3736D' + case .strC: + return Color.orange // '#DDAAEE' + case .dmdC: + return Color.yellow // '#FFFF00' + case .comC: + return Color.gray // '#898989' + case .endL: + return Color.gray // '#898989' + } +} + +enum TokenType: String { + case regC = "0" + case fnsC = "1" + case mopC = "2" + case namC = "4" + case digC = "5" + case parC = "6" + case dfnC = "7" + case strC = "8" + case dmdC = "D" + case comC = "C" + case endL = "E" +} + +struct Token: Identifiable { + var id = UUID() + let value: String + let type: TokenType +} + +func tokenize(_ str: String, _ parseKOutput: [String]) -> [[Token]] { + var tokens: [Token] = [] + var i = 0 + + while i < parseKOutput.count { + if let type = TokenType(rawValue: parseKOutput[i]) { + var j = i + 1 + while j < parseKOutput.count && parseKOutput[j] == "" { + j += 1 + } + let value = str[str.index(str.startIndex, offsetBy: i) ..< str.index(str.startIndex, offsetBy: j)] + tokens.append(Token(value: String(value), type: type)) + i = j + } else { + i += 1 + } + } + + var res: [[Token]] = [[]] + var temp: [Token] = [] + for token in tokens { + if token.value == "\n" { + if !temp.isEmpty { + res.append(temp) + } + temp = [] + } else { + temp.append(token) + } + } + + if !temp.isEmpty { + res.append(temp) + } + + return res +} diff --git a/Beacon/Utilities/Utils.swift b/Beacon/Utilities/Utils.swift index ee0a7d5..738ca34 100644 --- a/Beacon/Utilities/Utils.swift +++ b/Beacon/Utilities/Utils.swift @@ -58,7 +58,7 @@ func e(input: String) -> String { input = "\(vars) •Import \"\(Bundle.main.resourcePath!)/bqn-libs/\(filename)\"" } input = input.replacingOccurrences(of: "\"", with: #""""#) - input = "•Out ((•ReBQN{repl⇐\"loose\"})⎊{𝕊: \"Error: \"∾•CurrentError@}) \"\(input)\"" + input = "((•ReBQN{repl⇐\"loose\"})⎊{𝕊: •Out \"Error: \"∾•CurrentError@}) \"\(input)\"" return runCmd(cbqnCmd, input) } diff --git a/Beacon/Views/ConfigView.swift b/Beacon/Views/ConfigView.swift index 7e17565..9b896e1 100644 --- a/Beacon/Views/ConfigView.swift +++ b/Beacon/Views/ConfigView.swift @@ -17,7 +17,7 @@ enum AppFont: Int { case apl = 2 } -enum Language: Int { +enum Language: Int, Codable { case bqn = 0 case k = 1 } diff --git a/Beacon/Views/ContentView.swift b/Beacon/Views/ContentView.swift index 91e9382..6dedf0e 100644 --- a/Beacon/Views/ContentView.swift +++ b/Beacon/Views/ContentView.swift @@ -48,45 +48,27 @@ struct HistoryView: View { .font(Font.custom("BQN386 Unicode", size: 18)) .foregroundColor(.blue) } else if editType == Behavior.duplicate { - /* - HStack(spacing: 0) { - ForEach(tokenize(code: historyItem.src), id: \.value) { token in - switch token.type { - case .number: - Text(token.value) - .foregroundColor(.green) - case .function: - Text(token.value) - .foregroundColor(.purple) - case .variable: - Text(token.value) - .foregroundColor(.blue) - case .string: - Text(token.value) - .foregroundColor(.orange) - case .statement: - Text(token.value) - .foregroundColor(.pink) - case .comment: - Text(token.value) - .foregroundColor(.gray) - case .other: - Text(token.value) - .foregroundColor(.primary) - } - } - } - */ - Text(historyItem.src) - .font(Font.custom("BQN386 Unicode", size: 18)) - .foregroundColor(.blue) - .onTapGesture { - self.input = historyItem.src + VStack(spacing: 1) { + let tokens = historyItem.lang == .k + ? tokenize(historyItem.src, parseK(historyItem.src)) + : tokenize(historyItem.src, parseBQN(code: historyItem.src)) + ForEach(Array(tokens.enumerated()), id: \.offset) { _, line in + HStack(spacing: 0) { + ForEach(line, id: \.id) { token in + Text(token.value) + .foregroundColor(tokenToColor(token.type)) + .font(Font.custom("BQN386 Unicode", size: 18)) + .onTapGesture { + self.input = historyItem.src + } + } + }.frame(maxWidth: .infinity, alignment: .leading) } + } } Text("\(trimLongText(historyItem.out))") .font(Font.custom("BQN386 Unicode", size: 18)) - .foregroundColor(historyItem.out.starts(with: "Error:") ? .red : .primary) + .foregroundColor(historyItem.out.starts(with: "Error:") || historyItem.out.starts(with: "\"Error:") ? .red : .primary) .multilineTextAlignment(.leading) .onTapGesture { self.input = historyItem.out @@ -139,7 +121,7 @@ struct ContentView: View { print("Search item successfully indexed!") } } - viewModel.addMessage(with: input, out: output, for: curBuffer) + viewModel.addMessage(with: input, out: output, lang: lang, for: curBuffer) } else { isFocused = false } @@ -178,11 +160,11 @@ struct ContentView: View { .padding(.bottom, 5) ReplInput(text: $input, - helpOpen: $showHelp, - settingsOpen: $showSettings, - buffersOpen: $showBuffers, - lang: self.lang, onSubmit: { onMySubmit(input: self.input) }, - font: Font.custom("BQN386 Unicode", size: 20)) + helpOpen: $showHelp, + settingsOpen: $showSettings, + buffersOpen: $showBuffers, + lang: self.lang, onSubmit: { onMySubmit(input: self.input) }, + font: Font.custom("BQN386 Unicode", size: 20)) .padding(.bottom, 4) .focused($isFocused) .onTapGesture {