Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,8 @@ extension TextViewController {

if let findViewController, findViewController.viewModel.isShowingFindPanel,
textViewHasFocus || findPanelHasFocus,
findViewController.findPanel.handleKeyDown(event) == nil {
let findPanel = findViewController.findPanel,
findPanel.handleKeyDown(event) == nil {
return nil
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@ extension FindViewController {
viewModel.isShowingFindPanel = true

// Smooth out the animation by placing the find panel just outside the correct position before animating.
findPanel.isHidden = false
findPanelVerticalConstraint.constant = resolvedTopPadding - viewModel.panelHeight
installFindPanel().isHidden = false
findPanelVerticalConstraint?.constant = resolvedTopPadding - viewModel.panelHeight

view.layoutSubtreeIfNeeded()

Expand Down Expand Up @@ -65,13 +65,13 @@ extension FindViewController {
/// - Sets the text view to be the first responder.
func hideFindPanel(animated: Bool = true) {
viewModel.isShowingFindPanel = false
_ = findPanel.resignFirstResponder()
_ = findPanel?.resignFirstResponder()

conditionalAnimated(animated) {
viewModel.target?.findPanelWillHide(panelHeight: viewModel.panelHeight)
setFindPanelConstraintHide()
} onComplete: { [weak self] in
self?.findPanel.isHidden = true
self?.findPanel?.isHidden = true
self?.viewModel.isFocused = false
}

Expand Down Expand Up @@ -120,16 +120,15 @@ extension FindViewController {
/// Sets the find panel constraint to show the find panel.
/// Can be animated using implicit animation.
func setFindPanelConstraintShow() {
// Update the find panel's top to be equal to the view's top.
guard let findPanelVerticalConstraint else { return }
findPanelVerticalConstraint.constant = resolvedTopPadding
findPanelVerticalConstraint.isActive = true
}

/// Sets the find panel constraint to hide the find panel.
/// Can be animated using implicit animation.
func setFindPanelConstraintHide() {
// Update the find panel's top anchor to be equal to it's negative height, hiding it above the view.

guard let findPanelVerticalConstraint else { return }
// SwiftUI hates us. It refuses to move views outside of the safe are if they don't have the `.ignoresSafeArea`
// modifier, but with that modifier on it refuses to allow it to be animated outside the safe area.
// The only way I found to fix it was to multiply the height by 3 here.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,10 @@ final class FindViewController: NSViewController {
}

var childView: NSView
var findPanel: FindPanelHostingView
var findPanelVerticalConstraint: NSLayoutConstraint!
// Built on first show, as NSTextFinder builds its find bar: a hidden hosting view is still laid out, and
// SwiftUI inserting its controls forces a layout of the whole editor around them.
private(set) var findPanel: FindPanelHostingView?
private(set) var findPanelVerticalConstraint: NSLayoutConstraint?

/// The 'real' top padding amount.
/// Is equal to ``topPadding`` if set, or the view's top safe area inset if not.
Expand All @@ -35,7 +37,6 @@ final class FindViewController: NSViewController {
init(target: FindPanelTarget, childView: NSView) {
viewModel = FindPanelViewModel(target: target)
self.childView = childView
findPanel = FindPanelHostingView(viewModel: viewModel)
super.init(nibName: nil, bundle: nil)
viewModel.dismiss = { [weak self] in
self?.hideFindPanel()
Expand Down Expand Up @@ -63,21 +64,8 @@ final class FindViewController: NSViewController {
/// scopes its hover tracking and the ribbons' accessibility elements.
view.clipsToBounds = true
view.addSubview(childView)
// Ordered above the content so the panel wins hit-testing as well as drawing.
// `zPosition` alone only affects drawing, which left clicks falling through to the
// text view underneath.
view.addSubview(findPanel, positioned: .above, relativeTo: childView)
findPanel.wantsLayer = true

findPanelVerticalConstraint = findPanel.topAnchor.constraint(equalTo: view.topAnchor)

NSLayoutConstraint.activate([
// Constrain find panel
findPanelVerticalConstraint,
findPanel.leadingAnchor.constraint(equalTo: view.leadingAnchor),
findPanel.trailingAnchor.constraint(equalTo: view.trailingAnchor),

// Constrain child view
childView.topAnchor.constraint(equalTo: view.topAnchor),
childView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
childView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
Expand All @@ -87,12 +75,33 @@ final class FindViewController: NSViewController {

override func viewWillAppear() {
super.viewWillAppear()
if viewModel.isShowingFindPanel { // Update constraints for initial state
findPanel.isHidden = false
setFindPanelConstraintShow()
} else {
findPanel.isHidden = true
setFindPanelConstraintHide()
guard viewModel.isShowingFindPanel else { return }
installFindPanel().isHidden = false
setFindPanelConstraintShow()
}

@discardableResult
func installFindPanel() -> FindPanelHostingView {
if let findPanel { return findPanel }
let panel = FindPanelHostingView(viewModel: viewModel)
// Ordered above the content so the panel wins hit-testing as well as drawing.
// `zPosition` alone only affects drawing, which left clicks falling through to the
// text view underneath.
view.addSubview(panel, positioned: .above, relativeTo: childView)
panel.wantsLayer = true
let top = panel.topAnchor.constraint(equalTo: view.topAnchor)
NSLayoutConstraint.activate([
top,
panel.leadingAnchor.constraint(equalTo: view.leadingAnchor),
panel.trailingAnchor.constraint(equalTo: view.trailingAnchor)
])
findPanel = panel
findPanelVerticalConstraint = top
setFindPanelConstraintHide()
// The panel's onChange searches a term typed into it; one set before it existed has not been searched.
if !viewModel.findText.isEmpty {
viewModel.find()
}
return panel
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ internal struct EditorKeyMonitorCompositionTests {
Mock.beginComposition("le", in: editor.textView)
let escape = try #require(Mock.keyDown(keyCode: kVK_Escape, characters: "\u{1b}", in: window))

#expect(finder.findPanel.handleKeyDown(escape) === escape)
#expect(try #require(finder.findPanel).handleKeyDown(escape) === escape)
#expect(finder.viewModel.isShowingFindPanel)
}

Expand All @@ -202,7 +202,7 @@ internal struct EditorKeyMonitorCompositionTests {
try #require(fieldEditor.hasMarkedText())
let escape = try #require(Mock.keyDown(keyCode: kVK_Escape, characters: "\u{1b}", in: window))

#expect(finder.findPanel.handleKeyDown(escape) === escape)
#expect(try #require(finder.findPanel).handleKeyDown(escape) === escape)
#expect(finder.viewModel.isShowingFindPanel)
}

Expand All @@ -215,7 +215,7 @@ internal struct EditorKeyMonitorCompositionTests {
_ = window.makeFirstResponder(editor.textView)
let escape = try #require(Mock.keyDown(keyCode: kVK_Escape, characters: "\u{1b}", in: window))

#expect(finder.findPanel.handleKeyDown(escape) == nil)
#expect(try #require(finder.findPanel).handleKeyDown(escape) == nil)
#expect(finder.viewModel.isShowingFindPanel == false)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,13 +131,14 @@ struct TextViewControllerClippingTests {
@Test("The find panel sliding in from above shows only inside the editor")
func findPanelSlidesInsideTheEditor() throws {
let find = try #require(controller.findViewController)
find.findPanel.isHidden = false
find.findPanelVerticalConstraint.constant = find.resolvedTopPadding - find.viewModel.panelHeight / 2
let panel = find.installFindPanel()
panel.isHidden = false
try #require(find.findPanelVerticalConstraint).constant = find.resolvedTopPadding - find.viewModel.panelHeight / 2
find.view.layoutSubtreeIfNeeded()

let panelInWindow = find.findPanel.convert(find.findPanel.bounds, to: nil)
let panelInWindow = panel.convert(panel.bounds, to: nil)
#expect(panelInWindow.maxY > editorInWindow.maxY, "the panel starts its slide above the editor")
#expect(contains(editorInWindow, visibleRectInWindow(find.findPanel)))
#expect(contains(editorInWindow, visibleRectInWindow(panel)))
}

/// The band's red, loosely: the cached drawing lands in a color space that shifts the fill.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ final class TextViewControllerTests: XCTestCase {
scrollView.contentInsets,
NSEdgeInsets(top: 10 + findModel.viewModel.panelHeight, left: 0, bottom: 10, right: 0)
)
XCTAssertEqual(controller.findViewController?.findPanelVerticalConstraint.constant, 0)
XCTAssertEqual(controller.findViewController?.findPanelVerticalConstraint?.constant, 0)
XCTAssertEqual(controller.gutterView.frame.origin.y, -10 - findModel.viewModel.panelHeight)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,33 @@ struct FindPanelTests {
#expect(model.panelHeight == 54)
}

@Test("The find panel is built on its first show, not with the editor, and kept after it hides")
func findPanelIsBuiltOnFirstShow() {
#expect(viewController.findPanel == nil)
#expect(!viewController.view.subviews.contains { $0 is FindPanelHostingView })

viewController.showFindPanel(animated: false)
let panel = viewController.findPanel
#expect(panel?.superview === viewController.view)
#expect(panel?.isHidden == false)

viewController.hideFindPanel(animated: false)
#expect(viewController.findPanel === panel)
#expect(panel?.isHidden == true)
}

@Test("A term set before the panel was first shown is searched when it opens")
func termSetBeforeFirstShowIsSearched() {
let target = MockPanelTarget(text: "SELECT 1; SELECT 2")
let controller = FindViewController(target: target, childView: NSView())
controller.loadView()
controller.viewModel.findText = "SELECT"

controller.showFindPanel(animated: false)

#expect(controller.viewModel.matchCount == 2)
}

@Test func findPanelShowsOnCommandF() async throws {
// Show find panel
viewController.showFindPanel()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -168,22 +168,17 @@ public enum OracleSchemaQueries {
"ALTER SESSION SET CURRENT_SCHEMA = \(quoteIdentifier(schema))"
}

/// `ALL_PART_TABLES` carries the partition count without reading a LONG column, which this
/// driver cannot decode. Its `INTERVAL` column is the flag for interval partitioning, where the
/// count is a placeholder for a range the server extends on demand rather than a number of
/// partitions that exist, so those report no count instead of a fabricated one.
///
/// Being partitioned is projected separately from the count for exactly that reason: an
/// interval-partitioned table has partitions and no count to state, and reading the missing
/// count as "not partitioned" would take its partitions out of the tree.
// The count comes from `partitionCounts(owners:)`: joining `ALL_PART_TABLES` here made every hard
// parse of a 100-schema listing cost 0.5 to 2.4 s of server CPU, against 0.1 to 0.3 s without it.
// Being partitioned is projected apart from the count, since an interval-partitioned table has
// partitions and no count to state.
public static func tables(schema: String) -> String {
let owner = escapeLiteral(schema)
return """
SELECT t.table_name, 'BASE TABLE' AS table_type,
CASE WHEN pt.table_name IS NULL THEN 'N' ELSE 'Y' END AS is_partitioned,
CASE WHEN pt.interval IS NULL THEN pt.partition_count END AS partition_count
CASE WHEN t.partitioned = 'YES' THEN 'Y' ELSE 'N' END AS is_partitioned,
NULL AS partition_count
FROM \(OracleDictionary.allTables) t
LEFT JOIN \(OracleDictionary.allPartTables) pt ON pt.owner = t.owner AND pt.table_name = t.table_name
WHERE t.owner = '\(owner)'
UNION ALL
SELECT view_name, 'VIEW', 'N', NULL FROM \(OracleDictionary.allViews) WHERE owner = '\(owner)'
Expand All @@ -201,11 +196,10 @@ public enum OracleSchemaQueries {
let owners = chunk.map { "'\(escapeLiteral($0))'" }.joined(separator: ", ")
return """
SELECT t.table_name, 'BASE TABLE' AS table_type,
CASE WHEN pt.table_name IS NULL THEN 'N' ELSE 'Y' END AS is_partitioned,
CASE WHEN pt.interval IS NULL THEN pt.partition_count END AS partition_count,
CASE WHEN t.partitioned = 'YES' THEN 'Y' ELSE 'N' END AS is_partitioned,
NULL AS partition_count,
t.owner
FROM \(OracleDictionary.allTables) t
LEFT JOIN \(OracleDictionary.allPartTables) pt ON pt.owner = t.owner AND pt.table_name = t.table_name
WHERE t.owner IN (\(owners))
UNION ALL
SELECT view_name, 'VIEW', 'N', NULL, owner FROM \(OracleDictionary.allViews) WHERE owner IN (\(owners))
Expand All @@ -214,6 +208,47 @@ public enum OracleSchemaQueries {
}
}

// `ALL_PART_TABLES` states the count without reading a LONG column, which this driver cannot
// decode. An interval-partitioned table's count is a placeholder for the range the server extends
// on demand, so it reports none.
public static func partitionCounts(owners: [String]) -> [String] {
stride(from: 0, to: owners.count, by: maxOwnersPerTableListing).map { start in
let chunk = owners[start..<min(start + maxOwnersPerTableListing, owners.count)]
let list = chunk.map { "'\(escapeLiteral($0))'" }.joined(separator: ", ")
return """
SELECT owner, table_name, CASE WHEN interval IS NULL THEN partition_count END AS partition_count
FROM \(OracleDictionary.allPartTables)
WHERE owner IN (\(list))
"""
}
}

// A one-schema listing does not project the owner, so its rows count as `defaultOwner`'s.
public static func ownersWithPartitionedTables(in rows: [OracleTableRow], defaultOwner: String?) -> [String] {
let owners = rows.lazy.filter(\.isPartitioned).compactMap { $0.schema ?? defaultOwner }
return Array(Set(owners)).sorted()
}

public static func applyingPartitionCounts(
_ countRows: [[OracleRawCell]],
to rows: [OracleTableRow],
defaultOwner: String?
) -> [OracleTableRow] {
var counts: [String: [String: Int]] = [:]
for row in countRows {
guard let owner = row[safe: 0]?.stringValue,
let table = row[safe: 1]?.stringValue,
let count = row[safe: 2]?.stringValue.flatMap(Int.init) else { continue }
counts[owner, default: [:]][table] = count
}
return rows.map { row in
guard row.isPartitioned,
let owner = row.schema ?? defaultOwner,
let count = counts[owner]?[row.name] else { return row }
return OracleTableRow(name: row.name, isView: row.isView, isPartitioned: true, partitionCount: count, schema: row.schema)
}
}

/// One partitioned table's partitions. `HIGH_VALUE` is deliberately absent: it is a LONG
/// column, the datatype this driver already avoids reading because OracleNIO cannot decode it,
/// so an Oracle partition states its position rather than its bound.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ struct OracleDictionaryQualificationTests {
("users", OracleSchemaQueries.users),
("tables", OracleSchemaQueries.tables(schema: "HR")),
("tables in schemas", OracleSchemaQueries.tables(inSchemas: ["HR", "SALES"]).joined(separator: "\n")),
("partition counts", OracleSchemaQueries.partitionCounts(owners: ["HR", "SALES"]).joined(separator: "\n")),
("partitions", OracleSchemaQueries.partitions(schema: "HR", table: "T")),
("subpartitions", OracleSchemaQueries.subpartitions(schema: "HR", table: "T")),
("columns 11g", OracleSchemaQueries.columns(schema: "HR", table: "T", release: OracleServerRelease(major: 11))),
Expand Down
Loading
Loading