Count all element in multiple selections (close #978)

This commit is contained in:
1024jp 2023-03-13 19:35:41 +09:00
parent 25d5c0e04a
commit bd63200f2e
8 changed files with 85 additions and 27 deletions

View File

@ -7,6 +7,7 @@ Change Log
### Improvements
- Change counting characters/lines/words to count all elements in multiple selections.
- Change the threshold to trigger the automatic completion to 3 letters or more to optimize calculation time by large documents.
- Allow `_` as a character for the automatic completion candidates.
@ -16,6 +17,7 @@ Change Log
- Fix an issue that the application did not terminate when no documents exist and the application goes background.
4.5.0-beta.2 (555)
--------------------------

View File

@ -90,9 +90,16 @@ import AppKit
.merge(with: self.setting.objectWillChange)
.merge(with: Just(Void())) // initial calculation
.receive(on: DispatchQueue.main)
.compactMap { [weak self] in self?.textView.selectedString }
.compactMap { [weak self] in self?.textView.selectedStrings }
.receive(on: DispatchQueue.global())
.map { [unowned self] in $0.count(options: self.setting.options) }
.map { [unowned self] (strings) in
strings
.map { $0.count(options: self.setting.options) }
.reduce(0) { (total, count) in
guard let total, let count else { return nil }
return total + count
}
}
.receive(on: DispatchQueue.main)
.assign(to: &self.$selectionCount)
}

View File

