Show diffs without word wrapping
This commit is contained in:
@@ -200,7 +200,8 @@ xcrun simctl launch booted de.rfc1437.gotcha
|
|||||||
- [ ] Open a commit and verify Changed Files paths and statuses.
|
- [ ] Open a commit and verify Changed Files paths and statuses.
|
||||||
- [ ] Open a changed file and verify the diff title, old/new line numbers,
|
- [ ] Open a changed file and verify the diff title, old/new line numbers,
|
||||||
monospaced text, addition/removal/hunk colors, vertical scrolling, and
|
monospaced text, addition/removal/hunk colors, vertical scrolling, and
|
||||||
horizontal scrolling for long lines.
|
source-style horizontal scrolling for long lines without word wrapping,
|
||||||
|
clipped or jumping text, or content hidden beneath the navigation bar.
|
||||||
- [ ] Long-press diff text and verify normal selection and copying.
|
- [ ] Long-press diff text and verify normal selection and copying.
|
||||||
|
|
||||||
## Repository files
|
## Repository files
|
||||||
|
|||||||
@@ -738,6 +738,60 @@ final class RepositoryHistoryViewController: RefreshingTableViewController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private final class CodeScrollView: UIScrollView {
|
||||||
|
private let textView = UITextView(frame: .zero)
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
super.init(frame: frame)
|
||||||
|
translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
alwaysBounceVertical = true
|
||||||
|
alwaysBounceHorizontal = true
|
||||||
|
isDirectionalLockEnabled = true
|
||||||
|
showsHorizontalScrollIndicator = true
|
||||||
|
contentInsetAdjustmentBehavior = .never
|
||||||
|
textView.textContainer.lineBreakMode = .byClipping
|
||||||
|
textView.textContainer.widthTracksTextView = false
|
||||||
|
textView.isEditable = false
|
||||||
|
textView.isScrollEnabled = false
|
||||||
|
textView.isSelectable = true
|
||||||
|
addSubview(textView)
|
||||||
|
}
|
||||||
|
|
||||||
|
@available(*, unavailable)
|
||||||
|
required init?(coder: NSCoder) { fatalError("init(coder:) is not supported") }
|
||||||
|
|
||||||
|
func display(_ text: NSAttributedString) {
|
||||||
|
let text = NSMutableAttributedString(attributedString: text)
|
||||||
|
let paragraph = NSMutableParagraphStyle()
|
||||||
|
paragraph.lineBreakMode = .byClipping
|
||||||
|
text.addAttribute(.paragraphStyle, value: paragraph, range: NSRange(location: 0, length: text.length))
|
||||||
|
textView.textStorage.setAttributedString(text)
|
||||||
|
setNeedsLayout()
|
||||||
|
}
|
||||||
|
|
||||||
|
override func layoutSubviews() {
|
||||||
|
super.layoutSubviews()
|
||||||
|
textView.textContainer.size = CGSize(
|
||||||
|
width: CGFloat.greatestFiniteMagnitude,
|
||||||
|
height: CGFloat.greatestFiniteMagnitude
|
||||||
|
)
|
||||||
|
textView.layoutManager.ensureLayout(for: textView.textContainer)
|
||||||
|
let textSize = textView.layoutManager.usedRect(for: textView.textContainer).size
|
||||||
|
let size = CGSize(
|
||||||
|
width: max(
|
||||||
|
bounds.width,
|
||||||
|
ceil(textSize.width + textView.textContainerInset.left + textView.textContainerInset.right)
|
||||||
|
),
|
||||||
|
height: max(
|
||||||
|
bounds.height,
|
||||||
|
ceil(textSize.height + textView.textContainerInset.top + textView.textContainerInset.bottom)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
textView.frame = CGRect(origin: .zero, size: size)
|
||||||
|
contentSize = size
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
final class RepositoryFileViewController: UIViewController, QLPreviewControllerDataSource {
|
final class RepositoryFileViewController: UIViewController, QLPreviewControllerDataSource {
|
||||||
private let context: AppContext
|
private let context: AppContext
|
||||||
@@ -753,8 +807,6 @@ final class RepositoryFileViewController: UIViewController, QLPreviewControllerD
|
|||||||
private var historyController: RepositoryHistoryViewController?
|
private var historyController: RepositoryHistoryViewController?
|
||||||
private var displayedController: UIViewController?
|
private var displayedController: UIViewController?
|
||||||
private var displayedView: UIView?
|
private var displayedView: UIView?
|
||||||
private weak var sourceScrollView: UIScrollView?
|
|
||||||
private weak var sourceTextView: UITextView?
|
|
||||||
|
|
||||||
init(context: AppContext, owner: String, repository: String, path: String, name: String) {
|
init(context: AppContext, owner: String, repository: String, path: String, name: String) {
|
||||||
self.context = context
|
self.context = context
|
||||||
@@ -791,11 +843,6 @@ final class RepositoryFileViewController: UIViewController, QLPreviewControllerD
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override func viewDidLayoutSubviews() {
|
|
||||||
super.viewDidLayoutSubviews()
|
|
||||||
updateSourceLayout()
|
|
||||||
}
|
|
||||||
|
|
||||||
deinit {
|
deinit {
|
||||||
loadingTask?.cancel()
|
loadingTask?.cancel()
|
||||||
if let previewURL { try? FileManager.default.removeItem(at: previewURL) }
|
if let previewURL { try? FileManager.default.removeItem(at: previewURL) }
|
||||||
@@ -889,41 +936,16 @@ final class RepositoryFileViewController: UIViewController, QLPreviewControllerD
|
|||||||
.font: UIFont.monospacedSystemFont(ofSize: 13, weight: .regular),
|
.font: UIFont.monospacedSystemFont(ofSize: 13, weight: .regular),
|
||||||
.foregroundColor: UIColor.label,
|
.foregroundColor: UIColor.label,
|
||||||
])
|
])
|
||||||
let text = NSMutableAttributedString(attributedString: highlighted)
|
let codeView = CodeScrollView()
|
||||||
let paragraph = NSMutableParagraphStyle()
|
codeView.display(highlighted)
|
||||||
paragraph.lineBreakMode = .byClipping
|
displayedView = codeView
|
||||||
text.addAttribute(.paragraphStyle, value: paragraph, range: NSRange(location: 0, length: text.length))
|
view.addSubview(codeView)
|
||||||
|
|
||||||
let scrollView = UIScrollView()
|
|
||||||
scrollView.translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
scrollView.alwaysBounceHorizontal = true
|
|
||||||
scrollView.isDirectionalLockEnabled = true
|
|
||||||
scrollView.showsHorizontalScrollIndicator = true
|
|
||||||
scrollView.contentInsetAdjustmentBehavior = .never
|
|
||||||
|
|
||||||
let textView = UITextView(frame: .zero)
|
|
||||||
textView.textContainer.size = CGSize(
|
|
||||||
width: CGFloat.greatestFiniteMagnitude,
|
|
||||||
height: CGFloat.greatestFiniteMagnitude
|
|
||||||
)
|
|
||||||
textView.textContainer.lineBreakMode = .byClipping
|
|
||||||
textView.textContainer.widthTracksTextView = false
|
|
||||||
textView.textStorage.setAttributedString(text)
|
|
||||||
textView.isEditable = false
|
|
||||||
textView.isScrollEnabled = false
|
|
||||||
textView.isSelectable = true
|
|
||||||
sourceScrollView = scrollView
|
|
||||||
sourceTextView = textView
|
|
||||||
scrollView.addSubview(textView)
|
|
||||||
displayedView = scrollView
|
|
||||||
view.addSubview(scrollView)
|
|
||||||
NSLayoutConstraint.activate([
|
NSLayoutConstraint.activate([
|
||||||
scrollView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
|
codeView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
|
||||||
scrollView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
|
codeView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
|
||||||
scrollView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
|
codeView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
|
||||||
scrollView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
|
codeView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
|
||||||
])
|
])
|
||||||
updateSourceLayout()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func removeDisplayedView() {
|
private func removeDisplayedView() {
|
||||||
@@ -932,31 +954,6 @@ final class RepositoryFileViewController: UIViewController, QLPreviewControllerD
|
|||||||
displayedController?.removeFromParent()
|
displayedController?.removeFromParent()
|
||||||
displayedController = nil
|
displayedController = nil
|
||||||
displayedView = nil
|
displayedView = nil
|
||||||
sourceScrollView = nil
|
|
||||||
sourceTextView = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
private func updateSourceLayout() {
|
|
||||||
guard let scrollView = sourceScrollView, let textView = sourceTextView else { return }
|
|
||||||
textView.textContainer.widthTracksTextView = false
|
|
||||||
textView.textContainer.size = CGSize(
|
|
||||||
width: CGFloat.greatestFiniteMagnitude,
|
|
||||||
height: CGFloat.greatestFiniteMagnitude
|
|
||||||
)
|
|
||||||
textView.layoutManager.ensureLayout(for: textView.textContainer)
|
|
||||||
let textSize = textView.layoutManager.usedRect(for: textView.textContainer).size
|
|
||||||
let contentSize = CGSize(
|
|
||||||
width: max(
|
|
||||||
scrollView.bounds.width,
|
|
||||||
ceil(textSize.width + textView.textContainerInset.left + textView.textContainerInset.right)
|
|
||||||
),
|
|
||||||
height: max(
|
|
||||||
scrollView.bounds.height,
|
|
||||||
ceil(textSize.height + textView.textContainerInset.top + textView.textContainerInset.bottom)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
textView.frame = CGRect(origin: .zero, size: contentSize)
|
|
||||||
scrollView.contentSize = contentSize
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func showQuickLook(_ data: Data, name: String) throws {
|
private func showQuickLook(_ data: Data, name: String) throws {
|
||||||
@@ -1002,7 +999,7 @@ enum DiffSource {
|
|||||||
final class DiffViewController: UIViewController {
|
final class DiffViewController: UIViewController {
|
||||||
private let context: AppContext
|
private let context: AppContext
|
||||||
private let source: DiffSource
|
private let source: DiffSource
|
||||||
private let textView = UITextView()
|
private let codeView = CodeScrollView()
|
||||||
private let spinner = UIActivityIndicatorView(style: .medium)
|
private let spinner = UIActivityIndicatorView(style: .medium)
|
||||||
private var loadingTask: Task<Void, Never>?
|
private var loadingTask: Task<Void, Never>?
|
||||||
|
|
||||||
@@ -1018,22 +1015,14 @@ final class DiffViewController: UIViewController {
|
|||||||
override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
view.backgroundColor = .systemBackground
|
view.backgroundColor = .systemBackground
|
||||||
textView.translatesAutoresizingMaskIntoConstraints = false
|
codeView.refreshControl = UIRefreshControl()
|
||||||
textView.isEditable = false
|
codeView.refreshControl?.addTarget(self, action: #selector(reload), for: .valueChanged)
|
||||||
textView.isSelectable = true
|
view.addSubview(codeView)
|
||||||
textView.alwaysBounceVertical = true
|
|
||||||
textView.alwaysBounceHorizontal = true
|
|
||||||
textView.showsHorizontalScrollIndicator = true
|
|
||||||
textView.textContainer.widthTracksTextView = false
|
|
||||||
textView.textContainer.lineFragmentPadding = 8
|
|
||||||
textView.refreshControl = UIRefreshControl()
|
|
||||||
textView.refreshControl?.addTarget(self, action: #selector(reload), for: .valueChanged)
|
|
||||||
view.addSubview(textView)
|
|
||||||
NSLayoutConstraint.activate([
|
NSLayoutConstraint.activate([
|
||||||
textView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
codeView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
|
||||||
textView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
codeView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
|
||||||
textView.topAnchor.constraint(equalTo: view.topAnchor),
|
codeView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
|
||||||
textView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
codeView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
|
||||||
])
|
])
|
||||||
beginNavigationLoading(spinner)
|
beginNavigationLoading(spinner)
|
||||||
reload()
|
reload()
|
||||||
@@ -1063,12 +1052,12 @@ final class DiffViewController: UIViewController {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
title = page.title
|
title = page.title
|
||||||
textView.attributedText = diffText(page)
|
codeView.display(diffText(page))
|
||||||
} catch {
|
} catch {
|
||||||
if !Task.isCancelled { show(error: error) }
|
if !Task.isCancelled { show(error: error) }
|
||||||
}
|
}
|
||||||
endNavigationLoading(spinner)
|
endNavigationLoading(spinner)
|
||||||
textView.refreshControl?.endRefreshing()
|
codeView.refreshControl?.endRefreshing()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user