-
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Expand file tree
/
Copy pathCodeFileDocument.swift
More file actions
207 lines (169 loc) Β· 7.16 KB
/
CodeFileDocument.swift
File metadata and controls
207 lines (169 loc) Β· 7.16 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
//
// CodeFileDocument.swift
// CodeEditModules/CodeFile
//
// Created by Rehatbir Singh on 12/03/2022.
//
import AppKit
import Foundation
import SwiftUI
import UniformTypeIdentifiers
import CodeEditSourceEditor
import CodeEditLanguages
import Combine
import OSLog
enum CodeFileError: Error {
case failedToDecode
case failedToEncode
case fileTypeError
}
@objc(CodeFileDocument)
final class CodeFileDocument: NSDocument, ObservableObject {
struct OpenOptions {
let cursorPositions: [CursorPosition]
}
static let logger = Logger(subsystem: Bundle.main.bundleIdentifier ?? "", category: "CodeFileDocument")
/// Sent when the document is opened. The document will be sent in the notification's object.
static let didOpenNotification = Notification.Name(rawValue: "CodeFileDocument.didOpen")
/// Sent when the document is closed. The document's `fileURL` will be sent in the notification's object.
static let didCloseNotification = Notification.Name(rawValue: "CodeFileDocument.didClose")
/// The text content of the document, stored as a text storage
///
/// This is intentionally not a `@Published` variable. If it were published, SwiftUI would do a string
/// compare each time the contents are updated, which could cause a hang on each keystroke if the file is large
/// enough.
///
/// To receive notifications for content updates, subscribe to one of the publishers on ``contentCoordinator``.
var content: NSTextStorage?
/// The string encoding of the original file. Used to save the file back to the encoding it was loaded from.
var sourceEncoding: FileEncoding?
/// The coordinator to use to subscribe to edit events and cursor location events.
/// See ``CodeEditSourceEditor/CombineCoordinator``.
@Published var contentCoordinator: CombineCoordinator = CombineCoordinator()
/// Set by ``LanguageServer`` when initialized.
@Published var lspCoordinator: LSPContentCoordinator?
/// Used to override detected languages.
@Published var language: CodeLanguage?
/// Document-specific overridden indent option.
@Published var indentOption: SettingsData.TextEditingSettings.IndentOption?
/// Document-specific overridden tab width.
@Published var defaultTabWidth: Int?
/// Document-specific overridden line wrap preference.
@Published var wrapLines: Bool?
/// The type of data this file document contains.
///
/// If its text content is not nil, a `text` UTType is returned.
///
/// - Note: The UTType doesn't necessarily mean the file extension, it can be the MIME
/// type or any other form of data representation.
var utType: UTType? {
if content != nil {
return .text
}
guard let fileType, let type = UTType(fileType) else {
return nil
}
return type
}
/// A stable string to use when identifying documents with language servers.
var languageServerURI: String? { fileURL?.absolutePath }
/// Specify options for opening the file such as the initial cursor positions.
/// Nulled by ``CodeFileView`` on first load.
var openOptions: OpenOptions?
private let isDocumentEditedSubject = PassthroughSubject<Bool, Never>()
/// Publisher for isDocumentEdited property
var isDocumentEditedPublisher: AnyPublisher<Bool, Never> {
isDocumentEditedSubject.eraseToAnyPublisher()
}
// MARK: - NSDocument
override static var autosavesInPlace: Bool {
Settings.shared.preferences.general.isAutoSaveOn
}
override var autosavingFileType: String? {
Settings.shared.preferences.general.isAutoSaveOn
? fileType
: nil
}
override func makeWindowControllers() {
let window = NSWindow(
contentRect: NSRect(x: 0, y: 0, width: 750, height: 800),
styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView],
backing: .buffered, defer: false
)
let windowController = NSWindowController(window: window)
if let fileURL {
windowController.shouldCascadeWindows = false
windowController.windowFrameAutosaveName = fileURL.path
}
addWindowController(windowController)
window.contentView = NSHostingView(rootView: SettingsInjector {
WindowCodeFileView(codeFile: self)
})
window.makeKeyAndOrderFront(nil)
if let fileURL, UserDefaults.standard.object(forKey: "NSWindow Frame \(fileURL.path)") == nil {
window.center()
}
}
override func data(ofType _: String) throws -> Data {
guard let sourceEncoding, let data = (content?.string as NSString?)?.data(using: sourceEncoding.nsValue) else {
Self.logger.error("Failed to encode contents to \(self.sourceEncoding.debugDescription)")
throw CodeFileError.failedToEncode
}
return data
}
/// This function is used for decoding files.
/// It should not throw error as unsupported files can still be opened by QLPreviewView.
override func read(from data: Data, ofType _: String) throws {
var nsString: NSString?
let rawEncoding = NSString.stringEncoding(
for: data,
encodingOptions: [
.allowLossyKey: false, // Fail if using lossy encoding.
.suggestedEncodingsKey: FileEncoding.allCases.map { $0.nsValue },
.useOnlySuggestedEncodingsKey: true
],
convertedString: &nsString,
usedLossyConversion: nil
)
if let validEncoding = FileEncoding(rawEncoding), let nsString {
self.sourceEncoding = validEncoding
self.content = NSTextStorage(string: nsString as String)
} else {
Self.logger.error("Failed to read file from data using encoding: \(rawEncoding)")
}
NotificationCenter.default.post(name: Self.didOpenNotification, object: self)
}
/// Triggered when change occurred
override func updateChangeCount(_ change: NSDocument.ChangeType) {
super.updateChangeCount(change)
if CodeFileDocument.autosavesInPlace {
return
}
self.isDocumentEditedSubject.send(self.isDocumentEdited)
}
/// Triggered when changes saved
override func updateChangeCount(withToken changeCountToken: Any, for saveOperation: NSDocument.SaveOperationType) {
super.updateChangeCount(withToken: changeCountToken, for: saveOperation)
if CodeFileDocument.autosavesInPlace {
return
}
self.isDocumentEditedSubject.send(self.isDocumentEdited)
}
override func close() {
super.close()
NotificationCenter.default.post(name: Self.didCloseNotification, object: fileURL)
}
func getLanguage() -> CodeLanguage {
guard let url = fileURL else {
return .default
}
return language ?? CodeLanguage.detectLanguageFrom(
url: url,
prefixBuffer: content?.string.getFirstLines(5),
suffixBuffer: content?.string.getLastLines(5)
)
}
func findWorkspace() -> WorkspaceDocument? {
fileURL?.findWorkspace()
}
}