From e2f605276e6d41f9f086091c78141d86bbd9cf7b Mon Sep 17 00:00:00 2001 From: marcocarnevali Date: Mon, 21 Mar 2022 11:36:15 +0100 Subject: [PATCH] feat: new code editor --- .../xcshareddata/swiftpm/Package.resolved | 9 - .../Documents/WorkspaceCodeFileView.swift | 3 + CodeEdit/Documents/WorkspaceDocument.swift | 2 +- .../Quick Open/QuickOpenPreviewView.swift | 9 +- CodeEdit/Settings/GeneralSettingsView.swift | 16 +- CodeEdit/SideBar/SideBarItem.swift | 8 +- CodeEdit/WorkspaceView.swift | 6 +- .../xcshareddata/xcschemes/CodeFile.xcscheme | 67 +++++ .../CodeFile/src/CodeEditor+AppStorage.swift | 17 -- .../Modules/CodeFile/src/CodeEditor.swift | 148 +++++++++++ .../Modules/CodeFile/src/CodeFile.swift | 21 -- .../Modules/CodeFile/src/CodeFileView.swift | 32 ++- .../CodeFile/src/LineGutter/LineGutter.swift | 232 ++++++++++++++++++ .../Modules/CodeFile/src/Model/Language.swift | 213 ++++++++++++++++ .../Modules/CodeFile/src/Model/Theme.swift | 18 ++ .../src/TextView/CodeEditorTextView.swift | 88 +++++++ .../Modules/CodeFile/src/ThemedCodeView.swift | 46 ---- CodeEditModules/Package.swift | 8 +- 18 files changed, 824 insertions(+), 119 deletions(-) create mode 100644 CodeEditModules/.swiftpm/xcode/xcshareddata/xcschemes/CodeFile.xcscheme delete mode 100644 CodeEditModules/Modules/CodeFile/src/CodeEditor+AppStorage.swift create mode 100644 CodeEditModules/Modules/CodeFile/src/CodeEditor.swift create mode 100644 CodeEditModules/Modules/CodeFile/src/LineGutter/LineGutter.swift create mode 100644 CodeEditModules/Modules/CodeFile/src/Model/Language.swift create mode 100644 CodeEditModules/Modules/CodeFile/src/Model/Theme.swift create mode 100644 CodeEditModules/Modules/CodeFile/src/TextView/CodeEditorTextView.swift delete mode 100644 CodeEditModules/Modules/CodeFile/src/ThemedCodeView.swift diff --git a/CodeEdit.xcworkspace/xcshareddata/swiftpm/Package.resolved b/CodeEdit.xcworkspace/xcshareddata/swiftpm/Package.resolved index 7d8fafa3f9..ee206aceba 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/Documents/WorkspaceCodeFileView.swift b/CodeEdit/Documents/WorkspaceCodeFileView.swift index a5deea8514..1d1fe1dfeb 100644 --- a/CodeEdit/Documents/WorkspaceCodeFileView.swift +++ b/CodeEdit/Documents/WorkspaceCodeFileView.swift @@ -16,6 +16,9 @@ struct WorkspaceCodeFileView: View { @ViewBuilder var body: some View { if let item = workspace.openFileItems.first(where: { file in + if file.id == workspace.selectedId { + print("Item loaded is: ", file.url) + } return file.id == workspace.selectedId }) { if let codeFile = workspace.openedCodeFiles[item] { diff --git a/CodeEdit/Documents/WorkspaceDocument.swift b/CodeEdit/Documents/WorkspaceDocument.swift index 40eddfc6a4..8e88761112 100644 --- a/CodeEdit/Documents/WorkspaceDocument.swift +++ b/CodeEdit/Documents/WorkspaceDocument.swift @@ -86,7 +86,7 @@ class WorkspaceDocument: NSDocument, ObservableObject, NSToolbarDelegate { openedCodeFiles[item] = codeFile } selectedId = item.id - + Swift.print("Opening file for item: ", item.url) self.windowControllers.first?.window?.subtitle = item.url.lastPathComponent } catch let err { Swift.print(err) diff --git a/CodeEdit/Quick Open/QuickOpenPreviewView.swift b/CodeEdit/Quick Open/QuickOpenPreviewView.swift index 79cba2f5a3..aff4de8835 100644 --- a/CodeEdit/Quick Open/QuickOpenPreviewView.swift +++ b/CodeEdit/Quick Open/QuickOpenPreviewView.swift @@ -8,7 +8,6 @@ import SwiftUI import WorkspaceClient import CodeFile -import CodeEditor struct QuickOpenPreviewView: View { var item: WorkspaceClient.FileItem @@ -18,8 +17,12 @@ struct QuickOpenPreviewView: View { var body: some View { VStack { - if loaded { - ThemedCodeView($content, language: .init(url: item.url), editable: false) + if let codeFile = try? CodeFileDocument( + for: item.url, + withContentsOf: item.url, + ofType: "public.source-code" + ), loaded { + CodeFileView(codeFile: codeFile, editable: false) } else if let error = error { Text(error) } else { diff --git a/CodeEdit/Settings/GeneralSettingsView.swift b/CodeEdit/Settings/GeneralSettingsView.swift index 91d07a9f17..8c2c193d40 100644 --- a/CodeEdit/Settings/GeneralSettingsView.swift +++ b/CodeEdit/Settings/GeneralSettingsView.swift @@ -7,15 +7,15 @@ import SwiftUI import CodeFile -import CodeEditor // MARK: - View struct GeneralSettingsView: View { @AppStorage(Appearances.storageKey) var appearance: Appearances = .default @AppStorage(ReopenBehavior.storageKey) var reopenBehavior: ReopenBehavior = .default - @AppStorage(FileIconStyle.storageKey) var fileIconStyle: FileIconStyle = .default - @AppStorage(CodeEditorTheme.storageKey) var editorTheme: CodeEditor.ThemeName = .atelierSavannaAuto + @AppStorage(FileIconStyle.storageKey) var fileIconStyle: FileIconStyle = .default + @AppStorage(CodeFileView.Theme.storageKey) var editorTheme: CodeFileView.Theme = .atelierSavannaAuto + var body: some View { Form { Picker("Appearance".localized(), selection: $appearance) { @@ -50,18 +50,18 @@ struct GeneralSettingsView: View { Picker("Editor Theme".localized(), selection: $editorTheme) { Text("Atelier Savanna (Auto)") - .tag(CodeEditor.ThemeName.atelierSavannaAuto) + .tag(CodeFileView.Theme.atelierSavannaAuto) Text("Atelier Savanna Dark") - .tag(CodeEditor.ThemeName.atelierSavannaDark) + .tag(CodeFileView.Theme.atelierSavannaDark) Text("Atelier Savanna Light") - .tag(CodeEditor.ThemeName.atelierSavannaLight) + .tag(CodeFileView.Theme.atelierSavannaLight) // TODO: Pojoaque does not seem to work (does not change from previous selection) // Text("Pojoaque") // .tag(CodeEditor.ThemeName.pojoaque) Text("Agate") - .tag(CodeEditor.ThemeName.agate) + .tag(CodeFileView.Theme.agate) Text("Ocean") - .tag(CodeEditor.ThemeName.ocean) + .tag(CodeFileView.Theme.ocean) } } .padding() diff --git a/CodeEdit/SideBar/SideBarItem.swift b/CodeEdit/SideBar/SideBarItem.swift index 389e526bfa..8751bbf143 100644 --- a/CodeEdit/SideBar/SideBarItem.swift +++ b/CodeEdit/SideBar/SideBarItem.swift @@ -29,9 +29,11 @@ struct SideBarItem: View { func sidebarFileItem(_ item: WorkspaceClient.FileItem) -> some View { NavigationLink { - WorkspaceCodeFileView(windowController: windowController, - workspace: workspace) - .onAppear { workspace.openFile(item: item) } + WorkspaceCodeFileView( + windowController: windowController, + workspace: workspace + ) + .onAppear { workspace.openFile(item: item) } } label: { Label(item.url.lastPathComponent, systemImage: item.systemImage) .accentColor(iconStyle == .color ? item.iconColor : .secondary) diff --git a/CodeEdit/WorkspaceView.swift b/CodeEdit/WorkspaceView.swift index 7c82b36e25..a7c13171d1 100644 --- a/CodeEdit/WorkspaceView.swift +++ b/CodeEdit/WorkspaceView.swift @@ -30,8 +30,10 @@ struct WorkspaceView: View { SideBar(workspace: workspace, windowController: windowController) .frame(minWidth: 250) - WorkspaceCodeFileView(windowController: windowController, - workspace: workspace) + WorkspaceCodeFileView( + windowController: windowController, + workspace: workspace + ) } else { EmptyView() } diff --git a/CodeEditModules/.swiftpm/xcode/xcshareddata/xcschemes/CodeFile.xcscheme b/CodeEditModules/.swiftpm/xcode/xcshareddata/xcschemes/CodeFile.xcscheme new file mode 100644 index 0000000000..e89a7d7653 --- /dev/null +++ b/CodeEditModules/.swiftpm/xcode/xcshareddata/xcschemes/CodeFile.xcscheme @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/CodeEditModules/Modules/CodeFile/src/CodeEditor+AppStorage.swift b/CodeEditModules/Modules/CodeFile/src/CodeEditor+AppStorage.swift deleted file mode 100644 index 66a0496542..0000000000 --- a/CodeEditModules/Modules/CodeFile/src/CodeEditor+AppStorage.swift +++ /dev/null @@ -1,17 +0,0 @@ -// -// File.swift -// -// -// Created by Lukas Pistrol on 18.03.22. -// - -import Foundation -import CodeEditor - -public extension CodeEditor.ThemeName { - static var atelierSavannaAuto = CodeEditor.ThemeName(rawValue: "atelier-savanna-auto") -} - -public enum CodeEditorTheme { - public static let storageKey = "codeEditorTheme" -} diff --git a/CodeEditModules/Modules/CodeFile/src/CodeEditor.swift b/CodeEditModules/Modules/CodeFile/src/CodeEditor.swift new file mode 100644 index 0000000000..ac3b13f3ee --- /dev/null +++ b/CodeEditModules/Modules/CodeFile/src/CodeEditor.swift @@ -0,0 +1,148 @@ +// +// CodeEditor.swift +// CodeEdit +// +// Created by Marco Carnevali on 19/03/22. +// + +import Foundation +import AppKit +import SwiftUI +import Highlightr +import Combine + +struct CodeEditor: NSViewRepresentable { + @State private var isCurrentlyUpdatingView: ReferenceTypeBool = .init(value: false) + private var content: Binding + private let language: Language? + private let theme: Binding + private let highlightr = Highlightr() + + init( + content: Binding, + language: Language?, + theme: Binding + ) { + self.content = content + self.language = language + self.theme = theme + highlightr?.setTheme(to: theme.wrappedValue.rawValue) + } + + func makeNSView(context: Context) -> NSScrollView { + let scrollView = NSScrollView() + let textView = CodeEditorTextView( + textContainer: buildTextStorage( + language: language, + scrollView: scrollView + ) + ) + textView.autoresizingMask = .width + textView.maxSize = NSSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude) + textView.minSize = NSSize(width: 0, height: scrollView.contentSize.height) + textView.delegate = context.coordinator + + scrollView.drawsBackground = true + scrollView.borderType = .noBorder + scrollView.hasVerticalScroller = true + scrollView.hasHorizontalRuler = false + scrollView.autoresizingMask = [.width, .height] + + scrollView.documentView = textView + scrollView.verticalRulerView = LineGutter( + scrollView: scrollView, + width: 60, + font: .systemFont(ofSize: 10), + textColor: .labelColor, + backgroundColor: .windowBackgroundColor + ) + scrollView.rulersVisible = true + + updateTextView(textView) + return scrollView + } + + func updateNSView(_ scrollView: NSScrollView, context: Context) { + if let textView = scrollView.documentView as? CodeEditorTextView { + updateTextView(textView) + } + } + + final class Coordinator: NSObject, NSTextViewDelegate { + private var content: Binding + init(content: Binding) { + self.content = content + } + + func textDidChange(_ notification: Notification) { + guard let textView = notification.object as? NSTextView else { + return + } + content.wrappedValue = textView.string + } + + } + + func makeCoordinator() -> Coordinator { + Coordinator(content: content) + } + + private func updateTextView(_ textView: NSTextView) { + guard !isCurrentlyUpdatingView.value else { + return + } + + isCurrentlyUpdatingView.value = true + + defer { + isCurrentlyUpdatingView.value = false + } + + highlightr?.setTheme(to: theme.wrappedValue.rawValue) + + if content.wrappedValue != textView.string { + if let textStorage = textView.textStorage as? CodeAttributedString { + textStorage.language = language?.rawValue + textStorage.replaceCharacters( + in: NSRange(location: 0, length: textStorage.length), + with: content.wrappedValue + ) + } else { + textView.string = content.wrappedValue + } + } + } + + private func buildTextStorage(language: Language?, scrollView: NSScrollView) -> NSTextContainer { + // highlightr wrapper that enables real-time highlighting + let textStorage: CodeAttributedString + if let highlightr = highlightr { + textStorage = CodeAttributedString(highlightr: highlightr) + } else { + textStorage = CodeAttributedString() + } + textStorage.language = language?.rawValue + let layoutManager = NSLayoutManager() + textStorage.addLayoutManager(layoutManager) + let textContainer = NSTextContainer(containerSize: scrollView.frame.size) + textContainer.widthTracksTextView = true + textContainer.containerSize = NSSize( + width: scrollView.contentSize.width, + height: .greatestFiniteMagnitude + ) + layoutManager.addTextContainer(textContainer) + return textContainer + } +} + +extension CodeEditor { + // A wrapper around a `Bool` that enables updating + // the wrapped value during `View` renders. + private class ReferenceTypeBool { + var value: Bool + + init(value: Bool) { + self.value = value + } + } +} diff --git a/CodeEditModules/Modules/CodeFile/src/CodeFile.swift b/CodeEditModules/Modules/CodeFile/src/CodeFile.swift index 9fb606b3c5..6b4a98c8bf 100644 --- a/CodeEditModules/Modules/CodeFile/src/CodeFile.swift +++ b/CodeEditModules/Modules/CodeFile/src/CodeFile.swift @@ -6,7 +6,6 @@ // import AppKit -import CodeEditor import Foundation import SwiftUI @@ -25,14 +24,6 @@ public final class CodeFileDocument: NSDocument, ObservableObject { return true } - public func fileLanguage() -> CodeEditor.Language { - if let fileURL = fileURL { - return .init(url: fileURL) - } else { - return .markdown - } - } - override public func makeWindowControllers() { // Returns the Storyboard that contains your Document window. let contentView = CodeFileView(codeFile: self) @@ -57,15 +48,3 @@ public final class CodeFileDocument: NSDocument, ObservableObject { self.content = content } } - -public extension CodeEditor.Language { - init(url: URL) { - var value = url.pathExtension - switch value { - case "js": value = "javascript" - case "sh": value = "shell" - default: break - } - self.init(rawValue: value) - } -} diff --git a/CodeEditModules/Modules/CodeFile/src/CodeFileView.swift b/CodeEditModules/Modules/CodeFile/src/CodeFileView.swift index 0f03bc4c98..2827491cde 100644 --- a/CodeEditModules/Modules/CodeFile/src/CodeFileView.swift +++ b/CodeEditModules/Modules/CodeFile/src/CodeFileView.swift @@ -5,21 +5,43 @@ // Created by Marco Carnevali on 17/03/22. // -import CodeEditor +import Highlightr import Foundation import SwiftUI /// CodeFileView is just a wrapper of the `CodeEditor` dependency public struct CodeFileView: View { - @ObservedObject public var codeFile: CodeFileDocument + @ObservedObject private var codeFile: CodeFileDocument + @AppStorage(Theme.storageKey) var theme: Theme = .atelierSavannaAuto @Environment(\.colorScheme) private var colorScheme - @AppStorage(CodeEditorTheme.storageKey) var theme: CodeEditor.ThemeName = .atelierSavannaAuto + private let editable: Bool - public init(codeFile: CodeFileDocument) { + public init(codeFile: CodeFileDocument, editable: Bool = true) { self.codeFile = codeFile + self.editable = editable } public var body: some View { - ThemedCodeView($codeFile.content, language: codeFile.fileLanguage()) + CodeEditor( + content: $codeFile.content, + language: getLanguage(), + theme: $theme + ) + .disabled(!editable) + } + + private func getLanguage() -> CodeEditor.Language? { + if let url = codeFile.fileURL { + return .init(url: url) + } else { + return .plaintext + } + } + + private func getTheme() -> Theme { + if theme == .atelierSavannaAuto { + return colorScheme == .light ? .atelierSavannaLight : .atelierSavannaDark + } + return theme } } diff --git a/CodeEditModules/Modules/CodeFile/src/LineGutter/LineGutter.swift b/CodeEditModules/Modules/CodeFile/src/LineGutter/LineGutter.swift new file mode 100644 index 0000000000..f4dc13b25c --- /dev/null +++ b/CodeEditModules/Modules/CodeFile/src/LineGutter/LineGutter.swift @@ -0,0 +1,232 @@ +// +// LineGutter.swift +// CodeEdit +// +// Created by Marco Carnevali on 19/03/22. +// + +import Cocoa + +class LineGutter: NSRulerView { + private var _lineIndices: [Int]? { + didSet { + DispatchQueue.main.async { + let newThickness = self.calculateRuleThickness() + if fabs(self.ruleThickness - newThickness) > 1 { + self.ruleThickness = CGFloat(ceil(newThickness)) + self.needsDisplay = true + } + } + } + } + private var lineIndices: [Int]? { + get { + if _lineIndices == nil { + calculateLines() + } + return _lineIndices + } + } + + private var textView: NSTextView? { clientView as? NSTextView } + override var isOpaque: Bool { false } + override var clientView: NSView? { + willSet { + let center = NotificationCenter.default + if let oldView = clientView as? NSTextView, oldView != newValue { + center.removeObserver(self, name: NSText.didEndEditingNotification, object: oldView.textStorage) + center.removeObserver(self, name: NSView.boundsDidChangeNotification, object: scrollView?.contentView) + } + center.addObserver( + self, + selector: #selector(textDidChange(_:)), + name: NSText.didChangeNotification, + object: newValue + ) + scrollView?.contentView.postsBoundsChangedNotifications = true + center.addObserver( + self, + selector: #selector(boundsDidChange(_:)), + name: NSView.boundsDidChangeNotification, + object: scrollView?.contentView + ) + invalidateLineIndices() + } + } + + private let rulerMargin: CGFloat = 5 + private let rulerWidth: CGFloat + private let font: NSFont + public var textColor: NSColor + public var backgroundColor: NSColor + + init( + scrollView: NSScrollView, + width: CGFloat, + font: NSFont, + textColor: NSColor, + backgroundColor: NSColor + ) { + rulerWidth = width + self.font = font + self.textColor = textColor + self.backgroundColor = backgroundColor + super.init(scrollView: scrollView, orientation: .verticalRuler) + clientView = scrollView.documentView + ruleThickness = width + needsDisplay = true + } + + required init(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + NotificationCenter.default.removeObserver(self) + } + + @objc + func boundsDidChange(_ notification: Notification) { + needsDisplay = true + } + + @objc + func textDidChange(_ notification: Notification) { + invalidateLineIndices() + needsDisplay = true + } + + override func draw(_ dirtyRect: NSRect) { + drawHashMarksAndLabels(in: dirtyRect) + } + + func invalidateLineIndices() { + _lineIndices = nil + } + + func lineNumberForCharacterIndex(index: Int) -> Int { + guard let lineIndices = lineIndices else { + return 0 + } + + var left = 0, right = lineIndices.count + while right - left > 1 { + let mid = (left + right) / 2 + let lineIndex = lineIndices[mid] + if index < lineIndex { + right = mid + } else if index > lineIndex { + left = mid + } else { + return mid + 1 + } + } + return left + 1 + } + + func calculateRuleThickness() -> CGFloat { + let string = String(lineIndices?.last ?? 0) as NSString + let digitWidth = string.size(withAttributes: textAttributes()).width * 2 + rulerMargin + return max(digitWidth, rulerWidth) + } + + func calculateLines() { + var lineIndices = [Int]() + guard let textView = textView else { + return + } + let text = textView.string as NSString + let textLength = text.length + var totalLines = 0 + var charIndex = 0 + repeat { + lineIndices.append(charIndex) + charIndex = text.lineRange(for: NSRange(location: charIndex, length: 0)).upperBound + totalLines += 1 + } while charIndex < textLength + + // Check for trailing return + var lineEndIndex = 0, contentEndIndex = 0 + let lastObject = lineIndices[lineIndices.count - 1] + text.getLineStart( + nil, + end: &lineEndIndex, + contentsEnd: &contentEndIndex, + for: NSRange(location: lastObject, length: 0) + ) + if contentEndIndex < lineEndIndex { + lineIndices.append(lineEndIndex) + } + _lineIndices = lineIndices + } + + // swiftlint:disable function_body_length + override func drawHashMarksAndLabels(in rect: NSRect) { + guard let textView = textView, + let clientView = clientView, + let layoutManager = textView.layoutManager, + let container = textView.textContainer, + let scrollView = scrollView, + let lineIndices = lineIndices + else { return } + + // Make background + let docRect = convert(clientView.bounds, from: clientView) + let yOrigin = docRect.origin.y + let height = docRect.size.height + let width = bounds.size.width + backgroundColor.set() + + NSRect(x: 0, y: yOrigin, width: width, height: height).fill() + + // Code folding area + NSRect(x: width - 8, y: yOrigin, width: 8, height: height).fill() + + let nullRange = NSRange(location: NSNotFound, length: 0) + var lineRectCount = 0 + + let textVisibleRect = scrollView.contentView.bounds + let rulerBounds = bounds + let textInset = textView.textContainerInset.height + + let glyphRange = layoutManager.glyphRange(forBoundingRect: textVisibleRect, in: container) + let charRange = layoutManager.characterRange(forGlyphRange: glyphRange, actualGlyphRange: nil) + + let startChange = lineNumberForCharacterIndex(index: charRange.location) + let endChange = lineNumberForCharacterIndex(index: charRange.upperBound) + for lineNumber in startChange...endChange { + let charIndex = lineIndices[lineNumber - 1] + if let lineRectsForRange = layoutManager.rectArray( + forCharacterRange: NSRange(location: charIndex, length: 0), + withinSelectedCharacterRange: nullRange, + in: container, + rectCount: &lineRectCount + ), lineRectCount > 0 { + let ypos = textInset + lineRectsForRange[0].minY - textVisibleRect.minY + let labelText = NSString(format: "%ld", lineNumber) + let labelSize = labelText.size(withAttributes: textAttributes()) + + let lineNumberRect = NSRect( + x: rulerBounds.width - labelSize.width - rulerMargin, + y: ypos + (lineRectsForRange[0].height - labelSize.height) / 2, + width: rulerBounds.width - rulerMargin * 2, + height: lineRectsForRange[0].height + ) + + labelText.draw(in: lineNumberRect, withAttributes: textAttributes()) + } + + // we are past the visible range so exit for + if charIndex > charRange.upperBound { + break + } + } + } + + func textAttributes() -> [NSAttributedString.Key: AnyObject] { + return [ + NSAttributedString.Key.font: self.font, + NSAttributedString.Key.foregroundColor: self.textColor + ] + } +} diff --git a/CodeEditModules/Modules/CodeFile/src/Model/Language.swift b/CodeEditModules/Modules/CodeFile/src/Model/Language.swift new file mode 100644 index 0000000000..e40839d84f --- /dev/null +++ b/CodeEditModules/Modules/CodeFile/src/Model/Language.swift @@ -0,0 +1,213 @@ +// +// File.swift +// +// +// Created by Marco Carnevali on 22/03/22. +// +import Foundation.NSURL +// swiftlint:disable identifier_name +extension CodeEditor { + enum Language: String { + case abnf + case accesslog + case actionscript + case ada + case angelscript + case apache + case applescript + case arcade + case cpp + case arduino + case armasm + case xml + case asciidoc + case aspectj + case autohotkey + case autoit + case avrasm + case awk + case axapta + case bash + case basic + case bnf + case brainfuck + case cal + case capnproto + case ceylon + case clean + case clojure + case cmake + case coffeescript + case coq + case cos + case crmsh + case crystal + case cs + case csp + case css + case d + case markdown + case dart + case delphi + case diff + case django + case dns + case dockerfile + case dos + case dsconfig + case dts + case dust + case ebnf + case elixir + case elm + case ruby + case erb + case erlang + case excel + case fix + case flix + case fortran + case fsharp + case gams + case gauss + case gcode + case gherkin + case glsl + case gml + case go + case golo + case gradle + case groovy + case haml + case handlebars + case haskell + case haxe + case hsp + case htmlbars + case http + case hy + case inform7 + case ini + case irpf90 + case isbl + case java + case javascript + case json + case julia + case kotlin + case lasso + case ldif + case leaf + case less + case lisp + case livecodeserver + case livescript + case llvm + case lsl + case lua + case makefile + case mathematica + case matlab + case maxima + case mel + case mercury + case mipsasm + case mizar + case perl + case mojolicious + case monkey + case moonscript + case n1ql + case nginx + case nimrod + case nix + case nsis + case objectivec + case ocaml + case openscad + case oxygene + case parser3 + case pf + case pgsql + case php + case plaintext + case pony + case powershell + case processing + case profile + case prolog + case properties + case protobuf + case puppet + case purebasic + case python + case q + case qml + case r + case reasonml + case rib + case roboconf + case routeros + case rsl + case ruleslanguage + case rust + case sas + case scala + case scheme + case scilab + case scss + case shell + case smali + case smalltalk + case sml + case sqf + case sql + case stan + case stata + case step21 + case stylus + case subunit + case swift + case taggerscript + case yaml + case tap + case tcl + case tex + case thrift + case tp + case twig + case typescript + case vala + case vbnet + case vbscript + case verilog + case vhdl + case vim + case x86asm + case xl + case xquery + case zephir + case clojureRepl = "clojure-repl" + case vbscriptHtml = "vbscript-html" + case juliaRepl = "julia-repl" + case jbossCli = "jboss-cli" + case erlangRepl = "erlang-repl" + case oneC = "1c" + + init?(url: URL) { + let fileExtension = url.pathExtension.lowercased() + switch fileExtension { + case "js": self = .javascript + case "tf": self = .typescript + case "md": self = .markdown + case "py": self = .python + case "bat": self = .dos + case "cxx", "h", "hpp", "hxx": self = .cpp + case "scpt", "scptd", "applescript": self = .applescript + case "pl": self = .perl + case "txt": self = .plaintext + default: self.init(rawValue: fileExtension) + } + } + } +} diff --git a/CodeEditModules/Modules/CodeFile/src/Model/Theme.swift b/CodeEditModules/Modules/CodeFile/src/Model/Theme.swift new file mode 100644 index 0000000000..a54c94620a --- /dev/null +++ b/CodeEditModules/Modules/CodeFile/src/Model/Theme.swift @@ -0,0 +1,18 @@ +// +// Theme.swift +// CodeEdit +// +// Created by Marco Carnevali on 19/03/22. +// + +extension CodeFileView { + public enum Theme: String { + public static let storageKey = "codeEditorTheme" + + case agate + case ocean + case atelierSavannaDark = "atelier-savanna-dark" + case atelierSavannaLight = "atelier-savanna-light" + case atelierSavannaAuto = "atelier-savanna-auto" + } +} diff --git a/CodeEditModules/Modules/CodeFile/src/TextView/CodeEditorTextView.swift b/CodeEditModules/Modules/CodeFile/src/TextView/CodeEditorTextView.swift new file mode 100644 index 0000000000..a9d3ffe37c --- /dev/null +++ b/CodeEditModules/Modules/CodeFile/src/TextView/CodeEditorTextView.swift @@ -0,0 +1,88 @@ +// +// CodeEditorTextView.swift +// CodeEdit +// +// Created by Marco Carnevali on 19/03/22. +// + +import Cocoa + +class CodeEditorTextView: NSTextView { + private let tabNumber = 4 + + init( + textContainer container: NSTextContainer? + ) { + super.init(frame: .zero, textContainer: container) + + drawsBackground = true + isEditable = true + isHorizontallyResizable = false + isVerticallyResizable = true + allowsUndo = true + isRichText = false + isGrammarCheckingEnabled = false + isContinuousSpellCheckingEnabled = false + isAutomaticQuoteSubstitutionEnabled = false + isAutomaticDashSubstitutionEnabled = false + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + } + + private var swiftSelectedRange : Range { + let string = self.string + guard !string.isEmpty else { return string.startIndex.., language: CodeEditor.Language, editable: Bool = true) { - self._content = content - self.language = language - self.editable = editable - } - - public var body: some View { - CodeEditor( - source: $content, - language: language, - theme: getTheme(), - flags: editable ? .defaultEditorFlags : .defaultViewerFlags, - indentStyle: .system - ) - } - - private func getTheme() -> CodeEditor.ThemeName { - if theme == .atelierSavannaAuto { - return colorScheme == .light ? .atelierSavannaLight : .atelierSavannaDark - } - return theme - } -} - -struct SwiftUIView_Previews: PreviewProvider { - static var previews: some View { - ThemedCodeView(.constant("## Example"), language: .markdown) - } -} diff --git a/CodeEditModules/Package.swift b/CodeEditModules/Package.swift index 3a638ab78f..7a62556fd3 100644 --- a/CodeEditModules/Package.swift +++ b/CodeEditModules/Package.swift @@ -32,9 +32,9 @@ let package = Package( ], dependencies: [ .package( - name: "CodeEditor", - url: "https://github.com/ZeeZide/CodeEditor.git", - from: "1.2.0" + name: "Highlightr", + url: "https://github.com/raspu/Highlightr.git", + from: "2.1.2" ), .package( name: "SnapshotTesting", @@ -57,7 +57,7 @@ let package = Package( .target( name: "CodeFile", dependencies: [ - "CodeEditor" + "Highlightr" ], path: "Modules/CodeFile/src" ),