@@ -28,6 +28,86 @@ struct FileModel: Identifiable, Hashable {
2828 var id : String { relativePath }
2929}
3030
31+ /// Stable protocol shared by every Studio host. Keeping message names here
32+ /// prevents the iOS and macOS bridges from drifting independently.
33+ enum StudioProtocol {
34+ static let version = 1
35+ static let ready = " studio.ready "
36+ static let documentOpen = " document.open "
37+ static let documentChanged = " document.changed "
38+ static let documentSave = " document.save "
39+ static let documentReplace = " document.replace "
40+ static let documentSetReadOnly = " document.setReadOnly "
41+ static let editorInsert = " editor.insert "
42+ static let editorFormat = " editor.format "
43+ static let editorGetState = " editor.getState "
44+
45+ static func envelope( type: String , documentID: String ? , payload: [ String : Any ] ) -> [ String : Any ] {
46+ [
47+ " protocolVersion " : version,
48+ " type " : type,
49+ " documentID " : documentID as Any ? ?? NSNull ( ) ,
50+ " payload " : payload,
51+ ]
52+ }
53+ }
54+
55+ struct StudioDocumentSnapshot : Equatable {
56+ let content : String
57+ let version : Int
58+ let selection : Range < Int >
59+
60+ init ? ( state: Any ? ) {
61+ guard
62+ let state = state as? [ String : Any ] ,
63+ let content = state [ " content " ] as? String
64+ else { return nil }
65+ let version = state [ " version " ] as? Int ?? 0
66+ let selection = state [ " selection " ] as? [ String : Any ]
67+ let from = selection ? [ " from " ] as? Int ?? 0
68+ let to = selection ? [ " to " ] as? Int ?? from
69+ self . init ( content: content, version: version, selection: min ( from, to) ..< max ( from, to) )
70+ }
71+
72+ init ( content: String , version: Int , selection: Range < Int > ) {
73+ self . content = content
74+ self . version = version
75+ self . selection = selection
76+ }
77+ }
78+
79+ /// Owns document identity and the persisted-draft baseline for native Studio
80+ /// hosts. Web views remain responsible only for transport and presentation.
81+ final class StudioDocumentSession {
82+ private( set) var documentID : String ?
83+ private( set) var savedContent = " "
84+ private let drafts : StudioDraftStore
85+
86+ init ( drafts: StudioDraftStore = . shared) {
87+ self . drafts = drafts
88+ }
89+
90+ func open( documentID: String , content: String ) -> String {
91+ self . documentID = documentID
92+ savedContent = content
93+ return drafts. recover ( documentID: documentID, currentContent: content) ? . content ?? content
94+ }
95+
96+ func recordDraft( _ content: String ) {
97+ guard let documentID else { return }
98+ drafts. save ( documentID: documentID, baseContent: savedContent, content: content)
99+ }
100+
101+ func markSaved( _ content: String ) {
102+ savedContent = content
103+ if let documentID { drafts. remove ( documentID: documentID) }
104+ }
105+
106+ func needsSave( _ content: String ) -> Bool {
107+ content != savedContent
108+ }
109+ }
110+
31111/// Crash-safe editor drafts. A draft is restored only while its base file hash
32112/// still matches, so an iCloud or external edit always wins over stale local work.
33113struct StudioDraftRecord : Codable , Equatable {
0 commit comments