From bb34a0755ebeff41ba5a5031cfe4ce5ffb0fed39 Mon Sep 17 00:00:00 2001 From: Tihan-Nico Date: Sat, 19 Mar 2022 16:21:29 +0200 Subject: [PATCH 1/2] Added more editor themes and more language support in the themes --- .../xcshareddata/swiftpm/Package.resolved | 9 - CodeEdit/Settings/GeneralSettingsView.swift | 8 + .../Modules/CodeEditor/CodeEditor.swift | 309 +++++++++++++++++ .../Modules/CodeEditor/Language.swift | 79 +++++ .../Modules/CodeEditor/ThemeName.swift | 35 ++ .../Modules/CodeEditor/TypedString.swift | 32 ++ .../Modules/CodeEditor/UXCodeTextView.swift | 326 ++++++++++++++++++ .../UXCodeTextViewRepresentable.swift | 233 +++++++++++++ CodeEditModules/Package.swift | 25 +- 9 files changed, 1041 insertions(+), 15 deletions(-) create mode 100644 CodeEditModules/Modules/CodeEditor/CodeEditor.swift create mode 100644 CodeEditModules/Modules/CodeEditor/Language.swift create mode 100644 CodeEditModules/Modules/CodeEditor/ThemeName.swift create mode 100644 CodeEditModules/Modules/CodeEditor/TypedString.swift create mode 100644 CodeEditModules/Modules/CodeEditor/UXCodeTextView.swift create mode 100644 CodeEditModules/Modules/CodeEditor/UXCodeTextViewRepresentable.swift diff --git a/CodeEdit.xcworkspace/xcshareddata/swiftpm/Package.resolved b/CodeEdit.xcworkspace/xcshareddata/swiftpm/Package.resolved index 8450bfb4c..8083b8ae7 100644 --- a/CodeEdit.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/CodeEdit.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,15 +1,6 @@ { "object": { "pins": [ - { - "package": "CodeEditor", - "repositoryURL": "https://github.com/ZeeZide/CodeEditor.git", - "state": { - "branch": null, - "revision": "5856fac22b0a2174dbdea212784567c8c9cd1129", - "version": "1.2.0" - } - }, { "package": "Highlightr", "repositoryURL": "https://github.com/raspu/Highlightr", diff --git a/CodeEdit/Settings/GeneralSettingsView.swift b/CodeEdit/Settings/GeneralSettingsView.swift index 91d07a9f1..b9eec6401 100644 --- a/CodeEdit/Settings/GeneralSettingsView.swift +++ b/CodeEdit/Settings/GeneralSettingsView.swift @@ -62,6 +62,14 @@ struct GeneralSettingsView: View { .tag(CodeEditor.ThemeName.agate) Text("Ocean") .tag(CodeEditor.ThemeName.ocean) + Text("Xcode") + .tag(CodeEditor.ThemeName.xcode) + Text("Github") + .tag(CodeEditor.ThemeName.github) + Text("Google Code") + .tag(CodeEditor.ThemeName.googlecode) + Text("Visual Studio") + .tag(CodeEditor.ThemeName.vs) } } .padding() diff --git a/CodeEditModules/Modules/CodeEditor/CodeEditor.swift b/CodeEditModules/Modules/CodeEditor/CodeEditor.swift new file mode 100644 index 000000000..1462648cc --- /dev/null +++ b/CodeEditModules/Modules/CodeEditor/CodeEditor.swift @@ -0,0 +1,309 @@ +// +// CodeEditor.swift +// CodeEditor +// +// Created by Helge Heß. +// Copyright © 2021 ZeeZide GmbH. All rights reserved. +// + +import SwiftUI +import Highlightr + +/** + * An simple code editor (or viewer) with highlighting for SwiftUI (iOS and + * macOS). + * + * To use the code editor as a Viewer, simply pass the source code + * + * struct ContentView: View { + * + * var body: some View { + * CodeEditor(source: "let a = 42") + * } + * } + * + * If it should act as an actual editor, pass in a `Binding`: + * + * struct ContentView: View { + * + * @State private var source = "let a = 42\n" + * + * var body: some View { + * CodeEditor(source: $source, language: .swift, theme: .ocean) + * } + * } + * + * ### Languages and Themes + * + * Highlight.js supports more than 180 languages and over 80 different themes. + * + * The available languages and themes can be accessed using: + * + * CodeEditor.availableLanguages + * CodeEditor.availableThemes + * + * They can be used in a SwiftUI `Picker` like so: + * + * @State var source = "let it = be" + * @State var language = CodeEditor.Language.swift + * + * Picker("Language", selection: $language) { + * ForEach(CodeEditor.availableLanguages) { language in + * Text("\(language.rawValue.capitalized)") + * .tag(language) + * } + * } + * + * CodeEditor(source: $source, language: language) + * + * Note: The `CodeEditor` doesn't do automatic theme changes if the appearance + * changes. + * + * ### Smart Indent and Open/Close Pairing + * + * Inspired by [NTYSmartTextView](https://github.com/naoty/NTYSmartTextView), + * `CodeEditor` now also supports (on macOS): + * - smarter indents (preserving the indent of the previous line) + * - soft indents (insert a configurable amount of spaces if the user presses tabs) + * - auto character pairing, e.g. when entering `{`, the matching `}` will be auto-added + * + * To enable smart indents, add the `smartIndent` flag, e.g.: + * + * CodeEditor(source: $source, language: language, + * flags: [ .selectable, .editable, .smartIndent ]) + * + * It is enabled for editors by default. + * + * To configure soft indents, use the `indentStyle` parameter, e.g. + * + * CodeEditor(source: $source, language: language, + * indentStyle: .softTab(width: 2)) + * + * It defaults to tabs, as per system settings. + * + * Auto character pairing is automatic based on the language. E.g. there is a set of + * defaults for C like languages (e.g. Swift), Python or XML. The defaults can be overridden + * using the respective static variable in `CodeEditor`, + * or the desired pairing can be set explicitly: + * + * CodeEditor(source: $source, language: language, + * autoPairs: [ "{": "}", "<": ">", "'": "'" ]) + * + * + * ### Font Sizing + * + * On macOS the editor supports sizing of the font (using Cmd +/Cmd - and the + * font panel). + * To enable sizing commands, the WindowScene needs to have the proper commands + * applied, e.g.: + * + * WindowGroup { + * ContentView() + * } + * .commands { + * TextFormattingCommands() + * } + * + * To persist the binding, the `fontSize` binding is available. + * + * ### Highlightr and Shaper + * + * Based on the excellent [Highlightr](https://github.com/raspu/Highlightr). + * This means that it is using JavaScriptCore as the actual driver. As + * Highlightr says: + * + * > It will never be as fast as a native solution, but it's fast enough to be + * > used on a real time editor. + * + * The editor is similar to (but not exactly the same) the one used by + * [SVG Shaper for SwiftUI](https://zeezide.de/en/products/svgshaper/), + * for its SVG and Swift editor parts. + */ +public struct CodeEditor: View { + + /// Returns the available themes in the associated Highlightr package. + public static var availableThemes = + Highlightr()?.availableThemes().map(ThemeName.init).sorted() ?? [] + + /// Returns the available languages in the associated Highlightr package. + public static var availableLanguages = + Highlightr()?.supportedLanguages().map(Language.init).sorted() ?? [] + + + /** + * Flags available for `CodeEditor`, currently just: + * - `.editable` + * - `.selectable` + */ + @frozen public struct Flags: OptionSet { + public let rawValue : UInt8 + @inlinable public init(rawValue: UInt8) { self.rawValue = rawValue } + + /// `.editable` requires that the `source` of the `CodeEditor` is a + /// `Binding`. + public static let editable = Flags(rawValue: 1 << 0) + + /// Whether the displayed content should be selectable by the user. + public static let selectable = Flags(rawValue: 1 << 1) + + /// If the user starts a newline, the editor automagically adds the same + /// whitespace as on the previous line. + public static let smartIndent = Flags(rawValue: 1 << 2) + + public static let defaultViewerFlags : Flags = [ .selectable ] + public static let defaultEditorFlags : Flags = + [ .selectable, .editable, .smartIndent ] + } + + @frozen public enum IndentStyle: Equatable { + case system + case softTab(width: Int) + } + + /** + * Default auto pairing mappings for languages. + */ + public static var defaultAutoPairs : [ Language : [ String : String ] ] = [ + .c: cStyleAutoPairs, .cpp: cStyleAutoPairs, .objectivec: cStyleAutoPairs, + .swift: cStyleAutoPairs, + .java: cStyleAutoPairs, .javascript: cStyleAutoPairs, + .xml: xmlStyleAutoPairs, + .python: [ "(": ")", "[": "]", "\"": "\"", "'": "'", "`": "`" ] + ] + public static var cStyleAutoPairs = [ + "(": ")", "[": "]", "{": "}", "\"": "\"", "'": "'", "`": "`" + ] + public static var xmlStyleAutoPairs = [ "<": ">", "\"": "\"", "'": "'" ] + + + /** + * Configures a CodeEditor View with the given parameters. + * + * - Parameters: + * - source: A binding to a String that holds the source code to be + * edited (or displayed). + * - language: Optionally set a language (e.g. `.swift`), otherwise + * Highlight.js will attempt to detect the language. + * - theme: The name of the theme to use, defaults to "pojoaque". + * - fontSize: On macOS this Binding can be used to persist the size of + * the font in use. At runtime this is combined with the + * theme to produce the full font information. (optional) + * - flags: Configure whether the text is editable and/or selectable + * (defaults to both). + * - indentStyle: Optionally insert a configurable amount of spaces if the + * user hits "tab". + * - autoPairs: A mapping of open/close characters, where the close + * characters are automatically injected when the user enters + * the opening character. For example: `[ "{": "}" ]` would + * automatically insert the closing "}" if the user enters + * "{". If no value is given, the default mapping for the + * language is used. + * - inset: The editor can be inset in the scroll view. Defaults to + * 8/8. + */ + public init(source : Binding, + language : Language? = nil, + theme : ThemeName = .default, + fontSize : Binding? = nil, + flags : Flags = .defaultEditorFlags, + indentStyle : IndentStyle = .system, + autoPairs : [ String : String ]? = nil, + inset : CGSize? = nil) + { + self.source = source + self.fontSize = fontSize + self.language = language + self.themeName = theme + self.flags = flags + self.indentStyle = indentStyle + self.inset = inset ?? CGSize(width: 8, height: 8) + self.autoPairs = autoPairs + ?? language.flatMap({ CodeEditor.defaultAutoPairs[$0] }) + ?? [:] + } + + /** + * Configures a read-only CodeEditor View with the given parameters. + * + * - Parameters: + * - source: A String that holds the source code to be displayed. + * - language: Optionally set a language (e.g. `.swift`), otherwise + * Highlight.js will attempt to detect the language. + * - theme: The name of the theme to use, defaults to "pojoaque". + * - fontSize: On macOS this Binding can be used to persist the size of + * the font in use. At runtime this is combined with the + * theme to produce the full font information. (optional) + * - flags: Configure whether the text is selectable + * (defaults to both). + * - indentStyle: Optionally insert a configurable amount of spaces if the + * user hits "tab". + * - autoPairs: A mapping of open/close characters, where the close + * characters are automatically injected when the user enters + * the opening character. For example: `[ "{": "}" ]` would + * automatically insert the closing "}" if the user enters + * "{". If no value is given, the default mapping for the + * language is used. + * - inset: The editor can be inset in the scroll view. Defaults to + * 8/8. + */ + @inlinable + public init(source : String, + language : Language? = nil, + theme : ThemeName = .default, + fontSize : Binding? = nil, + flags : Flags = .defaultViewerFlags, + indentStyle : IndentStyle = .system, + autoPairs : [ String : String ]? = nil, + inset : CGSize? = nil) + { + assert(!flags.contains(.editable), "Editing requires a Binding") + self.init(source : .constant(source), + language : language, + theme : theme, + fontSize : fontSize, + flags : flags.subtracting(.editable), + indentStyle : indentStyle, + autoPairs : autoPairs, + inset : inset) + } + + private var source : Binding + private var fontSize : Binding? + private let language : Language? + private let themeName : ThemeName + private let flags : Flags + private let indentStyle : IndentStyle + private let autoPairs : [ String : String ] + private let inset : CGSize + + public var body: some View { + UXCodeTextViewRepresentable(source : source, + language : language, + theme : themeName, + fontSize : fontSize, + flags : flags, + indentStyle : indentStyle, + autoPairs : autoPairs, + inset : inset) + } +} + +struct CodeEditor_Previews: PreviewProvider { + + static var previews: some View { + + CodeEditor(source: "let a = 5") + .frame(width: 200, height: 100) + + CodeEditor(source: "let a = 5", language: .swift, theme: .pojoaque) + .frame(width: 200, height: 100) + + CodeEditor(source: + #""" + The quadratic formula is $-b \pm \sqrt{b^2 - 4ac} \over 2a$ + \bye + """#, language: .tex + ) + .frame(width: 540, height: 200) + } +} diff --git a/CodeEditModules/Modules/CodeEditor/Language.swift b/CodeEditModules/Modules/CodeEditor/Language.swift new file mode 100644 index 000000000..01a19702f --- /dev/null +++ b/CodeEditModules/Modules/CodeEditor/Language.swift @@ -0,0 +1,79 @@ +// +// Language.swift +// CodeEditor +// +// Created by Helge Heß. +// Copyright © 2021 ZeeZide GmbH. All rights reserved. +// + +public extension CodeEditor { + + @frozen + struct Language: TypedString { + + public let rawValue : String + + @inlinable + public init(rawValue: String) { self.rawValue = rawValue } + } +} + +public extension CodeEditor.Language { + + static var accesslog = CodeEditor.Language(rawValue: "accesslog") + static var actionscript = CodeEditor.Language(rawValue: "actionscript") + static var ada = CodeEditor.Language(rawValue: "ada") + static var apache = CodeEditor.Language(rawValue: "apache") + static var applescript = CodeEditor.Language(rawValue: "applescript") + static var bash = CodeEditor.Language(rawValue: "bash") + static var basic = CodeEditor.Language(rawValue: "basic") + static var brainfuck = CodeEditor.Language(rawValue: "brainfuck") + static var c = CodeEditor.Language(rawValue: "c") + static var clojure = CodeEditor.Language(rawValue: "clojure") + static var coffeescript = CodeEditor.Language(rawValue: "coffeescript") + static var cmake = CodeEditor.Language(rawValue: "cmake") + static var cpp = CodeEditor.Language(rawValue: "cpp") + static var cs = CodeEditor.Language(rawValue: "cs") + static var css = CodeEditor.Language(rawValue: "css") + static var diff = CodeEditor.Language(rawValue: "diff") + static var delphi = CodeEditor.Language(rawValue: "delphi") + static var django = CodeEditor.Language(rawValue: "django") + static var dockerfile = CodeEditor.Language(rawValue: "dockerfile") + static var fsharp = CodeEditor.Language(rawValue: "fsharp") + static var dart = CodeEditor.Language(rawValue: "dart") + static var go = CodeEditor.Language(rawValue: "go") + static var gradle = CodeEditor.Language(rawValue: "gradle") + static var groovy = CodeEditor.Language(rawValue: "groovy") + static var http = CodeEditor.Language(rawValue: "http") + static var java = CodeEditor.Language(rawValue: "java") + static var javascript = CodeEditor.Language(rawValue: "javascript") + static var json = CodeEditor.Language(rawValue: "json") + static var lua = CodeEditor.Language(rawValue: "lua") + static var markdown = CodeEditor.Language(rawValue: "markdown") + static var makefile = CodeEditor.Language(rawValue: "makefile") + static var mathematica = CodeEditor.Language(rawValue: "mathematica") + static var matlab = CodeEditor.Language(rawValue: "matlab") + static var nginx = CodeEditor.Language(rawValue: "nginx") + static var objectivec = CodeEditor.Language(rawValue: "objectivec") + static var perl = CodeEditor.Language(rawValue: "perl") + static var pgsql = CodeEditor.Language(rawValue: "pgsql") + static var php = CodeEditor.Language(rawValue: "php") + static var python = CodeEditor.Language(rawValue: "python") + static var protobuf = CodeEditor.Language(rawValue: "protobuf") + static var ruby = CodeEditor.Language(rawValue: "ruby") + static var rust = CodeEditor.Language(rawValue: "rust") + static var scala = CodeEditor.Language(rawValue: "scala") + static var scss = CodeEditor.Language(rawValue: "scss") + static var shell = CodeEditor.Language(rawValue: "shell") + static var smalltalk = CodeEditor.Language(rawValue: "smalltalk") + static var sql = CodeEditor.Language(rawValue: "sql") + static var swift = CodeEditor.Language(rawValue: "swift") + static var tcl = CodeEditor.Language(rawValue: "tcl") + static var tex = CodeEditor.Language(rawValue: "tex") + static var twig = CodeEditor.Language(rawValue: "twig") + static var typescript = CodeEditor.Language(rawValue: "typescript") + static var vbnet = CodeEditor.Language(rawValue: "vbnet") + static var vbscript = CodeEditor.Language(rawValue: "vbscript") + static var xml = CodeEditor.Language(rawValue: "xml") + static var yaml = CodeEditor.Language(rawValue: "yaml") +} diff --git a/CodeEditModules/Modules/CodeEditor/ThemeName.swift b/CodeEditModules/Modules/CodeEditor/ThemeName.swift new file mode 100644 index 000000000..7a3d9ecfe --- /dev/null +++ b/CodeEditModules/Modules/CodeEditor/ThemeName.swift @@ -0,0 +1,35 @@ +// +// ThemeName.swift +// CodeEditor +// +// Created by Helge Heß. +// Copyright © 2021 ZeeZide GmbH. All rights reserved. +// + +public extension CodeEditor { + + @frozen + struct ThemeName: TypedString { + + public let rawValue : String + + @inlinable + public init(rawValue: String) { self.rawValue = rawValue } + } +} + +public extension CodeEditor.ThemeName { + + static var `default` = pojoaque + + static var pojoaque = CodeEditor.ThemeName(rawValue: "pojoaque") + static var agate = CodeEditor.ThemeName(rawValue: "agate") + static var ocean = CodeEditor.ThemeName(rawValue: "ocean") + static var xcode = CodeEditor.ThemeName(rawValue: "xcode") + static var github = CodeEditor.ThemeName(rawValue: "github") + static var googlecode = CodeEditor.ThemeName(rawValue: "googlecode") + static var vs = CodeEditor.ThemeName(rawValue: "vs") + + static var atelierSavannaLight = CodeEditor.ThemeName(rawValue: "atelier-savanna-light") + static var atelierSavannaDark = CodeEditor.ThemeName(rawValue: "atelier-savanna-dark") +} diff --git a/CodeEditModules/Modules/CodeEditor/TypedString.swift b/CodeEditModules/Modules/CodeEditor/TypedString.swift new file mode 100644 index 000000000..07c13e32e --- /dev/null +++ b/CodeEditModules/Modules/CodeEditor/TypedString.swift @@ -0,0 +1,32 @@ +// +// TypedString.swift +// CodeEditor +// +// Created by Helge Heß. +// Copyright © 2021 ZeeZide GmbH. All rights reserved. +// + +import SwiftUI + +/** + * Simple helper to make typed strings. + */ +public protocol TypedString: RawRepresentable, Hashable, Comparable, Codable, + CustomStringConvertible, + Identifiable +{ + var rawValue: String { get } +} + +public extension TypedString where RawValue == String { + + @inlinable + var description : String { return self.rawValue } + @inlinable + var id : String { return self.rawValue } + + @inlinable + static func < (lhs: Self, rhs: Self) -> Bool { + return lhs.rawValue < rhs.rawValue + } +} diff --git a/CodeEditModules/Modules/CodeEditor/UXCodeTextView.swift b/CodeEditModules/Modules/CodeEditor/UXCodeTextView.swift new file mode 100644 index 000000000..883c57e2a --- /dev/null +++ b/CodeEditModules/Modules/CodeEditor/UXCodeTextView.swift @@ -0,0 +1,326 @@ +// +// UXCodeTextView.swift +// CodeEditor +// +// Created by Helge Heß. +// Copyright © 2021 ZeeZide GmbH. All rights reserved. +// + +import Highlightr + +#if os(macOS) +import AppKit + +typealias UXTextView = NSTextView +typealias UXTextViewDelegate = NSTextViewDelegate +#else +import UIKit + +typealias UXTextView = UITextView +typealias UXTextViewDelegate = UITextViewDelegate +#endif + +/** + * Subclass of NSTextView/UITextView which adds some code editing features to + * the respective Cocoa views. + * + * Currently pretty tightly coupled to `CodeEditor`. + */ +final class UXCodeTextView: UXTextView { + + fileprivate let highlightr = Highlightr() + + private var hlTextStorage : CodeAttributedString? { + return textStorage as? CodeAttributedString + } + + /// If the user starts a newline, the editor automagically adds the same + /// whitespace as on the previous line. + var isSmartIndentEnabled = true + + var indentStyle = CodeEditor.IndentStyle.system { + didSet { + guard oldValue != indentStyle else { return } + reindent(oldStyle: oldValue) + } + } + + var autoPairCompletion = [ String : String ]() + + var language : CodeEditor.Language? { + set { + guard hlTextStorage?.language != newValue?.rawValue else { return } + hlTextStorage?.language = newValue?.rawValue + } + get { return hlTextStorage?.language.flatMap(CodeEditor.Language.init) } + } + private(set) var themeName = CodeEditor.ThemeName.default { + didSet { + highlightr?.setTheme(to: themeName.rawValue) + if let font = highlightr?.theme?.codeFont { self.font = font } + } + } + + init() { + let textStorage = highlightr.flatMap { + CodeAttributedString(highlightr: $0) + } + ?? NSTextStorage() + + let layoutManager = NSLayoutManager() + textStorage.addLayoutManager(layoutManager) + + let textContainer = NSTextContainer() + textContainer.widthTracksTextView = true // those are key! + layoutManager.addTextContainer(textContainer) + + super.init(frame: .zero, textContainer: textContainer) + +#if os(macOS) + isVerticallyResizable = true + maxSize = .init(width: 0, height: 1_000_000) + + isRichText = false + allowsImageEditing = false + isGrammarCheckingEnabled = false + isContinuousSpellCheckingEnabled = false + isAutomaticSpellingCorrectionEnabled = false + isAutomaticLinkDetectionEnabled = false + isAutomaticDashSubstitutionEnabled = false + isAutomaticQuoteSubstitutionEnabled = false + usesRuler = false +#endif + } + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + + // MARK: - Actions + +#if os(macOS) + override func changeFont(_ sender: Any?) { + let coordinator = delegate as? UXCodeTextViewDelegate + + let old = coordinator?.fontSize + ?? highlightr?.theme?.codeFont?.pointSize + ?? NSFont.systemFontSize + let new : CGFloat + + let fm = NSFontManager.shared + switch fm.currentFontAction { + case .sizeUpFontAction : new = old + 1 + case .sizeDownFontAction : new = old - 1 + + case .viaPanelFontAction : + guard let font = fm.selectedFont else { + return super.changeFont(sender) + } + new = font.pointSize + + case .addTraitFontAction, .removeTraitFontAction: // bold/italic + NSSound.beep() + return + + default: + guard let font = fm.selectedFont else { + return super.changeFont(sender) + } + new = font.pointSize + } + + coordinator?.fontSize = new + applyNewFontSize(new) + } +#endif // macOS + + override func copy(_ sender: Any?) { + guard let coordinator = delegate as? UXCodeTextViewDelegate else { + assertionFailure("Expected coordinator as delegate") + return super.copy(sender) + } + if coordinator.allowCopy { super.copy(sender) } + } + + +#if os(macOS) + // MARK: - Smarts as shown in https://github.com/naoty/NTYSmartTextView + + private var isAutoPairEnabled : Bool { return !autoPairCompletion.isEmpty } + + override func insertNewline(_ sender: Any?) { + guard isSmartIndentEnabled else { return super.insertNewline(sender) } + + let currentLine = self.currentLine + let wsPrefix = currentLine.prefix(while: { + guard let scalar = $0.unicodeScalars.first else { return false } + return CharacterSet.whitespaces.contains(scalar) // yes, yes + }) + + super.insertNewline(sender) + + if !wsPrefix.isEmpty { + insertText(String(wsPrefix), replacementRange: selectedRange()) + } + } + + override func insertTab(_ sender: Any?) { + guard case .softTab(let width) = indentStyle else { + return super.insertTab(sender) + } + super.insertText(String(repeating: " ", count: width), + replacementRange: selectedRange()) + } + + override func insertText(_ string: Any, replacementRange: NSRange) { + super.insertText(string, replacementRange: replacementRange) + guard isAutoPairEnabled else { return } + guard let string = string as? String else { return } // TBD: NSAttrString + + guard let end = autoPairCompletion[string] else { return } + super.insertText(end, replacementRange: selectedRange()) + super.moveBackward(self) + } + + override func deleteBackward(_ sender: Any?) { + guard isAutoPairEnabled, !isStartOrEndOfLine else { + return super.deleteBackward(sender) + } + + let s = self.string + let selectedRange = swiftSelectedRange + guard selectedRange.lowerBound > s.startIndex, + selectedRange.lowerBound < s.endIndex else + { + return super.deleteBackward(sender) + } + + let startIdx = s.index(before: selectedRange.lowerBound) + let startChar = s[startIdx.. Bool { + applyNewTheme(nil, andFontSize: newSize) + } + + @discardableResult + func applyNewTheme(_ newTheme: CodeEditor.ThemeName) -> Bool { + guard themeName != newTheme else { return false } + guard let highlightr = highlightr, + highlightr.setTheme(to: newTheme.rawValue), + let theme = highlightr.theme else { return false } + if let font = theme.codeFont, font !== self.font { self.font = font } + return true + } + + @discardableResult + func applyNewTheme(_ newTheme: CodeEditor.ThemeName? = nil, + andFontSize newSize: CGFloat) -> Bool + { + // Setting the theme reloads it (i.e. makes a "copy"). + guard let highlightr = highlightr, highlightr.setTheme(to: (newTheme ?? themeName).rawValue), + let theme = highlightr.theme else { return false } + + guard theme.codeFont?.pointSize != newSize else { return true } + + theme.codeFont = theme.codeFont? .withSize(newSize) + theme.boldCodeFont = theme.boldCodeFont? .withSize(newSize) + theme.italicCodeFont = theme.italicCodeFont?.withSize(newSize) + if let font = theme.codeFont, font !== self.font { self.font = font } + return true + } +} + +protocol UXCodeTextViewDelegate: UXTextViewDelegate { + + var allowCopy : Bool { get } + var fontSize : CGFloat? { get set } +} + +// MARK: - Smarts as shown in https://github.com/naoty/NTYSmartTextView + +extension UXTextView { + + fileprivate var swiftSelectedRange : Range { + let s = self.string + guard !s.isEmpty else { return s.startIndex.. ( isStart: Bool, isEnd: Bool ) { + let s = self.string + let selectedRange = self.swiftSelectedRange + var lineStart = s.startIndex, lineEnd = s.endIndex, contentEnd = s.endIndex + string.getLineStart(&lineStart, end: &lineEnd, contentsEnd: &contentEnd, + for: selectedRange) + return ( isStart : selectedRange.lowerBound == lineStart, + isEnd : selectedRange.lowerBound == lineEnd ) + } +} + + +// MARK: - UXKit + +#if os(macOS) + +extension NSTextView { + var codeTextStorage : NSTextStorage? { return textStorage } +} +#else // iOS +extension UITextView { + + var string : String { // NeXTstep was right! + set { text = newValue} + get { return text } + } + + var codeTextStorage : NSTextStorage? { return textStorage } +} +#endif // iOS diff --git a/CodeEditModules/Modules/CodeEditor/UXCodeTextViewRepresentable.swift b/CodeEditModules/Modules/CodeEditor/UXCodeTextViewRepresentable.swift new file mode 100644 index 000000000..a5680c335 --- /dev/null +++ b/CodeEditModules/Modules/CodeEditor/UXCodeTextViewRepresentable.swift @@ -0,0 +1,233 @@ +// +// UXCodeTextViewRepresentable.swift +// CodeEditor +// +// Created by Helge Heß. +// Copyright © 2021 ZeeZide GmbH. All rights reserved. +// + +import SwiftUI + +#if os(macOS) +typealias UXViewRepresentable = NSViewRepresentable +#else +typealias UXViewRepresentable = UIViewRepresentable +#endif + +/** + * Move the gritty details our of the main representable. + */ +struct UXCodeTextViewRepresentable : UXViewRepresentable { + + /** + * Configures a CodeEditor View with the given parameters. + * + * - Parameters: + * - source: A binding to a String that holds the source code to be + * edited (or displayed). + * - language: Optionally set a language (e.g. `.swift`), otherwise + * Highlight.js will attempt to detect the language. + * - theme: The name of the theme to use. + * - fontSize: On macOS this Binding can be used to persist the size of + * the font in use. At runtime this is combined with the + * theme to produce the full font information. + * - flags: Configure whether the text is editable and/or selectable. + * - indentStyle: Optionally insert a configurable amount of spaces if the + * user hits "tab". + * - inset: The editor can be inset in the scroll view. Defaults to + * 8/8. + * - autoPairs: A mapping of open/close characters, where the close + * characters are automatically injected when the user enters + * the opening character. For example: `[ "<": ">" ]` would + * automatically insert the closing ">" if the user enters + * "<". + */ + public init(source : Binding, + language : CodeEditor.Language?, + theme : CodeEditor.ThemeName, + fontSize : Binding?, + flags : CodeEditor.Flags, + indentStyle : CodeEditor.IndentStyle, + autoPairs : [ String : String ], + inset : CGSize) + { + self.source = source + self.fontSize = fontSize + self.language = language + self.themeName = theme + self.flags = flags + self.indentStyle = indentStyle + self.autoPairs = autoPairs + self.inset = inset + } + + private var source : Binding + private var fontSize : Binding? + private let language : CodeEditor.Language? + private let themeName : CodeEditor.ThemeName + private let flags : CodeEditor.Flags + private let indentStyle : CodeEditor.IndentStyle + private let inset : CGSize + private let autoPairs : [ String : String ] + + + // MARK: - TextView Delegate Coordinator + + public final class Coordinator: NSObject, UXCodeTextViewDelegate { + + var parent : UXCodeTextViewRepresentable + + var fontSize : CGFloat? { + set { if let value = newValue { parent.fontSize?.wrappedValue = value } } + get { parent.fontSize?.wrappedValue } + } + + init(_ parent: UXCodeTextViewRepresentable) { + self.parent = parent + } + + public func textDidChange(_ notification: Notification) { + guard let textView = notification.object as? UXTextView else { + assertionFailure("unexpected notification object") + return + } + parent.source.wrappedValue = textView.string + } + + var allowCopy: Bool { + return parent.flags.contains(.selectable) + || parent.flags.contains(.editable) + } + } + + public func makeCoordinator() -> Coordinator { + return Coordinator(self) + } + + private func updateTextView(_ textView: UXCodeTextView) { + if let binding = fontSize { + textView.applyNewTheme(themeName, andFontSize: binding.wrappedValue) + } + else { + textView.applyNewTheme(themeName) + } + textView.language = language + + textView.indentStyle = indentStyle + textView.isSmartIndentEnabled = flags.contains(.smartIndent) + textView.autoPairCompletion = autoPairs + + if source.wrappedValue != textView.string { + if let textStorage = textView.codeTextStorage { + textStorage.replaceCharacters(in : NSMakeRange(0, textStorage.length), + with : source.wrappedValue) + } + else { + assertionFailure("no text storage?") + textView.string = source.wrappedValue + } + } + + textView.isEditable = flags.contains(.editable) + textView.isSelectable = flags.contains(.selectable) + } + +#if os(macOS) + public func makeNSView(context: Context) -> NSScrollView { + let textView = UXCodeTextView() + textView.autoresizingMask = [ .width, .height ] + textView.delegate = context.coordinator + textView.allowsUndo = true + textView.textContainerInset = inset + + let scrollView = NSScrollView() + scrollView.hasVerticalScroller = true + scrollView.documentView = textView + + updateTextView(textView) + return scrollView + } + + public func updateNSView(_ scrollView: NSScrollView, context: Context) { + guard let textView = scrollView.documentView as? UXCodeTextView else { + assertionFailure("unexpected text view") + return + } + if textView.delegate !== context.coordinator { + textView.delegate = context.coordinator + } + textView.textContainerInset = inset + updateTextView(textView) + } +#else // iOS etc + private var edgeInsets: UIEdgeInsets { + return UIEdgeInsets( + top : inset.height, left : inset.width, + bottom : inset.height, right : inset.width + ) + } + public func makeUIView(context: Context) -> UITextView { + let textView = UXCodeTextView() + textView.autoresizingMask = [ .flexibleWidth, .flexibleHeight ] + textView.delegate = context.coordinator + textView.textContainerInset = edgeInsets + updateTextView(textView) + return textView + } + + public func updateUIView(_ textView: UITextView, context: Context) { + guard let textView = textView as? UXCodeTextView else { + assertionFailure("unexpected text view") + return + } + if textView.delegate !== context.coordinator { + textView.delegate = context.coordinator + } + textView.textContainerInset = edgeInsets + updateTextView(textView) + } +#endif // iOS +} + +struct UXCodeTextViewRepresentable_Previews: PreviewProvider { + + static var previews: some View { + + UXCodeTextViewRepresentable(source : .constant("let a = 5"), + language : nil, + theme : .pojoaque, + fontSize : nil, + flags : [ .selectable ], + indentStyle : .system, + autoPairs : [:], + inset : .init(width: 8, height: 8)) + .frame(width: 200, height: 100) + + UXCodeTextViewRepresentable(source: .constant("let a = 5"), + language : .swift, + theme : .pojoaque, + fontSize : nil, + flags : [ .selectable ], + indentStyle : .system, + autoPairs : [:], + inset : .init(width: 8, height: 8)) + .frame(width: 200, height: 100) + + UXCodeTextViewRepresentable( + source: .constant( + #""" + The quadratic formula is $-b \pm \sqrt{b^2 - 4ac} \over 2a$ + \bye + """# + ), + language : .tex, + theme : .pojoaque, + fontSize : nil, + flags : [ .selectable ], + indentStyle : .system, + autoPairs : [:], + inset : .init(width: 8, height: 8) + ) + .frame(width: 540, height: 200) + } +} diff --git a/CodeEditModules/Package.swift b/CodeEditModules/Package.swift index 9fd0635b2..86b81f0a5 100644 --- a/CodeEditModules/Package.swift +++ b/CodeEditModules/Package.swift @@ -17,18 +17,24 @@ let package = Package( name: "CodeFile", targets: ["CodeFile"] ), - .library(name: "WelcomeModule", targets: ["WelcomeModule"]) + .library( + name: "WelcomeModule", + targets: ["WelcomeModule"] + ), + .library( + name: "CodeEditor", + targets: ["CodeEditor"] + ) ], dependencies: [ - .package( - name: "CodeEditor", - url: "https://github.com/ZeeZide/CodeEditor.git", - from: "1.2.0" - ), .package( name: "SnapshotTesting", url: "https://github.com/pointfreeco/swift-snapshot-testing.git", from: "1.9.0" + ), + .package( + url: "https://github.com/raspu/Highlightr", + from: "2.1.2" ) ], targets: [ @@ -68,6 +74,13 @@ let package = Package( "SnapshotTesting" ], path: "Modules/WelcomeModule/Tests" + ), + .target( + name: "CodeEditor", + dependencies: [ + "Highlightr" + ], + path: "Modules/CodeEditor" ) ] ) From 1e4f7e783a7a92db6fbca645a1b8a5546a4ca7e1 Mon Sep 17 00:00:00 2001 From: Tihan-Nico Date: Sat, 19 Mar 2022 19:45:23 +0200 Subject: [PATCH 2/2] Revert "Added more editor themes and more language support in the themes" This reverts commit bb34a0755ebeff41ba5a5031cfe4ce5ffb0fed39. --- .../xcshareddata/swiftpm/Package.resolved | 9 + CodeEdit/Settings/GeneralSettingsView.swift | 8 - .../Modules/CodeEditor/CodeEditor.swift | 309 ----------------- .../Modules/CodeEditor/Language.swift | 79 ----- .../Modules/CodeEditor/ThemeName.swift | 35 -- .../Modules/CodeEditor/TypedString.swift | 32 -- .../Modules/CodeEditor/UXCodeTextView.swift | 326 ------------------ .../UXCodeTextViewRepresentable.swift | 233 ------------- CodeEditModules/Package.swift | 25 +- 9 files changed, 15 insertions(+), 1041 deletions(-) delete mode 100644 CodeEditModules/Modules/CodeEditor/CodeEditor.swift delete mode 100644 CodeEditModules/Modules/CodeEditor/Language.swift delete mode 100644 CodeEditModules/Modules/CodeEditor/ThemeName.swift delete mode 100644 CodeEditModules/Modules/CodeEditor/TypedString.swift delete mode 100644 CodeEditModules/Modules/CodeEditor/UXCodeTextView.swift delete mode 100644 CodeEditModules/Modules/CodeEditor/UXCodeTextViewRepresentable.swift diff --git a/CodeEdit.xcworkspace/xcshareddata/swiftpm/Package.resolved b/CodeEdit.xcworkspace/xcshareddata/swiftpm/Package.resolved index 8083b8ae7..8450bfb4c 100644 --- a/CodeEdit.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/CodeEdit.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,6 +1,15 @@ { "object": { "pins": [ + { + "package": "CodeEditor", + "repositoryURL": "https://github.com/ZeeZide/CodeEditor.git", + "state": { + "branch": null, + "revision": "5856fac22b0a2174dbdea212784567c8c9cd1129", + "version": "1.2.0" + } + }, { "package": "Highlightr", "repositoryURL": "https://github.com/raspu/Highlightr", diff --git a/CodeEdit/Settings/GeneralSettingsView.swift b/CodeEdit/Settings/GeneralSettingsView.swift index b9eec6401..91d07a9f1 100644 --- a/CodeEdit/Settings/GeneralSettingsView.swift +++ b/CodeEdit/Settings/GeneralSettingsView.swift @@ -62,14 +62,6 @@ struct GeneralSettingsView: View { .tag(CodeEditor.ThemeName.agate) Text("Ocean") .tag(CodeEditor.ThemeName.ocean) - Text("Xcode") - .tag(CodeEditor.ThemeName.xcode) - Text("Github") - .tag(CodeEditor.ThemeName.github) - Text("Google Code") - .tag(CodeEditor.ThemeName.googlecode) - Text("Visual Studio") - .tag(CodeEditor.ThemeName.vs) } } .padding() diff --git a/CodeEditModules/Modules/CodeEditor/CodeEditor.swift b/CodeEditModules/Modules/CodeEditor/CodeEditor.swift deleted file mode 100644 index 1462648cc..000000000 --- a/CodeEditModules/Modules/CodeEditor/CodeEditor.swift +++ /dev/null @@ -1,309 +0,0 @@ -// -// CodeEditor.swift -// CodeEditor -// -// Created by Helge Heß. -// Copyright © 2021 ZeeZide GmbH. All rights reserved. -// - -import SwiftUI -import Highlightr - -/** - * An simple code editor (or viewer) with highlighting for SwiftUI (iOS and - * macOS). - * - * To use the code editor as a Viewer, simply pass the source code - * - * struct ContentView: View { - * - * var body: some View { - * CodeEditor(source: "let a = 42") - * } - * } - * - * If it should act as an actual editor, pass in a `Binding`: - * - * struct ContentView: View { - * - * @State private var source = "let a = 42\n" - * - * var body: some View { - * CodeEditor(source: $source, language: .swift, theme: .ocean) - * } - * } - * - * ### Languages and Themes - * - * Highlight.js supports more than 180 languages and over 80 different themes. - * - * The available languages and themes can be accessed using: - * - * CodeEditor.availableLanguages - * CodeEditor.availableThemes - * - * They can be used in a SwiftUI `Picker` like so: - * - * @State var source = "let it = be" - * @State var language = CodeEditor.Language.swift - * - * Picker("Language", selection: $language) { - * ForEach(CodeEditor.availableLanguages) { language in - * Text("\(language.rawValue.capitalized)") - * .tag(language) - * } - * } - * - * CodeEditor(source: $source, language: language) - * - * Note: The `CodeEditor` doesn't do automatic theme changes if the appearance - * changes. - * - * ### Smart Indent and Open/Close Pairing - * - * Inspired by [NTYSmartTextView](https://github.com/naoty/NTYSmartTextView), - * `CodeEditor` now also supports (on macOS): - * - smarter indents (preserving the indent of the previous line) - * - soft indents (insert a configurable amount of spaces if the user presses tabs) - * - auto character pairing, e.g. when entering `{`, the matching `}` will be auto-added - * - * To enable smart indents, add the `smartIndent` flag, e.g.: - * - * CodeEditor(source: $source, language: language, - * flags: [ .selectable, .editable, .smartIndent ]) - * - * It is enabled for editors by default. - * - * To configure soft indents, use the `indentStyle` parameter, e.g. - * - * CodeEditor(source: $source, language: language, - * indentStyle: .softTab(width: 2)) - * - * It defaults to tabs, as per system settings. - * - * Auto character pairing is automatic based on the language. E.g. there is a set of - * defaults for C like languages (e.g. Swift), Python or XML. The defaults can be overridden - * using the respective static variable in `CodeEditor`, - * or the desired pairing can be set explicitly: - * - * CodeEditor(source: $source, language: language, - * autoPairs: [ "{": "}", "<": ">", "'": "'" ]) - * - * - * ### Font Sizing - * - * On macOS the editor supports sizing of the font (using Cmd +/Cmd - and the - * font panel). - * To enable sizing commands, the WindowScene needs to have the proper commands - * applied, e.g.: - * - * WindowGroup { - * ContentView() - * } - * .commands { - * TextFormattingCommands() - * } - * - * To persist the binding, the `fontSize` binding is available. - * - * ### Highlightr and Shaper - * - * Based on the excellent [Highlightr](https://github.com/raspu/Highlightr). - * This means that it is using JavaScriptCore as the actual driver. As - * Highlightr says: - * - * > It will never be as fast as a native solution, but it's fast enough to be - * > used on a real time editor. - * - * The editor is similar to (but not exactly the same) the one used by - * [SVG Shaper for SwiftUI](https://zeezide.de/en/products/svgshaper/), - * for its SVG and Swift editor parts. - */ -public struct CodeEditor: View { - - /// Returns the available themes in the associated Highlightr package. - public static var availableThemes = - Highlightr()?.availableThemes().map(ThemeName.init).sorted() ?? [] - - /// Returns the available languages in the associated Highlightr package. - public static var availableLanguages = - Highlightr()?.supportedLanguages().map(Language.init).sorted() ?? [] - - - /** - * Flags available for `CodeEditor`, currently just: - * - `.editable` - * - `.selectable` - */ - @frozen public struct Flags: OptionSet { - public let rawValue : UInt8 - @inlinable public init(rawValue: UInt8) { self.rawValue = rawValue } - - /// `.editable` requires that the `source` of the `CodeEditor` is a - /// `Binding`. - public static let editable = Flags(rawValue: 1 << 0) - - /// Whether the displayed content should be selectable by the user. - public static let selectable = Flags(rawValue: 1 << 1) - - /// If the user starts a newline, the editor automagically adds the same - /// whitespace as on the previous line. - public static let smartIndent = Flags(rawValue: 1 << 2) - - public static let defaultViewerFlags : Flags = [ .selectable ] - public static let defaultEditorFlags : Flags = - [ .selectable, .editable, .smartIndent ] - } - - @frozen public enum IndentStyle: Equatable { - case system - case softTab(width: Int) - } - - /** - * Default auto pairing mappings for languages. - */ - public static var defaultAutoPairs : [ Language : [ String : String ] ] = [ - .c: cStyleAutoPairs, .cpp: cStyleAutoPairs, .objectivec: cStyleAutoPairs, - .swift: cStyleAutoPairs, - .java: cStyleAutoPairs, .javascript: cStyleAutoPairs, - .xml: xmlStyleAutoPairs, - .python: [ "(": ")", "[": "]", "\"": "\"", "'": "'", "`": "`" ] - ] - public static var cStyleAutoPairs = [ - "(": ")", "[": "]", "{": "}", "\"": "\"", "'": "'", "`": "`" - ] - public static var xmlStyleAutoPairs = [ "<": ">", "\"": "\"", "'": "'" ] - - - /** - * Configures a CodeEditor View with the given parameters. - * - * - Parameters: - * - source: A binding to a String that holds the source code to be - * edited (or displayed). - * - language: Optionally set a language (e.g. `.swift`), otherwise - * Highlight.js will attempt to detect the language. - * - theme: The name of the theme to use, defaults to "pojoaque". - * - fontSize: On macOS this Binding can be used to persist the size of - * the font in use. At runtime this is combined with the - * theme to produce the full font information. (optional) - * - flags: Configure whether the text is editable and/or selectable - * (defaults to both). - * - indentStyle: Optionally insert a configurable amount of spaces if the - * user hits "tab". - * - autoPairs: A mapping of open/close characters, where the close - * characters are automatically injected when the user enters - * the opening character. For example: `[ "{": "}" ]` would - * automatically insert the closing "}" if the user enters - * "{". If no value is given, the default mapping for the - * language is used. - * - inset: The editor can be inset in the scroll view. Defaults to - * 8/8. - */ - public init(source : Binding, - language : Language? = nil, - theme : ThemeName = .default, - fontSize : Binding? = nil, - flags : Flags = .defaultEditorFlags, - indentStyle : IndentStyle = .system, - autoPairs : [ String : String ]? = nil, - inset : CGSize? = nil) - { - self.source = source - self.fontSize = fontSize - self.language = language - self.themeName = theme - self.flags = flags - self.indentStyle = indentStyle - self.inset = inset ?? CGSize(width: 8, height: 8) - self.autoPairs = autoPairs - ?? language.flatMap({ CodeEditor.defaultAutoPairs[$0] }) - ?? [:] - } - - /** - * Configures a read-only CodeEditor View with the given parameters. - * - * - Parameters: - * - source: A String that holds the source code to be displayed. - * - language: Optionally set a language (e.g. `.swift`), otherwise - * Highlight.js will attempt to detect the language. - * - theme: The name of the theme to use, defaults to "pojoaque". - * - fontSize: On macOS this Binding can be used to persist the size of - * the font in use. At runtime this is combined with the - * theme to produce the full font information. (optional) - * - flags: Configure whether the text is selectable - * (defaults to both). - * - indentStyle: Optionally insert a configurable amount of spaces if the - * user hits "tab". - * - autoPairs: A mapping of open/close characters, where the close - * characters are automatically injected when the user enters - * the opening character. For example: `[ "{": "}" ]` would - * automatically insert the closing "}" if the user enters - * "{". If no value is given, the default mapping for the - * language is used. - * - inset: The editor can be inset in the scroll view. Defaults to - * 8/8. - */ - @inlinable - public init(source : String, - language : Language? = nil, - theme : ThemeName = .default, - fontSize : Binding? = nil, - flags : Flags = .defaultViewerFlags, - indentStyle : IndentStyle = .system, - autoPairs : [ String : String ]? = nil, - inset : CGSize? = nil) - { - assert(!flags.contains(.editable), "Editing requires a Binding") - self.init(source : .constant(source), - language : language, - theme : theme, - fontSize : fontSize, - flags : flags.subtracting(.editable), - indentStyle : indentStyle, - autoPairs : autoPairs, - inset : inset) - } - - private var source : Binding - private var fontSize : Binding? - private let language : Language? - private let themeName : ThemeName - private let flags : Flags - private let indentStyle : IndentStyle - private let autoPairs : [ String : String ] - private let inset : CGSize - - public var body: some View { - UXCodeTextViewRepresentable(source : source, - language : language, - theme : themeName, - fontSize : fontSize, - flags : flags, - indentStyle : indentStyle, - autoPairs : autoPairs, - inset : inset) - } -} - -struct CodeEditor_Previews: PreviewProvider { - - static var previews: some View { - - CodeEditor(source: "let a = 5") - .frame(width: 200, height: 100) - - CodeEditor(source: "let a = 5", language: .swift, theme: .pojoaque) - .frame(width: 200, height: 100) - - CodeEditor(source: - #""" - The quadratic formula is $-b \pm \sqrt{b^2 - 4ac} \over 2a$ - \bye - """#, language: .tex - ) - .frame(width: 540, height: 200) - } -} diff --git a/CodeEditModules/Modules/CodeEditor/Language.swift b/CodeEditModules/Modules/CodeEditor/Language.swift deleted file mode 100644 index 01a19702f..000000000 --- a/CodeEditModules/Modules/CodeEditor/Language.swift +++ /dev/null @@ -1,79 +0,0 @@ -// -// Language.swift -// CodeEditor -// -// Created by Helge Heß. -// Copyright © 2021 ZeeZide GmbH. All rights reserved. -// - -public extension CodeEditor { - - @frozen - struct Language: TypedString { - - public let rawValue : String - - @inlinable - public init(rawValue: String) { self.rawValue = rawValue } - } -} - -public extension CodeEditor.Language { - - static var accesslog = CodeEditor.Language(rawValue: "accesslog") - static var actionscript = CodeEditor.Language(rawValue: "actionscript") - static var ada = CodeEditor.Language(rawValue: "ada") - static var apache = CodeEditor.Language(rawValue: "apache") - static var applescript = CodeEditor.Language(rawValue: "applescript") - static var bash = CodeEditor.Language(rawValue: "bash") - static var basic = CodeEditor.Language(rawValue: "basic") - static var brainfuck = CodeEditor.Language(rawValue: "brainfuck") - static var c = CodeEditor.Language(rawValue: "c") - static var clojure = CodeEditor.Language(rawValue: "clojure") - static var coffeescript = CodeEditor.Language(rawValue: "coffeescript") - static var cmake = CodeEditor.Language(rawValue: "cmake") - static var cpp = CodeEditor.Language(rawValue: "cpp") - static var cs = CodeEditor.Language(rawValue: "cs") - static var css = CodeEditor.Language(rawValue: "css") - static var diff = CodeEditor.Language(rawValue: "diff") - static var delphi = CodeEditor.Language(rawValue: "delphi") - static var django = CodeEditor.Language(rawValue: "django") - static var dockerfile = CodeEditor.Language(rawValue: "dockerfile") - static var fsharp = CodeEditor.Language(rawValue: "fsharp") - static var dart = CodeEditor.Language(rawValue: "dart") - static var go = CodeEditor.Language(rawValue: "go") - static var gradle = CodeEditor.Language(rawValue: "gradle") - static var groovy = CodeEditor.Language(rawValue: "groovy") - static var http = CodeEditor.Language(rawValue: "http") - static var java = CodeEditor.Language(rawValue: "java") - static var javascript = CodeEditor.Language(rawValue: "javascript") - static var json = CodeEditor.Language(rawValue: "json") - static var lua = CodeEditor.Language(rawValue: "lua") - static var markdown = CodeEditor.Language(rawValue: "markdown") - static var makefile = CodeEditor.Language(rawValue: "makefile") - static var mathematica = CodeEditor.Language(rawValue: "mathematica") - static var matlab = CodeEditor.Language(rawValue: "matlab") - static var nginx = CodeEditor.Language(rawValue: "nginx") - static var objectivec = CodeEditor.Language(rawValue: "objectivec") - static var perl = CodeEditor.Language(rawValue: "perl") - static var pgsql = CodeEditor.Language(rawValue: "pgsql") - static var php = CodeEditor.Language(rawValue: "php") - static var python = CodeEditor.Language(rawValue: "python") - static var protobuf = CodeEditor.Language(rawValue: "protobuf") - static var ruby = CodeEditor.Language(rawValue: "ruby") - static var rust = CodeEditor.Language(rawValue: "rust") - static var scala = CodeEditor.Language(rawValue: "scala") - static var scss = CodeEditor.Language(rawValue: "scss") - static var shell = CodeEditor.Language(rawValue: "shell") - static var smalltalk = CodeEditor.Language(rawValue: "smalltalk") - static var sql = CodeEditor.Language(rawValue: "sql") - static var swift = CodeEditor.Language(rawValue: "swift") - static var tcl = CodeEditor.Language(rawValue: "tcl") - static var tex = CodeEditor.Language(rawValue: "tex") - static var twig = CodeEditor.Language(rawValue: "twig") - static var typescript = CodeEditor.Language(rawValue: "typescript") - static var vbnet = CodeEditor.Language(rawValue: "vbnet") - static var vbscript = CodeEditor.Language(rawValue: "vbscript") - static var xml = CodeEditor.Language(rawValue: "xml") - static var yaml = CodeEditor.Language(rawValue: "yaml") -} diff --git a/CodeEditModules/Modules/CodeEditor/ThemeName.swift b/CodeEditModules/Modules/CodeEditor/ThemeName.swift deleted file mode 100644 index 7a3d9ecfe..000000000 --- a/CodeEditModules/Modules/CodeEditor/ThemeName.swift +++ /dev/null @@ -1,35 +0,0 @@ -// -// ThemeName.swift -// CodeEditor -// -// Created by Helge Heß. -// Copyright © 2021 ZeeZide GmbH. All rights reserved. -// - -public extension CodeEditor { - - @frozen - struct ThemeName: TypedString { - - public let rawValue : String - - @inlinable - public init(rawValue: String) { self.rawValue = rawValue } - } -} - -public extension CodeEditor.ThemeName { - - static var `default` = pojoaque - - static var pojoaque = CodeEditor.ThemeName(rawValue: "pojoaque") - static var agate = CodeEditor.ThemeName(rawValue: "agate") - static var ocean = CodeEditor.ThemeName(rawValue: "ocean") - static var xcode = CodeEditor.ThemeName(rawValue: "xcode") - static var github = CodeEditor.ThemeName(rawValue: "github") - static var googlecode = CodeEditor.ThemeName(rawValue: "googlecode") - static var vs = CodeEditor.ThemeName(rawValue: "vs") - - static var atelierSavannaLight = CodeEditor.ThemeName(rawValue: "atelier-savanna-light") - static var atelierSavannaDark = CodeEditor.ThemeName(rawValue: "atelier-savanna-dark") -} diff --git a/CodeEditModules/Modules/CodeEditor/TypedString.swift b/CodeEditModules/Modules/CodeEditor/TypedString.swift deleted file mode 100644 index 07c13e32e..000000000 --- a/CodeEditModules/Modules/CodeEditor/TypedString.swift +++ /dev/null @@ -1,32 +0,0 @@ -// -// TypedString.swift -// CodeEditor -// -// Created by Helge Heß. -// Copyright © 2021 ZeeZide GmbH. All rights reserved. -// - -import SwiftUI - -/** - * Simple helper to make typed strings. - */ -public protocol TypedString: RawRepresentable, Hashable, Comparable, Codable, - CustomStringConvertible, - Identifiable -{ - var rawValue: String { get } -} - -public extension TypedString where RawValue == String { - - @inlinable - var description : String { return self.rawValue } - @inlinable - var id : String { return self.rawValue } - - @inlinable - static func < (lhs: Self, rhs: Self) -> Bool { - return lhs.rawValue < rhs.rawValue - } -} diff --git a/CodeEditModules/Modules/CodeEditor/UXCodeTextView.swift b/CodeEditModules/Modules/CodeEditor/UXCodeTextView.swift deleted file mode 100644 index 883c57e2a..000000000 --- a/CodeEditModules/Modules/CodeEditor/UXCodeTextView.swift +++ /dev/null @@ -1,326 +0,0 @@ -// -// UXCodeTextView.swift -// CodeEditor -// -// Created by Helge Heß. -// Copyright © 2021 ZeeZide GmbH. All rights reserved. -// - -import Highlightr - -#if os(macOS) -import AppKit - -typealias UXTextView = NSTextView -typealias UXTextViewDelegate = NSTextViewDelegate -#else -import UIKit - -typealias UXTextView = UITextView -typealias UXTextViewDelegate = UITextViewDelegate -#endif - -/** - * Subclass of NSTextView/UITextView which adds some code editing features to - * the respective Cocoa views. - * - * Currently pretty tightly coupled to `CodeEditor`. - */ -final class UXCodeTextView: UXTextView { - - fileprivate let highlightr = Highlightr() - - private var hlTextStorage : CodeAttributedString? { - return textStorage as? CodeAttributedString - } - - /// If the user starts a newline, the editor automagically adds the same - /// whitespace as on the previous line. - var isSmartIndentEnabled = true - - var indentStyle = CodeEditor.IndentStyle.system { - didSet { - guard oldValue != indentStyle else { return } - reindent(oldStyle: oldValue) - } - } - - var autoPairCompletion = [ String : String ]() - - var language : CodeEditor.Language? { - set { - guard hlTextStorage?.language != newValue?.rawValue else { return } - hlTextStorage?.language = newValue?.rawValue - } - get { return hlTextStorage?.language.flatMap(CodeEditor.Language.init) } - } - private(set) var themeName = CodeEditor.ThemeName.default { - didSet { - highlightr?.setTheme(to: themeName.rawValue) - if let font = highlightr?.theme?.codeFont { self.font = font } - } - } - - init() { - let textStorage = highlightr.flatMap { - CodeAttributedString(highlightr: $0) - } - ?? NSTextStorage() - - let layoutManager = NSLayoutManager() - textStorage.addLayoutManager(layoutManager) - - let textContainer = NSTextContainer() - textContainer.widthTracksTextView = true // those are key! - layoutManager.addTextContainer(textContainer) - - super.init(frame: .zero, textContainer: textContainer) - -#if os(macOS) - isVerticallyResizable = true - maxSize = .init(width: 0, height: 1_000_000) - - isRichText = false - allowsImageEditing = false - isGrammarCheckingEnabled = false - isContinuousSpellCheckingEnabled = false - isAutomaticSpellingCorrectionEnabled = false - isAutomaticLinkDetectionEnabled = false - isAutomaticDashSubstitutionEnabled = false - isAutomaticQuoteSubstitutionEnabled = false - usesRuler = false -#endif - } - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - - // MARK: - Actions - -#if os(macOS) - override func changeFont(_ sender: Any?) { - let coordinator = delegate as? UXCodeTextViewDelegate - - let old = coordinator?.fontSize - ?? highlightr?.theme?.codeFont?.pointSize - ?? NSFont.systemFontSize - let new : CGFloat - - let fm = NSFontManager.shared - switch fm.currentFontAction { - case .sizeUpFontAction : new = old + 1 - case .sizeDownFontAction : new = old - 1 - - case .viaPanelFontAction : - guard let font = fm.selectedFont else { - return super.changeFont(sender) - } - new = font.pointSize - - case .addTraitFontAction, .removeTraitFontAction: // bold/italic - NSSound.beep() - return - - default: - guard let font = fm.selectedFont else { - return super.changeFont(sender) - } - new = font.pointSize - } - - coordinator?.fontSize = new - applyNewFontSize(new) - } -#endif // macOS - - override func copy(_ sender: Any?) { - guard let coordinator = delegate as? UXCodeTextViewDelegate else { - assertionFailure("Expected coordinator as delegate") - return super.copy(sender) - } - if coordinator.allowCopy { super.copy(sender) } - } - - -#if os(macOS) - // MARK: - Smarts as shown in https://github.com/naoty/NTYSmartTextView - - private var isAutoPairEnabled : Bool { return !autoPairCompletion.isEmpty } - - override func insertNewline(_ sender: Any?) { - guard isSmartIndentEnabled else { return super.insertNewline(sender) } - - let currentLine = self.currentLine - let wsPrefix = currentLine.prefix(while: { - guard let scalar = $0.unicodeScalars.first else { return false } - return CharacterSet.whitespaces.contains(scalar) // yes, yes - }) - - super.insertNewline(sender) - - if !wsPrefix.isEmpty { - insertText(String(wsPrefix), replacementRange: selectedRange()) - } - } - - override func insertTab(_ sender: Any?) { - guard case .softTab(let width) = indentStyle else { - return super.insertTab(sender) - } - super.insertText(String(repeating: " ", count: width), - replacementRange: selectedRange()) - } - - override func insertText(_ string: Any, replacementRange: NSRange) { - super.insertText(string, replacementRange: replacementRange) - guard isAutoPairEnabled else { return } - guard let string = string as? String else { return } // TBD: NSAttrString - - guard let end = autoPairCompletion[string] else { return } - super.insertText(end, replacementRange: selectedRange()) - super.moveBackward(self) - } - - override func deleteBackward(_ sender: Any?) { - guard isAutoPairEnabled, !isStartOrEndOfLine else { - return super.deleteBackward(sender) - } - - let s = self.string - let selectedRange = swiftSelectedRange - guard selectedRange.lowerBound > s.startIndex, - selectedRange.lowerBound < s.endIndex else - { - return super.deleteBackward(sender) - } - - let startIdx = s.index(before: selectedRange.lowerBound) - let startChar = s[startIdx.. Bool { - applyNewTheme(nil, andFontSize: newSize) - } - - @discardableResult - func applyNewTheme(_ newTheme: CodeEditor.ThemeName) -> Bool { - guard themeName != newTheme else { return false } - guard let highlightr = highlightr, - highlightr.setTheme(to: newTheme.rawValue), - let theme = highlightr.theme else { return false } - if let font = theme.codeFont, font !== self.font { self.font = font } - return true - } - - @discardableResult - func applyNewTheme(_ newTheme: CodeEditor.ThemeName? = nil, - andFontSize newSize: CGFloat) -> Bool - { - // Setting the theme reloads it (i.e. makes a "copy"). - guard let highlightr = highlightr, highlightr.setTheme(to: (newTheme ?? themeName).rawValue), - let theme = highlightr.theme else { return false } - - guard theme.codeFont?.pointSize != newSize else { return true } - - theme.codeFont = theme.codeFont? .withSize(newSize) - theme.boldCodeFont = theme.boldCodeFont? .withSize(newSize) - theme.italicCodeFont = theme.italicCodeFont?.withSize(newSize) - if let font = theme.codeFont, font !== self.font { self.font = font } - return true - } -} - -protocol UXCodeTextViewDelegate: UXTextViewDelegate { - - var allowCopy : Bool { get } - var fontSize : CGFloat? { get set } -} - -// MARK: - Smarts as shown in https://github.com/naoty/NTYSmartTextView - -extension UXTextView { - - fileprivate var swiftSelectedRange : Range { - let s = self.string - guard !s.isEmpty else { return s.startIndex.. ( isStart: Bool, isEnd: Bool ) { - let s = self.string - let selectedRange = self.swiftSelectedRange - var lineStart = s.startIndex, lineEnd = s.endIndex, contentEnd = s.endIndex - string.getLineStart(&lineStart, end: &lineEnd, contentsEnd: &contentEnd, - for: selectedRange) - return ( isStart : selectedRange.lowerBound == lineStart, - isEnd : selectedRange.lowerBound == lineEnd ) - } -} - - -// MARK: - UXKit - -#if os(macOS) - -extension NSTextView { - var codeTextStorage : NSTextStorage? { return textStorage } -} -#else // iOS -extension UITextView { - - var string : String { // NeXTstep was right! - set { text = newValue} - get { return text } - } - - var codeTextStorage : NSTextStorage? { return textStorage } -} -#endif // iOS diff --git a/CodeEditModules/Modules/CodeEditor/UXCodeTextViewRepresentable.swift b/CodeEditModules/Modules/CodeEditor/UXCodeTextViewRepresentable.swift deleted file mode 100644 index a5680c335..000000000 --- a/CodeEditModules/Modules/CodeEditor/UXCodeTextViewRepresentable.swift +++ /dev/null @@ -1,233 +0,0 @@ -// -// UXCodeTextViewRepresentable.swift -// CodeEditor -// -// Created by Helge Heß. -// Copyright © 2021 ZeeZide GmbH. All rights reserved. -// - -import SwiftUI - -#if os(macOS) -typealias UXViewRepresentable = NSViewRepresentable -#else -typealias UXViewRepresentable = UIViewRepresentable -#endif - -/** - * Move the gritty details our of the main representable. - */ -struct UXCodeTextViewRepresentable : UXViewRepresentable { - - /** - * Configures a CodeEditor View with the given parameters. - * - * - Parameters: - * - source: A binding to a String that holds the source code to be - * edited (or displayed). - * - language: Optionally set a language (e.g. `.swift`), otherwise - * Highlight.js will attempt to detect the language. - * - theme: The name of the theme to use. - * - fontSize: On macOS this Binding can be used to persist the size of - * the font in use. At runtime this is combined with the - * theme to produce the full font information. - * - flags: Configure whether the text is editable and/or selectable. - * - indentStyle: Optionally insert a configurable amount of spaces if the - * user hits "tab". - * - inset: The editor can be inset in the scroll view. Defaults to - * 8/8. - * - autoPairs: A mapping of open/close characters, where the close - * characters are automatically injected when the user enters - * the opening character. For example: `[ "<": ">" ]` would - * automatically insert the closing ">" if the user enters - * "<". - */ - public init(source : Binding, - language : CodeEditor.Language?, - theme : CodeEditor.ThemeName, - fontSize : Binding?, - flags : CodeEditor.Flags, - indentStyle : CodeEditor.IndentStyle, - autoPairs : [ String : String ], - inset : CGSize) - { - self.source = source - self.fontSize = fontSize - self.language = language - self.themeName = theme - self.flags = flags - self.indentStyle = indentStyle - self.autoPairs = autoPairs - self.inset = inset - } - - private var source : Binding - private var fontSize : Binding? - private let language : CodeEditor.Language? - private let themeName : CodeEditor.ThemeName - private let flags : CodeEditor.Flags - private let indentStyle : CodeEditor.IndentStyle - private let inset : CGSize - private let autoPairs : [ String : String ] - - - // MARK: - TextView Delegate Coordinator - - public final class Coordinator: NSObject, UXCodeTextViewDelegate { - - var parent : UXCodeTextViewRepresentable - - var fontSize : CGFloat? { - set { if let value = newValue { parent.fontSize?.wrappedValue = value } } - get { parent.fontSize?.wrappedValue } - } - - init(_ parent: UXCodeTextViewRepresentable) { - self.parent = parent - } - - public func textDidChange(_ notification: Notification) { - guard let textView = notification.object as? UXTextView else { - assertionFailure("unexpected notification object") - return - } - parent.source.wrappedValue = textView.string - } - - var allowCopy: Bool { - return parent.flags.contains(.selectable) - || parent.flags.contains(.editable) - } - } - - public func makeCoordinator() -> Coordinator { - return Coordinator(self) - } - - private func updateTextView(_ textView: UXCodeTextView) { - if let binding = fontSize { - textView.applyNewTheme(themeName, andFontSize: binding.wrappedValue) - } - else { - textView.applyNewTheme(themeName) - } - textView.language = language - - textView.indentStyle = indentStyle - textView.isSmartIndentEnabled = flags.contains(.smartIndent) - textView.autoPairCompletion = autoPairs - - if source.wrappedValue != textView.string { - if let textStorage = textView.codeTextStorage { - textStorage.replaceCharacters(in : NSMakeRange(0, textStorage.length), - with : source.wrappedValue) - } - else { - assertionFailure("no text storage?") - textView.string = source.wrappedValue - } - } - - textView.isEditable = flags.contains(.editable) - textView.isSelectable = flags.contains(.selectable) - } - -#if os(macOS) - public func makeNSView(context: Context) -> NSScrollView { - let textView = UXCodeTextView() - textView.autoresizingMask = [ .width, .height ] - textView.delegate = context.coordinator - textView.allowsUndo = true - textView.textContainerInset = inset - - let scrollView = NSScrollView() - scrollView.hasVerticalScroller = true - scrollView.documentView = textView - - updateTextView(textView) - return scrollView - } - - public func updateNSView(_ scrollView: NSScrollView, context: Context) { - guard let textView = scrollView.documentView as? UXCodeTextView else { - assertionFailure("unexpected text view") - return - } - if textView.delegate !== context.coordinator { - textView.delegate = context.coordinator - } - textView.textContainerInset = inset - updateTextView(textView) - } -#else // iOS etc - private var edgeInsets: UIEdgeInsets { - return UIEdgeInsets( - top : inset.height, left : inset.width, - bottom : inset.height, right : inset.width - ) - } - public func makeUIView(context: Context) -> UITextView { - let textView = UXCodeTextView() - textView.autoresizingMask = [ .flexibleWidth, .flexibleHeight ] - textView.delegate = context.coordinator - textView.textContainerInset = edgeInsets - updateTextView(textView) - return textView - } - - public func updateUIView(_ textView: UITextView, context: Context) { - guard let textView = textView as? UXCodeTextView else { - assertionFailure("unexpected text view") - return - } - if textView.delegate !== context.coordinator { - textView.delegate = context.coordinator - } - textView.textContainerInset = edgeInsets - updateTextView(textView) - } -#endif // iOS -} - -struct UXCodeTextViewRepresentable_Previews: PreviewProvider { - - static var previews: some View { - - UXCodeTextViewRepresentable(source : .constant("let a = 5"), - language : nil, - theme : .pojoaque, - fontSize : nil, - flags : [ .selectable ], - indentStyle : .system, - autoPairs : [:], - inset : .init(width: 8, height: 8)) - .frame(width: 200, height: 100) - - UXCodeTextViewRepresentable(source: .constant("let a = 5"), - language : .swift, - theme : .pojoaque, - fontSize : nil, - flags : [ .selectable ], - indentStyle : .system, - autoPairs : [:], - inset : .init(width: 8, height: 8)) - .frame(width: 200, height: 100) - - UXCodeTextViewRepresentable( - source: .constant( - #""" - The quadratic formula is $-b \pm \sqrt{b^2 - 4ac} \over 2a$ - \bye - """# - ), - language : .tex, - theme : .pojoaque, - fontSize : nil, - flags : [ .selectable ], - indentStyle : .system, - autoPairs : [:], - inset : .init(width: 8, height: 8) - ) - .frame(width: 540, height: 200) - } -} diff --git a/CodeEditModules/Package.swift b/CodeEditModules/Package.swift index 86b81f0a5..9fd0635b2 100644 --- a/CodeEditModules/Package.swift +++ b/CodeEditModules/Package.swift @@ -17,24 +17,18 @@ let package = Package( name: "CodeFile", targets: ["CodeFile"] ), - .library( - name: "WelcomeModule", - targets: ["WelcomeModule"] - ), - .library( - name: "CodeEditor", - targets: ["CodeEditor"] - ) + .library(name: "WelcomeModule", targets: ["WelcomeModule"]) ], dependencies: [ + .package( + name: "CodeEditor", + url: "https://github.com/ZeeZide/CodeEditor.git", + from: "1.2.0" + ), .package( name: "SnapshotTesting", url: "https://github.com/pointfreeco/swift-snapshot-testing.git", from: "1.9.0" - ), - .package( - url: "https://github.com/raspu/Highlightr", - from: "2.1.2" ) ], targets: [ @@ -74,13 +68,6 @@ let package = Package( "SnapshotTesting" ], path: "Modules/WelcomeModule/Tests" - ), - .target( - name: "CodeEditor", - dependencies: [ - "Highlightr" - ], - path: "Modules/CodeEditor" ) ] )