Skip to content

Commit

Permalink
Extract EditedRangeSet to EditorCore package
Browse files Browse the repository at this point in the history
  • Loading branch information
1024jp committed Jul 10, 2024
1 parent ecbe7d6 commit 082966f
Show file tree
Hide file tree
Showing 7 changed files with 122 additions and 40 deletions.
10 changes: 0 additions & 10 deletions CotEditor.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -412,9 +412,6 @@
2A8DA9451D286C53003D0C4B /* ScriptManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A8DA9431D286C53003D0C4B /* ScriptManager.swift */; };
2A8DA9471D28ED93003D0C4B /* URL.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A8DA9461D28ED93003D0C4B /* URL.swift */; };
2A8DA9481D28ED93003D0C4B /* URL.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A8DA9461D28ED93003D0C4B /* URL.swift */; };
2A8E47E2299A2314006A40D8 /* EditedRangeSet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A8E47E1299A2314006A40D8 /* EditedRangeSet.swift */; };
2A8E47E3299A2314006A40D8 /* EditedRangeSet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A8E47E1299A2314006A40D8 /* EditedRangeSet.swift */; };
2A8E47E5299A2401006A40D8 /* EditedRangeSetTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A8E47E4299A2401006A40D8 /* EditedRangeSetTests.swift */; };
2A8EAE3E2BA3B15D00448875 /* Credits.json in Resources */ = {isa = PBXBuildFile; fileRef = 2A8EAE3D2BA3B15D00448875 /* Credits.json */; };
2A8EAE3F2BA3B15D00448875 /* Credits.json in Resources */ = {isa = PBXBuildFile; fileRef = 2A8EAE3D2BA3B15D00448875 /* Credits.json */; };
2A8EAE412BA3C3DC00448875 /* AboutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A8EAE402BA3C3DC00448875 /* AboutView.swift */; };
Expand Down Expand Up @@ -1007,8 +1004,6 @@
2A8961911DB76A3400E9E0EC /* MainMenu.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MainMenu.swift; sourceTree = "<group>"; };
2A8DA9431D286C53003D0C4B /* ScriptManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ScriptManager.swift; sourceTree = "<group>"; };
2A8DA9461D28ED93003D0C4B /* URL.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URL.swift; sourceTree = "<group>"; };
2A8E47E1299A2314006A40D8 /* EditedRangeSet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditedRangeSet.swift; sourceTree = "<group>"; };
2A8E47E4299A2401006A40D8 /* EditedRangeSetTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditedRangeSetTests.swift; sourceTree = "<group>"; };
2A8EAE3D2BA3B15D00448875 /* Credits.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = Credits.json; sourceTree = "<group>"; };
2A8EAE402BA3C3DC00448875 /* AboutView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutView.swift; sourceTree = "<group>"; };
2A8EAE552BA7E2BE00448875 /* Licenses */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Licenses; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1335,7 +1330,6 @@
children = (
2A3E61C627C4962B00C6E5B6 /* Formatters */,
2AC186DC1E2F4264002F4D27 /* Debug.swift */,
2A8E47E1299A2314006A40D8 /* EditedRangeSet.swift */,
2AA704CD2987878B008CBCB5 /* Node.swift */,
2A11F2121E669BFA005E1675 /* PointerBridge.swift */,
2AA4EE3C28D55CE80014B045 /* DelegateContext.swift */,
Expand Down Expand Up @@ -2086,7 +2080,6 @@
2ABEFB6923DC0CA0008769F4 /* EditorCounterTests.swift */,
2A80BE8F27FFFA8900D2F7FF /* LineEndingScannerTests.swift */,
2A1125C023F180FF006A1DB2 /* LineRangeCacheableTests.swift */,
2A8E47E4299A2401006A40D8 /* EditedRangeSetTests.swift */,
2A3F8F672429E04000CBBA89 /* DebouncerTests.swift */,
2A04E9BA27FD6911008C82D8 /* SnippetTests.swift */,
2A57B991294EDD9600771696 /* FormatStylesTests.swift */,
Expand Down Expand Up @@ -2690,7 +2683,6 @@
2AE144BC2B01E341005E8CF1 /* DonationSettingsView.swift in Sources */,
2ACDC0921D1726BD009B72D6 /* DotView.swift in Sources */,
2A68722F288A5C44006D6B41 /* DraggableHostingView.swift in Sources */,
2A8E47E2299A2314006A40D8 /* EditedRangeSet.swift in Sources */,
2AD7B9B01D3E832E00E5D6D7 /* EditorCounter.swift in Sources */,
2A158C222945F54B000A4EC1 /* EditorOpacityView.swift in Sources */,
2AEC69C51D41A1BE0089F96F /* EditorTextView.swift in Sources */,
Expand Down Expand Up @@ -2913,7 +2905,6 @@
2AC39F731E8AC80E009F97D5 /* CollectionTests.swift in Sources */,
2A2E56D72C018ADB00416F9E /* ComparableTests.swift in Sources */,
2A3F8F682429E04000CBBA89 /* DebouncerTests.swift in Sources */,
2A8E47E5299A2401006A40D8 /* EditedRangeSetTests.swift in Sources */,
2ABEFB6A23DC0CA0008769F4 /* EditorCounterTests.swift in Sources */,
2A4D69291D40032300FBBD0B /* EncodingTests.swift in Sources */,
2AC72EA2253478D5001D3CA0 /* FileDropItemTests.swift in Sources */,
Expand Down Expand Up @@ -2998,7 +2989,6 @@
2AE144BD2B01E341005E8CF1 /* DonationSettingsView.swift in Sources */,
2ACDC0911D1726BD009B72D6 /* DotView.swift in Sources */,
2A687230288A5C44006D6B41 /* DraggableHostingView.swift in Sources */,
2A8E47E3299A2314006A40D8 /* EditedRangeSet.swift in Sources */,
2AD7B9AF1D3E832E00E5D6D7 /* EditorCounter.swift in Sources */,
2A158C232945F54B000A4EC1 /* EditorOpacityView.swift in Sources */,
2AEC69C41D41A1BE0089F96F /* EditorTextView.swift in Sources */,
Expand Down
5 changes: 3 additions & 2 deletions CotEditor/Sources/URLDetector.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
//

import AppKit.NSTextStorage
import EditedRangeSet
import StringBasics
import ValueRange

Expand All @@ -32,7 +33,7 @@ import ValueRange
// MARK: Private Properties

private let textStorage: NSTextStorage
private var editedRanges = EditedRangeSet()
private var editedRanges: EditedRangeSet
private let delay: Duration = .seconds(0.5)

private var textEditingObserver: (any NSObjectProtocol)?
Expand All @@ -45,7 +46,7 @@ import ValueRange

self.textStorage = textStorage

self.editedRanges.append(editedRange: textStorage.range)
self.editedRanges = EditedRangeSet(range: textStorage.range)
self.textEditingObserver = self.observeTextStorage(textStorage)
self.task = Task { try await self.detectInvalidRanges() }
}
Expand Down
5 changes: 5 additions & 0 deletions Packages/EditorCore/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ let package = Package(
.library(name: "EditorCore", targets: [
"CharacterInfo",
"Defaults",
"EditedRangeSet",
"FileEncoding",
"FilePermissions",
"FuzzyRange",
Expand All @@ -30,6 +31,7 @@ let package = Package(

.library(name: "CharacterInfo", targets: ["CharacterInfo"]),
.library(name: "Defaults", targets: ["Defaults"]),
.library(name: "EditedRangeSet", targets: ["EditedRangeSet"]),
.library(name: "FileEncoding", targets: ["FileEncoding"]),
.library(name: "FilePermissions", targets: ["FilePermissions"]),
.library(name: "FuzzyRange", targets: ["FuzzyRange"]),
Expand All @@ -53,6 +55,9 @@ let package = Package(
.target(name: "Defaults"),
.testTarget(name: "DefaultsTests", dependencies: ["Defaults"]),

.target(name: "EditedRangeSet", dependencies: ["StringBasics"]),
.testTarget(name: "EditedRangeSetTests", dependencies: ["EditedRangeSet"]),

.target(name: "FileEncoding", dependencies: ["ValueRange"], resources: [.process("Resources")]),
.testTarget(name: "FileEncodingTests", dependencies: ["FileEncoding"], resources: [.process("Resources")]),

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
//
// EditedRangeSet.swift
// EditedRangeSet
//
// CotEditor
// https://coteditor.com
Expand All @@ -8,7 +9,7 @@
//
// ---------------------------------------------------------------------------
//
// © 2023 1024jp
// © 2023-2024 1024jp
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
Expand All @@ -24,29 +25,44 @@
//

import Foundation
import StringBasics

/// Edited range storage to postpone validations.
///
/// This is similar to the IndexSet but preserving zero-width edited ranges.
struct EditedRangeSet {
public struct EditedRangeSet: Sendable {

private(set) var ranges: [NSRange] = []
public private(set) var ranges: [NSRange] = []


public init(range: NSRange? = nil) {

if let range {
self.ranges = [range]
}
}


/// The range that contains all ranges.
var range: NSRange? {
public var range: NSRange? {

self.ranges.union
}


/// Clears all stored ranges.
public mutating func clear() {

self.ranges.removeAll()
}


/// Updates edit ranges by assuming the string was edited in an NSTextStorage and the storage returns the given `editedRange` and `changeInLength`.
///
/// - Parameters:
/// - editedRange: The current remained range where edited.
/// - changeInLength: The difference between the current length of the edited range and its length before editing.
mutating func append(editedRange: NSRange, changeInLength: Int = 0) {
public mutating func append(editedRange: NSRange, changeInLength: Int) {

assert(editedRange.location != NSNotFound)

Expand Down Expand Up @@ -79,26 +95,4 @@ struct EditedRangeSet {
self.ranges.insert(editedRange, at: index)
}
}


/// Clears all stored ranges.
mutating func clear() {

self.ranges.removeAll()
}
}


private extension Sequence<NSRange> {

/// The range that contains all ranges.
var union: NSRange? {

guard
let lowerBound = self.map(\.lowerBound).min(),
let upperBound = self.map(\.upperBound).max()
else { return nil }

return NSRange(lowerBound..<upperBound)
}
}
44 changes: 44 additions & 0 deletions Packages/EditorCore/Sources/EditedRangeSet/NSRange.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
//
// NSRange.swift
// EditedRangeSet
//
// CotEditor
// https://coteditor.com
//
// Created by 1024jp on 2024-07-10.
//
// ---------------------------------------------------------------------------
//
// © 2024 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 Foundation
import StringBasics

extension Sequence<NSRange> {

/// The range that contains all ranges.
var union: NSRange? {

let ranges = self.filter { !$0.isNotFound }

guard
let lowerBound = ranges.map(\.lowerBound).min(),
let upperBound = ranges.map(\.upperBound).max()
else { return nil }

return NSRange(lowerBound..<upperBound)
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
//
// EditedRangeSetTests.swift
// EditedRangeSetTests
//
// CotEditor
// https://coteditor.com
Expand All @@ -26,7 +27,7 @@
import AppKit.NSTextStorage
import Combine
import Testing
@testable import CotEditor
@testable import EditedRangeSet

struct EditedRangeSetTests {

Expand Down
47 changes: 47 additions & 0 deletions Packages/EditorCore/Tests/EditedRangeSetTests/NSRangeTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
//
// NSRangeTests.swift
// EditedRangeSetTests
//
// CotEditor
// https://coteditor.com
//
// Created by 1024jp on 2024-07-10.
//
// ---------------------------------------------------------------------------
//
// © 2024 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 Foundation
import Testing
@testable import EditedRangeSet

struct NSRangeTests {

@Test func union() throws {

#expect([NSRange]().union == nil)
#expect([NSRange(location: NSNotFound, length: 0)].union == nil)
#expect([NSRange(0..<0)].union == NSRange(0..<0))
#expect([NSRange(1..<1)].union == NSRange(1..<1))
#expect([NSRange(1..<3)].union == NSRange(1..<3))

#expect([NSRange(1..<3), NSRange(2..<4)].union == NSRange(1..<4))
#expect([NSRange(1..<3), NSRange(5..<9)].union == NSRange(1..<9))
#expect([NSRange(5..<9), NSRange(1..<3)].union == NSRange(1..<9))
#expect([NSRange(5..<100), NSRange(1..<3), NSRange(2..<3)].union == NSRange(1..<100))
#expect([NSRange(5..<9), NSRange(location: NSNotFound, length: 0), NSRange(1..<3)].union == NSRange(1..<9))
}
}

0 comments on commit 082966f

Please sign in to comment.