diff --git a/.swiftlint.yml b/.swiftlint.yml
index bce3d69bc..dbb608ab8 100644
--- a/.swiftlint.yml
+++ b/.swiftlint.yml
@@ -1,2 +1,3 @@
disabled_rules:
- - todo
\ No newline at end of file
+ - todo
+ - trailing_comma
diff --git a/CodeEdit.xcworkspace/xcshareddata/swiftpm/Package.resolved b/CodeEdit.xcworkspace/xcshareddata/swiftpm/Package.resolved
index 7d8fafa3f..ee206aceb 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/ContentView.swift b/CodeEdit/ContentView.swift
deleted file mode 100644
index a6cfe131d..000000000
--- a/CodeEdit/ContentView.swift
+++ /dev/null
@@ -1,216 +0,0 @@
-//
-// ContentView.swift
-// CodeEdit
-//
-// Created by Austin Condiff on 3/10/22.
-//
-
-import SwiftUI
-import WorkspaceClient
-import CodeEditorView
-
-struct WorkspaceView: View {
- @State private var directoryURL: URL?
- @State private var workspaceClient: WorkspaceClient?
- // TODO: Create a ViewModel to hold selectedId, openFileItems,
- // ... to pass it to subviews as an EnvironmentObject (less boilerplate parameters)
- @State private var selectedId: WorkspaceClient.FileId?
- @State private var openFileItems: [WorkspaceClient.FileItem] = []
- @State private var urlInit = false
- @State private var showingAlert = false
- @State private var alertTitle = ""
- @State private var alertMsg = ""
-
- var tabBarHeight = 28.0
- private var path: String = ""
-
- func closeFileTab(item: WorkspaceClient.FileItem) {
- guard let idx = openFileItems.firstIndex(of: item) else { return }
- let closedFileItem = openFileItems.remove(at: idx)
- guard closedFileItem.id == selectedId else { return }
-
- if openFileItems.isEmpty {
- selectedId = nil
- } else if idx == 0 {
- selectedId = openFileItems.first?.id
- } else {
- selectedId = openFileItems[idx - 1].id
- }
- }
-
- var body: some View {
- NavigationView {
- if let workspaceClient = workspaceClient, let directoryURL = directoryURL {
- sidebar(workspaceClient: workspaceClient, directoryURL: directoryURL)
- .frame(minWidth: 250)
- .toolbar {
- ToolbarItem(placement: .primaryAction) {
- Button(action: toggleSidebar) {
- Image(systemName: "sidebar.leading").imageScale(.large)
- }
- .help("Show/Hide Sidebar")
- }
- }
- if openFileItems.isEmpty {
- Text("Open file from sidebar")
- } else {
- ZStack {
- if let selectedId = selectedId {
- if let selectedItem = try? workspaceClient.getFileItem(selectedId) {
- CodeEditorView(fileURL: selectedItem.url)
- .padding(.top, 31.0)
- }
- }
-
- VStack {
- tabBar
- .frame(maxHeight: tabBarHeight)
- .background(Material.regular)
-
- Spacer()
- }
- }
- }
- } else {
- EmptyView()
- }
- }
- .toolbar {
- ToolbarItem(placement: .navigation) {
- Button(
- action: {},
- label: { Image(systemName: "chevron.left").imageScale(.large) }
- )
- .help("Back")
- }
- ToolbarItem(placement: .navigation) {
- Button(
- action: {},
- label: { Image(systemName: "chevron.right").imageScale(.large) }
- )
- .disabled(true)
- .help("Forward")
- }
- }
- .frame(minWidth: 800, minHeight: 600)
- .onOpenURL { url in
- urlInit = true
- do {
- self.workspaceClient = try .default(
- fileManager: .default,
- folderURL: url,
- ignoredFilesAndFolders: ignoredFilesAndDirectory
- )
- self.directoryURL = url
- } catch {
- self.alertTitle = "Unable to Open Workspace"
- self.alertMsg = error.localizedDescription
- self.showingAlert = true
- print(error.localizedDescription)
- }
- }
- .alert(alertTitle, isPresented: $showingAlert, actions: {
- Button(
- action: { showingAlert = false },
- label: { Text("OK") }
- )
- }, message: { Text(alertMsg) })
- }
-
- var tabBar: some View {
- VStack(spacing: 0.0) {
- ScrollView(.horizontal, showsIndicators: false) {
- ScrollViewReader { value in
- HStack(alignment: .center, spacing: 0.0) {
- ForEach(openFileItems, id: \.id) { item in
- let isActive = selectedId == item.id
-
- Button(
- action: { selectedId = item.id },
- label: {
- HStack(spacing: 0.0) {
- FileTabRow(fileItem: item, isSelected: isActive) {
- withAnimation {
- closeFileTab(item: item)
- }
- }
-
- Divider()
- }
- )
- .frame(height: tabBarHeight)
- .foregroundColor(isActive ? .primary : .gray)
- .background(isActive ? Material.bar : Material.regular)
- .animation(.easeOut(duration: 0.2), value: openFileItems)
- }
- .buttonStyle(.plain)
- .id(item.id)
- }
- }
- .onChange(of: selectedId) { newValue in
- withAnimation {
- value.scrollTo(newValue)
- }
- }
- }
- }
-
- Divider()
- .foregroundColor(.gray)
- .frame(height: 1.0)
- }
- }
-
- func sidebar(
- workspaceClient: WorkspaceClient,
- directoryURL: URL
- ) -> some View {
- List {
- Section(header: Text(directoryURL.lastPathComponent)) {
- OutlineGroup(workspaceClient.getFiles(), children: \.children) { item in
- if item.children == nil {
- Button(
- action: {
- withAnimation {
- if !openFileItems.contains(item) {
- openFileItems.append(item)
- }
- }
- selectedId = item.id
- },
- label: {
- Label(
- item.url.lastPathComponent,
- systemImage: item.systemImage
- )
- .accentColor(.secondary)
- .font(.callout)
- }
- )
- .buttonStyle(.plain)
- } else {
- Label(item.url.lastPathComponent, systemImage: item.systemImage)
- .accentColor(.secondary)
- .font(.callout)
- }
- }
- }
- }
- }
-
- private func toggleSidebar() {
-#if os(iOS)
-#else
- NSApp.keyWindow?.firstResponder?.tryToPerform(
- #selector(NSSplitViewController.toggleSidebar(_:)),
- with: nil
- )
-#endif
- }
- }
-
- struct ContentView_Previews: PreviewProvider {
- static var previews: some View {
- WorkspaceView()
- }
- }
diff --git a/CodeEdit/Documents/WorkspaceCodeFileView.swift b/CodeEdit/Documents/WorkspaceCodeFileView.swift
index 162fb47e7..254b8af85 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 309c97ef1..d91f8d38c 100644
--- a/CodeEdit/Documents/WorkspaceDocument.swift
+++ b/CodeEdit/Documents/WorkspaceDocument.swift
@@ -87,7 +87,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 79cba2f5a..aff4de883 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 91d07a9f1..8c2c193d4 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/WorkspaceView.swift b/CodeEdit/WorkspaceView.swift
index c6b51d21d..caa751d35 100644
--- a/CodeEdit/WorkspaceView.swift
+++ b/CodeEdit/WorkspaceView.swift
@@ -31,11 +31,17 @@ struct WorkspaceView: View {
NavigatorSidebar(workspace: workspace, windowController: windowController)
.frame(minWidth: 250)
HSplitView {
- WorkspaceCodeFileView(windowController: windowController,
- workspace: workspace)
- .frame(maxWidth: .infinity, maxHeight: .infinity)
- InspectorSidebar(workspace: workspace, windowController: windowController)
- .frame(minWidth: 250, maxWidth: .infinity, maxHeight: .infinity)
+ WorkspaceCodeFileView(
+ windowController: windowController,
+ workspace: workspace
+ )
+ .frame(maxWidth: .infinity, maxHeight: .infinity)
+
+ InspectorSidebar(
+ workspace: workspace,
+ windowController: windowController
+ )
+ .frame(minWidth: 250, maxWidth: .infinity, maxHeight: .infinity)
}
} else {
EmptyView()
@@ -56,7 +62,7 @@ struct WorkspaceView: View {
}
}
-struct ContentView_Previews: PreviewProvider {
+struct WorkspaceView_Previews: PreviewProvider {
static var previews: some View {
WorkspaceView(windowController: NSWindowController(), workspace: .init())
}
diff --git a/CodeEditModules/.swiftpm/xcode/xcshareddata/xcschemes/CodeFile.xcscheme b/CodeEditModules/.swiftpm/xcode/xcshareddata/xcschemes/CodeFile.xcscheme
new file mode 100644
index 000000000..e89a7d765
--- /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 66a049654..000000000
--- 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 000000000..2b3a998f2
--- /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: 45,
+ font: .monospacedDigitSystemFont(ofSize: 11, weight: .regular),
+ textColor: .secondaryLabelColor,
+ backgroundColor: .clear
+ )
+ 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 9fb606b3c..6b4a98c8b 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 0f03bc4c9..2827491cd 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 000000000..0dbd7a46c
--- /dev/null
+++ b/CodeEditModules/Modules/CodeFile/src/LineGutter/LineGutter.swift
@@ -0,0 +1,242 @@
+//
+// 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]? {
+ // swiftlint:disable:next implicit_getter
+ 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 rect = calculateStringSize(string)
+ return max(rect.width, 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 = calculateStringSize(labelText)
+
+ 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 calculateStringSize(_ string: NSString) -> CGRect {
+ string.boundingRect(
+ with: NSSize(width: self.frame.width, height: .greatestFiniteMagnitude),
+ options: .usesLineFragmentOrigin,
+ attributes: textAttributes(),
+ context: nil
+ )
+ }
+
+ func textAttributes() -> [NSAttributedString.Key: AnyObject] {
+ [
+ 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 000000000..e40839d84
--- /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 000000000..a54c94620
--- /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 000000000..565691b4b
--- /dev/null
+++ b/CodeEditModules/Modules/CodeFile/src/TextView/CodeEditorTextView.swift
@@ -0,0 +1,87 @@
+//
+// 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 91d100d51..b488d1d79 100644
--- a/CodeEditModules/Package.swift
+++ b/CodeEditModules/Package.swift
@@ -36,9 +36,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",
@@ -61,7 +61,7 @@ let package = Package(
.target(
name: "CodeFile",
dependencies: [
- "CodeEditor"
+ "Highlightr"
],
path: "Modules/CodeFile/src"
),