mirror of
https://github.com/coteditor/CotEditor.git
synced 2024-10-06 07:27:56 +03:00
Refactor outline navigation
This commit is contained in:
parent
964538b819
commit
56440e9868
@ -346,6 +346,7 @@
|
||||
2A78BFB31D1B240900A583D2 /* UpdaterManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A78BFB21D1B240900A583D2 /* UpdaterManager.swift */; };
|
||||
2A78BFBC1D1B376000A583D2 /* ServicesProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A78BFBB1D1B376000A583D2 /* ServicesProvider.swift */; };
|
||||
2A78BFBD1D1B376000A583D2 /* ServicesProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A78BFBB1D1B376000A583D2 /* ServicesProvider.swift */; };
|
||||
2A7B279924E435FE00F02304 /* OutlineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A7B279824E435FE00F02304 /* OutlineTests.swift */; };
|
||||
2A80C65E1CEE33C100AA664D /* Credits.html in Resources */ = {isa = PBXBuildFile; fileRef = 2A80C65C1CEE33C100AA664D /* Credits.html */; };
|
||||
2A80C65F1CEE33C100AA664D /* Credits.html in Resources */ = {isa = PBXBuildFile; fileRef = 2A80C65C1CEE33C100AA664D /* Credits.html */; };
|
||||
2A80C6681CEE540F00AA664D /* Acknowledgments.html in Resources */ = {isa = PBXBuildFile; fileRef = 2A80C6661CEE540F00AA664D /* Acknowledgments.html */; };
|
||||
@ -1105,6 +1106,7 @@
|
||||
2A78BFAF1D1B168E00A583D2 /* WebDocumentViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WebDocumentViewController.swift; sourceTree = "<group>"; };
|
||||
2A78BFB21D1B240900A583D2 /* UpdaterManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UpdaterManager.swift; sourceTree = "<group>"; };
|
||||
2A78BFBB1D1B376000A583D2 /* ServicesProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ServicesProvider.swift; sourceTree = "<group>"; };
|
||||
2A7B279824E435FE00F02304 /* OutlineTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OutlineTests.swift; sourceTree = "<group>"; };
|
||||
2A80C65D1CEE33C100AA664D /* en */ = {isa = PBXFileReference; lastKnownFileType = text.html; name = en; path = en.lproj/Credits.html; sourceTree = "<group>"; };
|
||||
2A80C6601CEE351200AA664D /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.html; name = ja; path = ja.lproj/Credits.html; sourceTree = "<group>"; };
|
||||
2A80C6611CEE351400AA664D /* de */ = {isa = PBXFileReference; lastKnownFileType = text.html; name = de; path = de.lproj/Credits.html; sourceTree = "<group>"; };
|
||||
@ -2205,6 +2207,7 @@
|
||||
2AED46721E43942300751C45 /* TextFindTests.swift */,
|
||||
2A7135821CFFDC6600ADA555 /* FilePermissionTests.swift */,
|
||||
2A1893AC1FFF6A0100AD244F /* LineSortTests.swift */,
|
||||
2A7B279824E435FE00F02304 /* OutlineTests.swift */,
|
||||
2A5EDDBC241B64EB00A07810 /* TextClippingTests.swift */,
|
||||
2A64A2352387754000646BE4 /* UserDefaultsObservationTests.swift */,
|
||||
2A719F6523CD92370026F877 /* FuzzyRangeTests.swift */,
|
||||
@ -3038,6 +3041,7 @@
|
||||
2ABFF6D71D02856A00BE2795 /* ShortcutTests.swift in Sources */,
|
||||
2A643BB3245172EB00B2AD54 /* NSBezierPathTests.swift in Sources */,
|
||||
2A476CB11D09D0500088E37A /* FontExtensionTests.swift in Sources */,
|
||||
2A7B279924E435FE00F02304 /* OutlineTests.swift in Sources */,
|
||||
2AA2E0261C0454730087BDD6 /* StringIndentationTests.swift in Sources */,
|
||||
2A476CAE1D09C8C80088E37A /* URLExtensionsTests.swift in Sources */,
|
||||
2AC71DE21BF0BDBC002E1434 /* StringExtensionsTests.swift in Sources */,
|
||||
|
@ -68,4 +68,15 @@ extension NSTextView {
|
||||
return (indentLocation < location) ? indentLocation : lineRange.location
|
||||
}
|
||||
|
||||
|
||||
/// Select the given range with visual feedback.
|
||||
///
|
||||
/// - Parameter range: The character range to select.
|
||||
func select(range: NSRange) {
|
||||
|
||||
self.selectedRange = range
|
||||
self.centerSelectionInVisibleArea(self)
|
||||
self.window?.makeFirstResponder(self)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -151,18 +151,18 @@ final class NavigationBarController: NSViewController {
|
||||
/// Can select the prev item in outline menu?
|
||||
var canSelectPrevItem: Bool {
|
||||
|
||||
guard let menu = self.outlineMenu else { return false }
|
||||
guard let textView = self.textView else { return false }
|
||||
|
||||
return (menu.indexOfSelectedItem > 1)
|
||||
return self.outlineItems.previousItem(for: textView.selectedRange) != nil
|
||||
}
|
||||
|
||||
|
||||
/// Can select the next item in outline menu?
|
||||
var canSelectNextItem: Bool {
|
||||
|
||||
guard let menu = self.outlineMenu else { return false }
|
||||
guard let textView = self.textView else { return false }
|
||||
|
||||
return menu.itemArray[(menu.indexOfSelectedItem + 1)...].contains { $0.representedObject != nil }
|
||||
return self.outlineItems.nextItem(for: textView.selectedRange) != nil
|
||||
}
|
||||
|
||||
|
||||
@ -182,39 +182,35 @@ final class NavigationBarController: NSViewController {
|
||||
@IBAction func selectOutlineMenuItem(_ sender: NSMenuItem) {
|
||||
|
||||
guard
|
||||
let range = sender.representedObject as? NSRange,
|
||||
let textView = self.textView
|
||||
let textView = self.textView,
|
||||
let range = sender.representedObject as? NSRange
|
||||
else { return assertionFailure() }
|
||||
|
||||
textView.selectedRange = range
|
||||
textView.centerSelectionInVisibleArea(self)
|
||||
textView.window?.makeFirstResponder(textView)
|
||||
textView.select(range: range)
|
||||
}
|
||||
|
||||
|
||||
/// Select the previous outline menu item.
|
||||
@IBAction func selectPrevItemOfOutlineMenu(_ sender: Any?) {
|
||||
|
||||
guard let popUp = self.outlineMenu, self.canSelectPrevItem else { return }
|
||||
guard
|
||||
let textView = self.textView,
|
||||
let item = self.outlineItems.previousItem(for: textView.selectedRange)
|
||||
else { return }
|
||||
|
||||
let index = popUp.itemArray[..<popUp.indexOfSelectedItem]
|
||||
.lastIndex { $0.representedObject is NSRange } ?? 0
|
||||
|
||||
popUp.menu!.performActionForItem(at: index)
|
||||
textView.select(range: item.range)
|
||||
}
|
||||
|
||||
|
||||
/// Select the next outline menu item.
|
||||
@IBAction func selectNextItemOfOutlineMenu(_ sender: Any?) {
|
||||
|
||||
guard let popUp = self.outlineMenu, self.canSelectNextItem else { return }
|
||||
guard
|
||||
let textView = self.textView,
|
||||
let item = self.outlineItems.nextItem(for: textView.selectedRange)
|
||||
else { return }
|
||||
|
||||
let index = popUp.itemArray[(popUp.indexOfSelectedItem + 1)...]
|
||||
.firstIndex { $0.representedObject is NSRange }
|
||||
|
||||
if let index = index {
|
||||
popUp.menu!.performActionForItem(at: index)
|
||||
}
|
||||
textView.select(range: item.range)
|
||||
}
|
||||
|
||||
|
||||
|
@ -77,9 +77,44 @@ extension OutlineItem {
|
||||
|
||||
extension BidirectionalCollection where Element == OutlineItem {
|
||||
|
||||
func indexOfItem(for characterRange: NSRange, allowsSeparator: Bool = true) -> Index? {
|
||||
/// Return the index of element for the given range.
|
||||
///
|
||||
/// - Parameter range: The character range to refer.
|
||||
/// - Returns: The index of the corresponding outline item, or `nil` if not exist.
|
||||
func indexOfItem(at location: Int, allowsSeparator: Bool = false) -> Index? {
|
||||
|
||||
return self.lastIndex { $0.range.location <= characterRange.location && (allowsSeparator || !$0.isSeparator ) }
|
||||
return self.lastIndex { $0.range.location <= location && (allowsSeparator || !$0.isSeparator ) }
|
||||
}
|
||||
|
||||
|
||||
/// Return the previous non-separator element from the given range.
|
||||
///
|
||||
/// - Parameter range: The character range to refer.
|
||||
/// - Returns: The previous outline item, or `nil` if not exist.
|
||||
func previousItem(for range: NSRange) -> OutlineItem? {
|
||||
|
||||
guard let currentIndex = self.indexOfItem(at: range.lowerBound) else { return nil }
|
||||
|
||||
return self[..<currentIndex].last { !$0.isSeparator }
|
||||
}
|
||||
|
||||
|
||||
/// Return the next non-separator element from the given range.
|
||||
///
|
||||
/// - Parameter range: The character range to refer.
|
||||
/// - Returns: The next outline item, or `nil` if not exist.
|
||||
func nextItem(for range: NSRange) -> OutlineItem? {
|
||||
|
||||
if let first = self.first(where: { !$0.isSeparator }), range.upperBound < first.range.location {
|
||||
return first
|
||||
}
|
||||
|
||||
guard
|
||||
let currentIndex = self.indexOfItem(at: range.upperBound),
|
||||
currentIndex <= self.endIndex
|
||||
else { return nil }
|
||||
|
||||
return self[self.index(after: currentIndex)...].first { !$0.isSeparator }
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -174,9 +174,7 @@ final class OutlineViewController: NSViewController {
|
||||
textView.string.nsRange.upperBound >= item.range.upperBound
|
||||
else { return }
|
||||
|
||||
textView.selectedRange = item.range
|
||||
textView.scrollRangeToVisible(item.range)
|
||||
textView.showFindIndicator(for: item.range)
|
||||
textView.select(range: item.range)
|
||||
}
|
||||
|
||||
|
||||
@ -220,7 +218,7 @@ final class OutlineViewController: NSViewController {
|
||||
|
||||
guard
|
||||
let textView = textView ?? self.document?.textView,
|
||||
let row = self.outlineItems.indexOfItem(for: textView.selectedRange, allowsSeparator: false),
|
||||
let row = self.outlineItems.indexOfItem(at: textView.selectedRange.location),
|
||||
outlineView.numberOfRows > row
|
||||
else { return outlineView.deselectAll(nil) }
|
||||
|
||||
|
82
Tests/OutlineTests.swift
Normal file
82
Tests/OutlineTests.swift
Normal file
@ -0,0 +1,82 @@
|
||||
//
|
||||
// OutlineTests.swift
|
||||
//
|
||||
// CotEditor
|
||||
// https://coteditor.com
|
||||
//
|
||||
// Created by 1024jp on 2020-08-12.
|
||||
//
|
||||
// ---------------------------------------------------------------------------
|
||||
//
|
||||
// © 2020 1024jp
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// https://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import CotEditor
|
||||
|
||||
final class OutlineTests: XCTestCase {
|
||||
|
||||
private let items: [OutlineItem] = [
|
||||
OutlineItem(title: "a", range: NSRange(location: 10, length: 5)), // 0
|
||||
OutlineItem(title: .separator, range: NSRange(location: 20, length: 5)),
|
||||
OutlineItem(title: .separator, range: NSRange(location: 30, length: 5)),
|
||||
OutlineItem(title: "b", range: NSRange(location: 40, length: 5)), // 3
|
||||
OutlineItem(title: .separator, range: NSRange(location: 50, length: 5)),
|
||||
OutlineItem(title: "c", range: NSRange(location: 60, length: 5)), // 5
|
||||
OutlineItem(title: .separator, range: NSRange(location: 70, length: 5)),
|
||||
]
|
||||
|
||||
private let emptyItems: [OutlineItem] = []
|
||||
|
||||
|
||||
|
||||
func testIndex() throws {
|
||||
|
||||
XCTAssertNil(self.emptyItems.indexOfItem(at: 10))
|
||||
|
||||
XCTAssertNil(self.items.indexOfItem(at: 9))
|
||||
XCTAssertEqual(self.items.indexOfItem(at: 10), 0)
|
||||
XCTAssertEqual(self.items.indexOfItem(at: 18), 0)
|
||||
XCTAssertEqual(self.items.indexOfItem(at: 20), 0)
|
||||
XCTAssertEqual(self.items.indexOfItem(at: 40), 3)
|
||||
XCTAssertEqual(self.items.indexOfItem(at: 50), 3)
|
||||
XCTAssertEqual(self.items.indexOfItem(at: 59), 3)
|
||||
XCTAssertEqual(self.items.indexOfItem(at: 60), 5)
|
||||
}
|
||||
|
||||
|
||||
func testPreviousItem() throws {
|
||||
|
||||
XCTAssertNil(self.emptyItems.previousItem(for: NSRange(10..<20)))
|
||||
|
||||
XCTAssertNil(self.items.previousItem(for: NSRange(10..<20)))
|
||||
XCTAssertNil(self.items.previousItem(for: NSRange(19..<19)))
|
||||
XCTAssertEqual(self.items.previousItem(for: NSRange(59..<70)), items[0])
|
||||
XCTAssertEqual(self.items.previousItem(for: NSRange(60..<70)), items[3])
|
||||
}
|
||||
|
||||
|
||||
func testNextItem() throws {
|
||||
|
||||
XCTAssertNil(self.emptyItems.nextItem(for: NSRange(10..<20)))
|
||||
|
||||
XCTAssertEqual(self.items.nextItem(for: NSRange(0..<0)), items[0])
|
||||
XCTAssertEqual(self.items.nextItem(for: NSRange(0..<10)), items[3])
|
||||
XCTAssertEqual(self.items.nextItem(for: NSRange(40..<40)), items[5])
|
||||
XCTAssertNil(self.items.nextItem(for: NSRange(60..<60)))
|
||||
XCTAssertNil(self.items.nextItem(for: NSRange(40..<61)))
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue
Block a user