55// Created by Gary Tokman on 3/26/26.
66//
77
8+ import Litext
89import MarkdownParser
910import MarkdownView
1011import UIKit
@@ -13,6 +14,8 @@ class DetailViewController: UIViewController {
1314
1415 private let example : DiffExample
1516 private let markdownView = MarkdownTextView ( )
17+ private let commentButton = UIButton ( type: . system)
18+ private var currentSelectionInfo : LineSelectionInfo ?
1619
1720 init ( example: DiffExample ) {
1821 self . example = example
@@ -29,7 +32,33 @@ class DetailViewController: UIViewController {
2932 title = example. title
3033 view. backgroundColor = . systemBackground
3134
32- // Custom menu items
35+ switch example. selectionMode {
36+ case . textSelection:
37+ setupTextSelection ( )
38+ case . lineSelection:
39+ setupLineSelection ( )
40+ }
41+
42+ view. addSubview ( markdownView)
43+ markdownView. translatesAutoresizingMaskIntoConstraints = false
44+ NSLayoutConstraint . activate ( [
45+ markdownView. topAnchor. constraint ( equalTo: view. safeAreaLayoutGuide. topAnchor, constant: 16 ) ,
46+ markdownView. leadingAnchor. constraint ( equalTo: view. leadingAnchor, constant: 16 ) ,
47+ markdownView. trailingAnchor. constraint ( equalTo: view. trailingAnchor, constant: - 16 ) ,
48+ ] )
49+
50+ let parser = MarkdownParser ( )
51+ let result = parser. parse ( example. markdown)
52+ let content = MarkdownTextView . PreprocessedContent (
53+ parserResult: result,
54+ theme: . default
55+ )
56+ markdownView. setMarkdown ( content)
57+ }
58+
59+ // MARK: - Text Selection
60+
61+ private func setupTextSelection( ) {
3362 markdownView. textView. customMenuItems = [
3463 LTXCustomMenuItem (
3564 title: " Explain " ,
@@ -50,21 +79,225 @@ class DetailViewController: UIViewController {
5079 print ( " Reject: \" \( context. text) \" (lines \( context. startLine) - \( context. endLine) ) " )
5180 } ,
5281 ]
82+ }
5383
54- view. addSubview ( markdownView)
55- markdownView. translatesAutoresizingMaskIntoConstraints = false
84+ // MARK: - Line Selection
85+
86+ private func setupLineSelection( ) {
87+ markdownView. lineSelectionHandler = { [ weak self] info in
88+ self ? . updateCommentButton ( info: info)
89+ }
90+
91+ setupCommentButton ( )
92+ }
93+
94+ private func setupCommentButton( ) {
95+ commentButton. isHidden = true
96+
97+ var config = UIButton . Configuration. filled ( )
98+ config. baseBackgroundColor = . systemGray5
99+ config. baseForegroundColor = . label
100+ config. cornerStyle = . capsule
101+ config. contentInsets = NSDirectionalEdgeInsets ( top: 10 , leading: 16 , bottom: 10 , trailing: 16 )
102+ config. image = UIImage ( systemName: " text.bubble " )
103+ config. imagePadding = 8
104+ config. preferredSymbolConfigurationForImage = UIImage . SymbolConfiguration ( pointSize: 14 )
105+ commentButton. configuration = config
106+ commentButton. addTarget ( self , action: #selector( commentTapped) , for: . touchUpInside)
107+
108+ view. addSubview ( commentButton)
109+ commentButton. translatesAutoresizingMaskIntoConstraints = false
56110 NSLayoutConstraint . activate ( [
57- markdownView. topAnchor. constraint ( equalTo: view. safeAreaLayoutGuide. topAnchor, constant: 16 ) ,
58- markdownView. leadingAnchor. constraint ( equalTo: view. leadingAnchor, constant: 16 ) ,
59- markdownView. trailingAnchor. constraint ( equalTo: view. trailingAnchor, constant: - 16 ) ,
111+ commentButton. centerXAnchor. constraint ( equalTo: view. centerXAnchor) ,
112+ commentButton. bottomAnchor. constraint ( equalTo: view. safeAreaLayoutGuide. bottomAnchor, constant: - 16 ) ,
60113 ] )
114+ }
115+
116+ private func updateCommentButton( info: LineSelectionInfo ? ) {
117+ currentSelectionInfo = info
118+ guard let info else {
119+ commentButton. isHidden = true
120+ return
121+ }
122+
123+ let range = info. lineRange
124+ let title : String
125+ if range. lowerBound == range. upperBound {
126+ title = " Comment on line \( range. lowerBound) "
127+ } else {
128+ title = " Comment on lines \( range. lowerBound) - \( range. upperBound) "
129+ }
130+ commentButton. configuration? . title = title
131+ commentButton. isHidden = false
132+
133+ UIView . animate ( withDuration: 0.2 ) {
134+ self . commentButton. transform = CGAffineTransform ( scaleX: 1.05 , y: 1.05 )
135+ } completion: { _ in
136+ UIView . animate ( withDuration: 0.15 ) {
137+ self . commentButton. transform = . identity
138+ }
139+ }
140+ }
141+
142+ @objc private func commentTapped( ) {
143+ guard let info = currentSelectionInfo else { return }
144+ let sheet = CommentSheetViewController ( selectionInfo: info, language: example. language)
145+ if let pc = sheet. presentationController as? UISheetPresentationController {
146+ pc. detents = [ . large( ) ]
147+ }
148+ present ( sheet, animated: true )
149+ }
150+ }
151+
152+ // MARK: - Comment Sheet
153+
154+ class CommentSheetViewController : UIViewController {
155+
156+ private let selectionInfo : LineSelectionInfo
157+ private let language : String ?
158+ private let codeMarkdownView = MarkdownTextView ( )
159+ private let commentTextView = UITextView ( )
160+
161+ init ( selectionInfo: LineSelectionInfo , language: String ? ) {
162+ self . selectionInfo = selectionInfo
163+ self . language = language
164+ super. init ( nibName: nil , bundle: nil )
165+ }
166+
167+ @available ( * , unavailable)
168+ required init ? ( coder: NSCoder ) {
169+ fatalError ( )
170+ }
171+
172+ override func viewDidLoad( ) {
173+ super. viewDidLoad ( )
174+ view. backgroundColor = . systemBackground
175+ setupNavBar ( )
176+ setupCodePreview ( )
177+ setupCommentTextView ( )
178+ }
179+
180+ override func viewDidAppear( _ animated: Bool ) {
181+ super. viewDidAppear ( animated)
182+ commentTextView. becomeFirstResponder ( )
183+ }
184+
185+ private func setupNavBar( ) {
186+ let navBar = UINavigationBar ( )
187+ navBar. isTranslucent = false
188+ navBar. barTintColor = . systemBackground
189+ navBar. shadowImage = UIImage ( )
190+ navBar. translatesAutoresizingMaskIntoConstraints = false
191+ view. addSubview ( navBar)
192+ NSLayoutConstraint . activate ( [
193+ navBar. topAnchor. constraint ( equalTo: view. topAnchor) ,
194+ navBar. leadingAnchor. constraint ( equalTo: view. leadingAnchor) ,
195+ navBar. trailingAnchor. constraint ( equalTo: view. trailingAnchor) ,
196+ ] )
197+
198+ let navItem = UINavigationItem ( title: " Add Comment " )
199+
200+ let closeButton = UIBarButtonItem (
201+ image: UIImage ( systemName: " xmark " ) ,
202+ style: . plain,
203+ target: self ,
204+ action: #selector( closeTapped)
205+ )
206+ closeButton. tintColor = . secondaryLabel
207+ navItem. leftBarButtonItem = closeButton
208+
209+ let commentAction = UIAction ( title: " Comment " ) { [ weak self] _ in
210+ self ? . submitComment ( )
211+ }
212+ let commentButton = UIButton ( type: . system, primaryAction: commentAction)
213+ commentButton. configuration = {
214+ var config = UIButton . Configuration. plain ( )
215+ config. title = " Comment "
216+ config. baseForegroundColor = . label
217+ config. contentInsets = NSDirectionalEdgeInsets ( top: 6 , leading: 14 , bottom: 6 , trailing: 14 )
218+ return config
219+ } ( )
220+ navItem. rightBarButtonItem = UIBarButtonItem ( customView: commentButton)
221+
222+ navBar. setItems ( [ navItem] , animated: false )
223+ }
224+
225+ private func setupCodePreview( ) {
226+ codeMarkdownView. translatesAutoresizingMaskIntoConstraints = false
227+ view. addSubview ( codeMarkdownView)
228+ NSLayoutConstraint . activate ( [
229+ codeMarkdownView. topAnchor. constraint ( equalTo: view. topAnchor, constant: 56 ) ,
230+ codeMarkdownView. leadingAnchor. constraint ( equalTo: view. leadingAnchor, constant: 16 ) ,
231+ codeMarkdownView. trailingAnchor. constraint ( equalTo: view. trailingAnchor, constant: - 16 ) ,
232+ ] )
233+
234+ let lines = selectionInfo. contents
235+ let startLine = selectionInfo. lineRange. lowerBound
236+ let lang = language ?? " "
237+ let diffBlock = lines. enumerated ( ) . map { idx, line in
238+ let lineNum = startLine + idx
239+ return " \( lineNum) \( line) "
240+ } . joined ( separator: " \n " )
241+
242+ let markdown = """
243+ ```diff \( lang)
244+ \( diffBlock)
245+ ```
246+ """
61247
62248 let parser = MarkdownParser ( )
63- let result = parser. parse ( example . markdown)
249+ let result = parser. parse ( markdown)
64250 let content = MarkdownTextView . PreprocessedContent (
65251 parserResult: result,
66252 theme: . default
67253 )
68- markdownView. setMarkdown ( content)
254+ codeMarkdownView. setMarkdown ( content)
255+ }
256+
257+ private func setupCommentTextView( ) {
258+ commentTextView. font = . systemFont( ofSize: 16 )
259+ commentTextView. textColor = . label
260+ commentTextView. backgroundColor = . clear
261+ commentTextView. textContainerInset = UIEdgeInsets ( top: 12 , left: 12 , bottom: 12 , right: 12 )
262+ commentTextView. translatesAutoresizingMaskIntoConstraints = false
263+
264+ let placeholder = UILabel ( )
265+ placeholder. text = " Leave a comment... "
266+ placeholder. font = . systemFont( ofSize: 16 )
267+ placeholder. textColor = . placeholderText
268+ placeholder. translatesAutoresizingMaskIntoConstraints = false
269+ commentTextView. addSubview ( placeholder)
270+ NSLayoutConstraint . activate ( [
271+ placeholder. topAnchor. constraint ( equalTo: commentTextView. topAnchor, constant: 12 ) ,
272+ placeholder. leadingAnchor. constraint ( equalTo: commentTextView. leadingAnchor, constant: 17 ) ,
273+ ] )
274+ self . placeholderLabel = placeholder
275+ commentTextView. delegate = self
276+
277+ view. addSubview ( commentTextView)
278+ NSLayoutConstraint . activate ( [
279+ commentTextView. topAnchor. constraint ( equalTo: codeMarkdownView. bottomAnchor, constant: 8 ) ,
280+ commentTextView. leadingAnchor. constraint ( equalTo: view. leadingAnchor) ,
281+ commentTextView. trailingAnchor. constraint ( equalTo: view. trailingAnchor) ,
282+ commentTextView. bottomAnchor. constraint ( equalTo: view. keyboardLayoutGuide. topAnchor) ,
283+ ] )
284+ }
285+
286+ private weak var placeholderLabel : UILabel ?
287+
288+ @objc private func closeTapped( ) {
289+ dismiss ( animated: true )
290+ }
291+
292+ private func submitComment( ) {
293+ let text = commentTextView. text ?? " "
294+ print ( " Comment submitted: \" \( text) \" on lines \( selectionInfo. lineRange) " )
295+ dismiss ( animated: true )
296+ }
297+ }
298+
299+ extension CommentSheetViewController : UITextViewDelegate {
300+ func textViewDidChange( _ textView: UITextView ) {
301+ placeholderLabel? . isHidden = !textView. text. isEmpty
69302 }
70303}
0 commit comments