Skip to content

Commit ec9c663

Browse files
Merge branch 'main' into lsp-provider
2 parents c2ac9ae + e17584e commit ec9c663

File tree

8 files changed

+148
-30
lines changed

8 files changed

+148
-30
lines changed

CodeEdit.xcodeproj/project.pbxproj

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -349,6 +349,8 @@
349349
6C4104E3297C87A000F472BA /* BlurButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C4104E2297C87A000F472BA /* BlurButtonStyle.swift */; };
350350
6C4104E6297C884F00F472BA /* AboutDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C4104E5297C884F00F472BA /* AboutDetailView.swift */; };
351351
6C4104E9297C970F00F472BA /* AboutDefaultView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C4104E8297C970F00F472BA /* AboutDefaultView.swift */; };
352+
6C48B5C52C0A2835001E9955 /* FileEncoding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C48B5C42C0A2835001E9955 /* FileEncoding.swift */; };
353+
6C48B5C92C0B5F7A001E9955 /* NSTextStorage+isEmpty.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C48B5C72C0B5F7A001E9955 /* NSTextStorage+isEmpty.swift */; };
352354
6C48D8F22972DAFC00D6D205 /* Env+IsFullscreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C48D8F12972DAFC00D6D205 /* Env+IsFullscreen.swift */; };
353355
6C48D8F42972DB1A00D6D205 /* Env+Window.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C48D8F32972DB1A00D6D205 /* Env+Window.swift */; };
354356
6C48D8F72972E5F300D6D205 /* WindowObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C48D8F62972E5F300D6D205 /* WindowObserver.swift */; };
@@ -945,6 +947,8 @@
945947
6C4104E2297C87A000F472BA /* BlurButtonStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlurButtonStyle.swift; sourceTree = "<group>"; };
946948
6C4104E5297C884F00F472BA /* AboutDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutDetailView.swift; sourceTree = "<group>"; };
947949
6C4104E8297C970F00F472BA /* AboutDefaultView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutDefaultView.swift; sourceTree = "<group>"; };
950+
6C48B5C42C0A2835001E9955 /* FileEncoding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileEncoding.swift; sourceTree = "<group>"; };
951+
6C48B5C72C0B5F7A001E9955 /* NSTextStorage+isEmpty.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSTextStorage+isEmpty.swift"; sourceTree = "<group>"; };
948952
6C48D8F12972DAFC00D6D205 /* Env+IsFullscreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Env+IsFullscreen.swift"; sourceTree = "<group>"; };
949953
6C48D8F32972DB1A00D6D205 /* Env+Window.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Env+Window.swift"; sourceTree = "<group>"; };
950954
6C48D8F62972E5F300D6D205 /* WindowObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowObserver.swift; sourceTree = "<group>"; };
@@ -1183,6 +1187,7 @@
11831187
5831E3CE2933F3DE00D5A6D2 /* Controllers */,
11841188
611191F82B08CC8000D4459B /* Indexer */,
11851189
58798249292E78D80085B254 /* CodeFileDocument.swift */,
1190+
6C48B5C42C0A2835001E9955 /* FileEncoding.swift */,
11861191
043C321527E3201F006AE443 /* WorkspaceDocument.swift */,
11871192
043BCF02281DA18A000AC47C /* WorkspaceDocument+SearchState.swift */,
11881193
61A53A802B4449F00093BF8A /* WorkspaceDocument+Index.swift */,
@@ -2285,6 +2290,7 @@
22852290
588847672992AAB800996D95 /* Array */,
22862291
6CBD1BC42978DE3E006639D5 /* Text */,
22872292
5831E3D02934036D00D5A6D2 /* NSTableView */,
2293+
6C48B5C82C0B5F7A001E9955 /* NSTextStorage */,
22882294
5831E3CA2933E86F00D5A6D2 /* View */,
22892295
5831E3C72933E7F700D5A6D2 /* Bundle */,
22902296
5831E3C62933E7E600D5A6D2 /* Color */,
@@ -2526,6 +2532,15 @@
25262532
path = FindNavigatorResultList;
25272533
sourceTree = "<group>";
25282534
};
2535+
6C48B5C82C0B5F7A001E9955 /* NSTextStorage */ = {
2536+
isa = PBXGroup;
2537+
children = (
2538+
6C48B5C72C0B5F7A001E9955 /* NSTextStorage+isEmpty.swift */,
2539+
);
2540+
name = NSTextStorage;
2541+
path = CodeEdit/Utils/Extensions/NSTextStorage;
2542+
sourceTree = SOURCE_ROOT;
2543+
};
25292544
6C48D8EF2972DAC300D6D205 /* Environment */ = {
25302545
isa = PBXGroup;
25312546
children = (
@@ -3595,6 +3610,7 @@
35953610
6C48D8F22972DAFC00D6D205 /* Env+IsFullscreen.swift in Sources */,
35963611
587B9E8729301D8F00AC7927 /* GitHubRepositories.swift in Sources */,
35973612
6CE6226B2A2A1C730013085C /* UtilityAreaTab.swift in Sources */,
3613+
6C48B5C52C0A2835001E9955 /* FileEncoding.swift in Sources */,
35983614
587B9DA329300ABD00AC7927 /* SettingsTextEditor.swift in Sources */,
35993615
B6F0517B29D9E46400D72287 /* SourceControlSettingsView.swift in Sources */,
36003616
6C147C4D29A32AA30089B630 /* EditorAreaView.swift in Sources */,
@@ -3857,6 +3873,7 @@
38573873
6C81916729B3E80700B75C92 /* ModifierKeysObserver.swift in Sources */,
38583874
613899BC2B6E709C00A5CAF6 /* URL+FuzzySearchable.swift in Sources */,
38593875
611192002B08CCD700D4459B /* SearchIndexer+Memory.swift in Sources */,
3876+
6C48B5C92C0B5F7A001E9955 /* NSTextStorage+isEmpty.swift in Sources */,
38603877
587B9E8129301D8F00AC7927 /* PublicKey.swift in Sources */,
38613878
611191FE2B08CCD200D4459B /* SearchIndexer+File.swift in Sources */,
38623879
77A01E302BB4270F00F0EA38 /* ProjectCEWorkspaceSettingsView.swift in Sources */,
@@ -5071,8 +5088,8 @@
50715088
isa = XCRemoteSwiftPackageReference;
50725089
repositoryURL = "https://github.com/CodeEditApp/CodeEditSourceEditor";
50735090
requirement = {
5074-
kind = exactVersion;
5075-
version = 0.7.2;
5091+
kind = upToNextMajorVersion;
5092+
minimumVersion = 0.7.3;
50765093
};
50775094
};
50785095
6CDEFC9429E22C2700B7C684 /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */ = {

CodeEdit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,17 +24,17 @@
2424
"kind" : "remoteSourceControl",
2525
"location" : "https://github.com/CodeEditApp/CodeEditLanguages.git",
2626
"state" : {
27-
"revision" : "620b463c88894741e20d4711c9435b33547de5d2",
28-
"version" : "0.1.18"
27+
"revision" : "5b27f139269e1ea49ceae5e56dca44a3ccad50a1",
28+
"version" : "0.1.19"
2929
}
3030
},
3131
{
3232
"identity" : "codeeditsourceeditor",
3333
"kind" : "remoteSourceControl",
3434
"location" : "https://github.com/CodeEditApp/CodeEditSourceEditor.git",
3535
"state" : {
36-
"revision" : "7360f00bf7ec8e93b4833357bd254bef7e5c943d",
37-
"version" : "0.7.2"
36+
"revision" : "cf85789d527d569e94edfd674c5ac8071b244dd9",
37+
"version" : "0.7.3"
3838
}
3939
},
4040
{

CodeEdit/Features/Documents/CodeFileDocument.swift

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,21 @@ final class CodeFileDocument: NSDocument, ObservableObject {
2626
let cursorPositions: [CursorPosition]
2727
}
2828

29-
@Published var content = ""
29+
/// The text content of the document, stored as a text storage
30+
///
31+
/// This is intentionally not a `@Published` variable. If it were published, SwiftUI would do a string
32+
/// compare each time the contents are updated, which could cause a hang on each keystroke if the file is large
33+
/// enough.
34+
///
35+
/// To receive notifications for content updates, subscribe to one of the publishers on ``contentCoordinator``.
36+
var content: NSTextStorage?
37+
38+
/// The string encoding of the original file. Used to save the file back to the encoding it was loaded from.
39+
var sourceEncoding: FileEncoding?
40+
41+
/// The coordinator to use to subscribe to edit events and cursor location events.
42+
/// See ``CodeEditSourceEditor/CombineCoordinator``.
43+
@Published var contentCoordinator: CombineCoordinator = CombineCoordinator()
3044

3145
/// Used to override detected languages.
3246
@Published var language: CodeLanguage?
@@ -51,7 +65,7 @@ final class CodeFileDocument: NSDocument, ObservableObject {
5165
/// - Note: The UTType doesn't necessarily mean the file extension, it can be the MIME
5266
/// type or any other form of data representation.
5367
var utType: UTType? {
54-
if !self.content.isEmpty {
68+
if content != nil && content?.isEmpty ?? true {
5569
return .text
5670
}
5771
guard let fileType, let type = UTType(fileType) else {
@@ -117,16 +131,30 @@ final class CodeFileDocument: NSDocument, ObservableObject {
117131
}
118132

119133
override func data(ofType _: String) throws -> Data {
120-
guard let data = content.data(using: .utf8) else { throw CodeFileError.failedToEncode }
134+
guard let sourceEncoding, let data = (content?.string as NSString?)?.data(using: sourceEncoding.nsValue) else {
135+
throw CodeFileError.failedToEncode
136+
}
121137
return data
122138
}
123139

124140
/// This function is used for decoding files.
125141
/// It should not throw error as unsupported files can still be opened by QLPreviewView.
126142
override func read(from data: Data, ofType _: String) throws {
127143
var nsString: NSString?
128-
NSString.stringEncoding(for: data, encodingOptions: nil, convertedString: &nsString, usedLossyConversion: nil)
129-
self.content = nsString as? String ?? ""
144+
let rawEncoding = NSString.stringEncoding(
145+
for: data,
146+
encodingOptions: [
147+
.allowLossyKey: false, // Fail if using lossy encoding.
148+
.suggestedEncodingsKey: FileEncoding.allCases.map { $0.nsValue },
149+
.useOnlySuggestedEncodingsKey: true
150+
],
151+
convertedString: &nsString,
152+
usedLossyConversion: nil
153+
)
154+
if let validEncoding = FileEncoding(rawEncoding), let nsString {
155+
self.sourceEncoding = validEncoding
156+
self.content = NSTextStorage(string: nsString as String)
157+
}
130158
}
131159

132160
/// Triggered when change occurred
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
//
2+
// FileEncoding.swift
3+
// CodeEdit
4+
//
5+
// Created by Khan Winter on 5/31/24.
6+
//
7+
8+
import Foundation
9+
10+
enum FileEncoding: CaseIterable {
11+
case utf8
12+
case utf16BE
13+
case utf16LE
14+
15+
var nsValue: UInt {
16+
switch self {
17+
case .utf8:
18+
return NSUTF8StringEncoding
19+
case .utf16BE:
20+
return NSUTF16BigEndianStringEncoding
21+
case .utf16LE:
22+
return NSUTF16LittleEndianStringEncoding
23+
}
24+
}
25+
26+
init?(_ int: UInt) {
27+
switch int {
28+
case NSUTF8StringEncoding:
29+
self = .utf8
30+
case NSUTF16BigEndianStringEncoding:
31+
self = .utf16BE
32+
case NSUTF16LittleEndianStringEncoding:
33+
self = .utf16LE
34+
default:
35+
return nil
36+
}
37+
}
38+
}

CodeEdit/Features/Editor/Models/EditorInstance.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,6 @@ class EditorInstance: Hashable {
7777
/// - Returns: The number of lines contained by the given range. Or `0` if the text view could not be found,
7878
/// or lines could not be found for the given range.
7979
func linesInRange(_ range: NSRange) -> Int {
80-
// TODO: textView should be public, workaround for now
8180
guard let controller = textViewController,
8281
let scrollView = controller.view as? NSScrollView,
8382
let textView = scrollView.documentView as? TextView,

CodeEdit/Features/Editor/Views/CodeFileView.swift

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -48,15 +48,15 @@ struct CodeFileView: View {
4848

4949
@StateObject private var themeModel: ThemeModel = .shared
5050

51-
private var cancellables = [AnyCancellable]()
51+
private var cancellables = Set<AnyCancellable>()
5252

5353
private let isEditable: Bool
5454

5555
private let undoManager = CEUndoManager()
5656

5757
init(codeFile: CodeFileDocument, textViewCoordinators: [TextViewCoordinator] = [], isEditable: Bool = true) {
58-
self.codeFile = codeFile
59-
self.textViewCoordinators = textViewCoordinators
58+
self._codeFile = .init(wrappedValue: codeFile)
59+
self.textViewCoordinators = textViewCoordinators + [codeFile.contentCoordinator]
6060
self.isEditable = isEditable
6161

6262
if let openOptions = codeFile.openOptions {
@@ -65,16 +65,12 @@ struct CodeFileView: View {
6565
}
6666

6767
codeFile
68-
.$content
69-
.dropFirst()
70-
.debounce(
71-
for: 0.25,
72-
scheduler: DispatchQueue.main
73-
)
68+
.contentCoordinator
69+
.textUpdatePublisher
70+
.debounce(for: 0.25, scheduler: DispatchQueue.main)
7471
.sink { _ in
7572
codeFile.updateChangeCount(.changeDone)
76-
codeFile.autosave(withImplicitCancellability: false) { _ in
77-
}
73+
codeFile.autosave(withImplicitCancellability: false) { _ in }
7874
}
7975
.store(in: &cancellables)
8076

@@ -109,7 +105,7 @@ struct CodeFileView: View {
109105

110106
var body: some View {
111107
CodeEditSourceEditor(
112-
$codeFile.content,
108+
codeFile.content ?? NSTextStorage(),
113109
language: getLanguage(),
114110
theme: selectedTheme.editor.editorTheme,
115111
font: font,
@@ -160,8 +156,8 @@ struct CodeFileView: View {
160156
}
161157
return codeFile.language ?? CodeLanguage.detectLanguageFrom(
162158
url: url,
163-
prefixBuffer: codeFile.content.getFirstLines(5),
164-
suffixBuffer: codeFile.content.getLastLines(5)
159+
prefixBuffer: codeFile.content?.string.getFirstLines(5),
160+
suffixBuffer: codeFile.content?.string.getLastLines(5)
165161
)
166162
}
167163

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
//
2+
// NSTextStorage+isEmpty.swift
3+
// CodeEdit
4+
//
5+
// Created by Khan Winter on 5/19/24.
6+
//
7+
8+
import AppKit
9+
10+
extension NSTextStorage {
11+
var isEmpty: Bool {
12+
length == 0
13+
}
14+
}

CodeEditTests/Features/CodeFile/CodeFileTests.swift

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@ import XCTest
1111
@testable import CodeEdit
1212

1313
final class CodeFileUnitTests: XCTestCase {
14-
func testViewContentLoading() throws {
14+
var fileURL: URL!
15+
16+
override func setUp() async throws {
1517
let directory = try FileManager.default.url(
1618
for: .developerApplicationDirectory,
1719
in: .userDomainMask,
@@ -21,9 +23,10 @@ final class CodeFileUnitTests: XCTestCase {
2123
.appendingPathComponent("CodeEdit", isDirectory: true)
2224
.appendingPathComponent("WorkspaceClientTests", isDirectory: true)
2325
try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
26+
fileURL = directory.appendingPathComponent("fakeFile.swift")
27+
}
2428

25-
let fileURL = directory.appendingPathComponent("fakeFile.swift")
26-
29+
func testLoadUTF8Encoding() throws {
2730
let fileContent = "func test(){}"
2831

2932
try fileContent.data(using: .utf8)?.write(to: fileURL)
@@ -32,6 +35,29 @@ final class CodeFileUnitTests: XCTestCase {
3235
withContentsOf: fileURL,
3336
ofType: "public.source-code"
3437
)
35-
XCTAssertEqual(codeFile.content, fileContent)
38+
XCTAssertEqual(codeFile.content?.string, fileContent)
39+
XCTAssertEqual(codeFile.sourceEncoding, .utf8)
40+
}
41+
42+
func testWriteUTF8Encoding() throws {
43+
let codeFile = CodeFileDocument()
44+
codeFile.content = NSTextStorage(string: "func test(){}")
45+
codeFile.sourceEncoding = .utf8
46+
try codeFile.write(to: fileURL, ofType: "public.source-code")
47+
48+
let data = try Data(contentsOf: fileURL)
49+
var nsString: NSString?
50+
let fileEncoding = NSString.stringEncoding(
51+
for: data,
52+
encodingOptions: [
53+
.suggestedEncodingsKey: FileEncoding.allCases.map { $0.nsValue },
54+
.useOnlySuggestedEncodingsKey: true
55+
],
56+
convertedString: &nsString,
57+
usedLossyConversion: nil
58+
)
59+
60+
XCTAssertEqual(codeFile.content?.string as NSString?, nsString)
61+
XCTAssertEqual(fileEncoding, NSUTF8StringEncoding)
3662
}
3763
}

0 commit comments

Comments
 (0)