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" ),