@ -9,7 +9,7 @@
// ---------------------------------------------------------------------------
//
// © 2004-2007 nakamuxu
// © 2014-2022 1024jp
// © 2014-2023 1024jp
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@ -26,6 +26,9 @@
import Cocoa
extension NSValue: @unchecked Sendable { }
final class DocumentAnalyzer {
// MARK: Public Properties
@ -91,12 +94,13 @@ final class DocumentAnalyzer {
try await Task.sleep(nanoseconds: delay * 1_000_000) // debounce
let string = await textView.string.immutable
let selectedRange = await textView.selectedRange
let selectedRanges = await textView.selectedRanges
.map(\.rangeValue)
.compactMap { Range($0, in: string) }
let counter = EditorCounter(string: string,
selectedRange: Range(selectedRange, in: string) ?? string.startIndex..<string.startIndex,
requiredInfo: self.requiredInfoTypes,
countsWholeText: self.needsCountWholeText)
guard !selectedRanges.isEmpty else { return assertionFailure() }
let counter = EditorCounter(string: string, selectedRanges: selectedRanges, requiredInfo: self.requiredInfoTypes, countsWholeText: self.needsCountWholeText)
var result = try await counter.count()

View File

@ -8,7 +8,7 @@
//
// ---------------------------------------------------------------------------
//
// © 2014-2022 1024jp
// © 2014-2023 1024jp
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@ -82,7 +82,7 @@ final actor EditorCounter {
// MARK: Private Properties
let string: String
let selectedRange: Range<String.Index>
let selectedRanges: [Range<String.Index>]
let requiredInfo: EditorInfoTypes
let countsWholeText: Bool
@ -92,12 +92,12 @@ final actor EditorCounter {
// MARK: -
// MARK: Lifecycle
init(string: String, selectedRange: Range<String.Index>, requiredInfo: EditorInfoTypes, countsWholeText: Bool) {
init(string: String, selectedRanges: [Range<String.Index>], requiredInfo: EditorInfoTypes, countsWholeText: Bool) {
assert(selectedRange.upperBound <= string.endIndex)
assert(selectedRanges.allSatisfy { $0.upperBound <= string.endIndex })
self.string = string
self.selectedRange = selectedRange
self.selectedRanges = selectedRanges
self.requiredInfo = requiredInfo
self.countsWholeText = countsWholeText
}
@ -110,7 +110,7 @@ final actor EditorCounter {
var result = EditorCountResult()
let selectedString = self.string[self.selectedRange]
let selectedStrings = self.selectedRanges.map { self.string[$0] }
if self.countsWholeText {
if self.requiredInfo.contains(.characters) {
@ -131,38 +131,38 @@ final actor EditorCounter {
if self.requiredInfo.contains(.characters) {
try Task.checkCancellation()
result.characters.selected = selectedString.count
result.characters.selected = selectedStrings.map(\.count).reduce(0, +)
}
if self.requiredInfo.contains(.lines) {
try Task.checkCancellation()
result.lines.selected = selectedString.numberOfLines
result.lines.selected = self.string.numberOfLines(in: self.selectedRanges)
}
if self.requiredInfo.contains(.words) {
try Task.checkCancellation()
result.words.selected = selectedString.numberOfWords
result.words.selected = selectedStrings.map(\.numberOfWords).reduce(0, +)
}
if self.requiredInfo.contains(.location) {
try Task.checkCancellation()
result.location = self.string.distance(from: self.string.startIndex,
to: self.selectedRange.lowerBound)
to: self.selectedRanges[0].lowerBound)
}
if self.requiredInfo.contains(.line) {
try Task.checkCancellation()
result.line = self.string.lineNumber(at: self.selectedRange.lowerBound)
result.line = self.string.lineNumber(at: self.selectedRanges[0].lowerBound)
}
if self.requiredInfo.contains(.column) {
try Task.checkCancellation()
result.column = self.string.columnNumber(at: self.selectedRange.lowerBound)
result.column = self.string.columnNumber(at: self.selectedRanges[0].lowerBound)
}
if self.requiredInfo.contains(.unicode) {
result.unicode = (selectedString.compareCount(with: 1) == .equal)
? selectedString.first?.unicodeScalars.map(\.codePoint).joined(separator: ", ")
result.unicode = (selectedStrings[0].compareCount(with: 1) == .equal)
? selectedStrings[0].first?.unicodeScalars.map(\.codePoint).joined(separator: ", ")
: nil
}

View File

@ -34,6 +34,15 @@ extension NSTextView {
}
/// The selected strings.
var selectedStrings: [String] {
self.selectedRanges
.map(\.rangeValue)
.map { (self.string as NSString).substring(with: $0) }
}
/// character just before the given range
func character(before range: NSRange) -> Unicode.Scalar? {

View File

@ -81,6 +81,38 @@ extension StringProtocol {
}
/// Count the number of lines in the given ranges including the last blank line.
///
/// - Parameter ranges: The character ranges to count lines.
/// - Returns: The number of lines.
func numberOfLines(in ranges: [Range<String.Index>]) -> Int {
assert(!ranges.isEmpty)
if self.isEmpty || ranges.isEmpty { return 0 }
// use simple count for efficiency
if ranges.count == 1 {
return self.numberOfLines(in: ranges[0])
}
// evaluate line ranges to avoid double-count lines holding multiple ranges
var lineRanges: [Range<String.Index>] = []
for range in ranges {
let lineRange = self.lineRange(for: range)
self.enumerateSubstrings(in: lineRange, options: [.byLines, .substringNotRequired]) { (_, substringRange, _, _) in
lineRanges.append(substringRange)
}
if self[range].last?.isNewline == true {
lineRanges.append(self.lineRange(at: range.upperBound))
}
}
return lineRanges.unique.count
}
/// Calculate the number of characters from the beginning of the line where the given character index locates (0-based).
///
/// - Parameter index: The character index.

View File

@ -8,7 +8,7 @@
//
// ---------------------------------------------------------------------------
//
// © 2020-2022 1024jp
// © 2020-2023 1024jp
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@ -39,7 +39,7 @@ final class EditorInfoCountOperationTests: XCTestCase {
let selectedRange = Range(NSRange(0..<3), in: self.testString)!
let counter = EditorCounter(
string: self.testString,
selectedRange: selectedRange,
selectedRanges: [selectedRange],
requiredInfo: [],
countsWholeText: true)
@ -59,7 +59,7 @@ final class EditorInfoCountOperationTests: XCTestCase {
let selectedRange = Range(NSRange(11..<21), in: self.testString)!
let counter = EditorCounter(
string: self.testString,
selectedRange: selectedRange,
selectedRanges: [selectedRange],
requiredInfo: .all,
countsWholeText: true)
@ -84,7 +84,7 @@ final class EditorInfoCountOperationTests: XCTestCase {
let selectedRange = Range(NSRange(11..<21), in: self.testString)!
let counter = EditorCounter(
string: self.testString,
selectedRange: selectedRange,
selectedRanges: [selectedRange],
requiredInfo: .all,
countsWholeText: false)
@ -110,7 +110,7 @@ final class EditorInfoCountOperationTests: XCTestCase {
let selectedRange = Range(NSRange(1..<4), in: string)!
let counter = EditorCounter(
string: string,
selectedRange: selectedRange,
selectedRanges: [selectedRange],
requiredInfo: .all,
countsWholeText: true)

View File

@ -136,6 +136,10 @@ final class StringExtensionsTests: XCTestCase {
XCTAssertEqual("a\u{FEFF}\nb".numberOfLines, 2)
XCTAssertEqual("\u{FEFF}\nb".numberOfLines, 2)
XCTAssertEqual("\u{FEFF}0000000000000000".numberOfLines, 1)
let bomString = "\u{FEFF}\nb"
let range = bomString.startIndex..<bomString.index(bomString.startIndex, offsetBy: 2)
XCTAssertEqual(bomString.numberOfLines(in: [range, range]), 2) // "\u{FEFF}\nb"
}