Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commiteb1d382

Browse files
Fix Marked Text Input (#40)
### DescriptionFixes marked text input for sequences longer than one marked character. Also ensures marked text works consistently with multiple cursors and adds testing for the marked text functionality.Also:- Fixes a few lint markers in test files that have caused issues for others.- Adds a public `TextView.markedTextAttributes` property for modifying the marked text attributes if desired.### Related Issues*closes#37 *closes#36 *closes#26 *closesCodeEditApp/CodeEditSourceEditor#188### Screenshotshttps://github.com/CodeEditApp/CodeEditTextView/assets/35942988/9f6eb84b-c668-45a4-9d30-75cbd5d4fccd
1 parent40458fe commiteb1d382

File tree

7 files changed

+211
-59
lines changed

7 files changed

+211
-59
lines changed

‎.swiftlint.yml‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
excluded:
2+
-.build
3+
14
disabled_rules:
25
-todo
36
-trailing_comma
@@ -13,4 +16,4 @@ identifier_name:
1316
excluded:
1417
-c
1518
-id
16-
-vc
19+
-vc

‎Sources/CodeEditTextView/MarkedTextManager/MarkedTextManager.swift‎

Lines changed: 29 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,10 @@
77

88
import AppKit
99

10-
/// Manages marked ranges
10+
/// Manages marked ranges. Not a public API.
1111
classMarkedTextManager{
12+
/// Struct for passing attribute and range information easily down into line fragments, typesetters w/o
13+
/// requiring a reference to the marked text manager.
1214
structMarkedRanges{
1315
letranges:[NSRange]
1416
letattributes:[NSAttributedString.Key:Any]
@@ -18,7 +20,9 @@ class MarkedTextManager {
1820
private(set)varmarkedRanges:[NSRange]=[]
1921

2022
/// The attributes to use for marked text. Defaults to a single underline when `nil`
21-
varmarkedTextAttributes:[NSAttributedString.Key:Any]?
23+
varmarkedTextAttributes:[NSAttributedString.Key:Any]=[
24+
.underlineStyle:NSUnderlineStyle.single.rawValue
25+
]
2226

2327
/// True if there is marked text being tracked.
2428
varhasMarkedText:Bool{
@@ -31,32 +35,40 @@ class MarkedTextManager {
3135
}
3236

3337
/// Updates the stored marked ranges.
38+
///
39+
/// Two cases here:
40+
/// - No marked ranges yet:
41+
/// - Create new marked ranges from the text selection, with the length of the text being inserted
42+
/// - Marked ranges exist:
43+
/// - Update the existing marked ranges, using the original ranges as a reference. The marked ranges don't
44+
/// change position, so we update each one with the new length and then move it to reflect each cursor's
45+
/// added text.
46+
///
3447
/// - Parameters:
3548
/// - insertLength: The length of the string being inserted.
36-
/// - replacementRange: The range to replace with marked text.
37-
/// - selectedRange: The selected range from `NSTextInput`.
3849
/// - textSelections: The current text selections.
39-
func updateMarkedRanges(
40-
insertLength:Int,
41-
replacementRange:NSRange,
42-
selectedRange:NSRange,
43-
textSelections:[TextSelectionManager.TextSelection]
44-
){
45-
if replacementRange.location== NSNotFound{
46-
markedRanges= textSelections.map{
47-
NSRange(location: $0.range.location, length: insertLength)
48-
}
50+
func updateMarkedRanges(insertLength:Int, textSelections:[NSRange]){
51+
varcumulativeExistingDiff=0
52+
letlengthDiff= insertLength
53+
varnewRanges=[NSRange]()
54+
letranges:[NSRange]=if markedRanges.isEmpty{
55+
textSelections.sorted(by:{ $0.location< $1.location})
4956
}else{
50-
markedRanges=[selectedRange]
57+
markedRanges.sorted(by:{ $0.location< $1.location})
58+
}
59+
60+
for(idx, range)in ranges.enumerated(){
61+
newRanges.append(NSRange(location: range.location+ cumulativeExistingDiff, length: insertLength))
62+
cumulativeExistingDiff+= insertLength- range.length
5163
}
64+
markedRanges= newRanges
5265
}
5366

5467
/// Finds any marked ranges for a line and returns them.
5568
/// - Parameter lineRange: The range of the line.
5669
/// - Returns: A `MarkedRange` struct with information about attributes and ranges. `nil` if there is no marked
5770
/// text for this line.
5871
func markedRanges(in lineRange:NSRange)->MarkedRanges?{
59-
letattributes= markedTextAttributes??[.underlineStyle:NSUnderlineStyle.single.rawValue]
6072
letranges= markedRanges.compactMap{
6173
$0.intersection(lineRange)
6274
}.map{
@@ -65,7 +77,7 @@ class MarkedTextManager {
6577
if ranges.isEmpty{
6678
returnnil
6779
}else{
68-
returnMarkedRanges(ranges: ranges, attributes:attributes)
80+
returnMarkedRanges(ranges: ranges, attributes:markedTextAttributes)
6981
}
7082
}
7183

‎Sources/CodeEditTextView/TextView/TextView+NSTextInput.swift‎

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -80,13 +80,10 @@ extension TextView: NSTextInputClient {
8080

8181
// MARK: - Marked Text
8282

83-
///Replaces a specified range in the receiver’stextstorage with the given string and sets the selection.
83+
///Sets up markedtextfor a marking session. See ``MarkedTextManager`` for more details.
8484
///
85-
/// If there is no marked text, the current selection is replaced. If there is no selection, the string is
86-
/// inserted at the insertion point.
87-
///
88-
/// When `string` is an `NSString` object, the receiver is expected to render the marked text with
89-
/// distinguishing appearance (for example, `NSTextView` renders with `markedTextAttributes`).
85+
/// Decides whether or not to insert/replace text. Then updates the current marked ranges and updates cursor
86+
/// positions.
9087
///
9188
/// - Parameters:
9289
/// - string: The string to insert. Can be either an NSString or NSAttributedString instance.
@@ -96,13 +93,26 @@ extension TextView: NSTextInputClient {
9693
guard isEditable,let insertString=anyToString(string)else{return}
9794
// Needs to insert text, but not notify the undo manager.
9895
_undoManager?.disable()
96+
letshouldInsert= layoutManager.markedTextManager.markedRanges.isEmpty
97+
98+
// Copy the text selections *before* we modify them.
99+
letselectionCopies= selectionManager.textSelections.map(\.range)
100+
101+
if shouldInsert{
102+
_insertText(insertString: insertString, replacementRange: replacementRange)
103+
}else{
104+
replaceCharacters(in: layoutManager.markedTextManager.markedRanges, with: insertString)
105+
}
99106
layoutManager.markedTextManager.updateMarkedRanges(
100107
insertLength:(insertStringasNSString).length,
101-
replacementRange: replacementRange,
102-
selectedRange: selectedRange,
103-
textSelections: selectionManager.textSelections
108+
textSelections: selectionCopies
104109
)
105-
_insertText(insertString: insertString, replacementRange: replacementRange)
110+
111+
// Reset the selected ranges to reflect the replaced text.
112+
selectionManager.setSelectedRanges(layoutManager.markedTextManager.markedRanges.map({
113+
NSRange(location: $0.max, length:0)
114+
}))
115+
106116
_undoManager?.enable()
107117
}
108118

‎Sources/CodeEditTextView/TextView/TextView.swift‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,18 @@ public class TextView: NSView, NSTextContent {
198198
}
199199
}
200200

201+
/// The attributes used to render marked text.
202+
/// Defaults to a single underline.
203+
publicvarmarkedTextAttributes:[NSAttributedString.Key:Any]{
204+
get{
205+
layoutManager.markedTextManager.markedTextAttributes
206+
}
207+
set{
208+
layoutManager.markedTextManager.markedTextAttributes= newValue
209+
layoutManager.layoutLines() // Layout lines to refresh attributes. This should be rare.
210+
}
211+
}
212+
201213
openvarcontentType:NSTextContentType?
202214

203215
/// The text view's delegate.
Lines changed: 18 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
import XCTest
22
@testableimport CodeEditTextView
33

4-
// swiftlint:disable all
5-
64
classLineEndingTests:XCTestCase{
75
func test_lineEndingCreateUnix(){
86
// The \n character
@@ -29,64 +27,57 @@ class LineEndingTests: XCTestCase {
2927
}
3028

3129
func test_detectLineEndingDefault(){
32-
// There was a bug in this that caused it to flake sometimes, so we run this a couple times to ensure it's not flaky.
30+
// There was a bug in this that caused it to flake sometimes, so we run this a couple times to ensure it's not
31+
// flaky.
3332
// The odds of it being bad with the earlier bug after running 20 times is incredibly small
3433
for_in0..<20{
3534
letstorage=NSTextStorage(string:"hello world") // No line ending
3635
letlineStorage=TextLineStorage<TextLine>()
3736
lineStorage.buildFromTextStorage(storage, estimatedLineHeight:10)
3837
letdetected=LineEnding.detectLineEnding(lineStorage: lineStorage, textStorage: storage)
39-
XCTAssertTrue(detected==.lineFeed,"Default detected line ending incorrect, expected:\n, got:\(detected.rawValue.debugDescription)")
38+
XCTAssertEqual(detected,.lineFeed)
39+
}
40+
}
41+
42+
letcorpus="abcdefghijklmnopqrstuvwxyz123456789"
43+
func makeRandomText(_ goalLineEnding:LineEnding)->String{
44+
(10..<Int.random(in:20..<100)).reduce(""){ partialResult, _in
45+
return partialResult+ String(
46+
(0..<Int.random(in:1..<20)).map{ _in corpus.randomElement()!}
47+
)+ goalLineEnding.rawValue
4048
}
4149
}
4250

4351
func test_detectLineEndingUnix(){
44-
letcorpus="abcdefghijklmnopqrstuvwxyz123456789"
4552
letgoalLineEnding=LineEnding.lineFeed
4653

47-
lettext=(10..<Int.random(in:20..<100)).reduce(""){ partialResult, _in
48-
return partialResult+ String((0..<Int.random(in:1..<20)).map{ _in corpus.randomElement()!})+ goalLineEnding.rawValue
49-
}
50-
51-
letstorage=NSTextStorage(string: text)
54+
letstorage=NSTextStorage(string:makeRandomText(goalLineEnding))
5255
letlineStorage=TextLineStorage<TextLine>()
5356
lineStorage.buildFromTextStorage(storage, estimatedLineHeight:10)
5457

5558
letdetected=LineEnding.detectLineEnding(lineStorage: lineStorage, textStorage: storage)
56-
XCTAssertTrue(detected== goalLineEnding,"Incorrect detected line ending, expected:\(goalLineEnding.rawValue.debugDescription), got\(detected.rawValue.debugDescription)")
59+
XCTAssertEqual(detected,goalLineEnding)
5760
}
5861

5962
func test_detectLineEndingCLRF(){
60-
letcorpus="abcdefghijklmnopqrstuvwxyz123456789"
6163
letgoalLineEnding=LineEnding.carriageReturnLineFeed
6264

63-
lettext=(10..<Int.random(in:20..<100)).reduce(""){ partialResult, _in
64-
return partialResult+ String((0..<Int.random(in:1..<20)).map{ _in corpus.randomElement()!})+ goalLineEnding.rawValue
65-
}
66-
67-
letstorage=NSTextStorage(string: text)
65+
letstorage=NSTextStorage(string:makeRandomText(goalLineEnding))
6866
letlineStorage=TextLineStorage<TextLine>()
6967
lineStorage.buildFromTextStorage(storage, estimatedLineHeight:10)
7068

7169
letdetected=LineEnding.detectLineEnding(lineStorage: lineStorage, textStorage: storage)
72-
XCTAssertTrue(detected== goalLineEnding,"Incorrect detected line ending, expected:\(goalLineEnding.rawValue.debugDescription), got\(detected.rawValue.debugDescription)")
70+
XCTAssertEqual(detected,goalLineEnding)
7371
}
7472

7573
func test_detectLineEndingMacOS(){
76-
letcorpus="abcdefghijklmnopqrstuvwxyz123456789"
7774
letgoalLineEnding=LineEnding.carriageReturn
7875

79-
lettext=(10..<Int.random(in:20..<100)).reduce(""){ partialResult, _in
80-
return partialResult+ String((0..<Int.random(in:1..<20)).map{ _in corpus.randomElement()!})+ goalLineEnding.rawValue
81-
}
82-
83-
letstorage=NSTextStorage(string: text)
76+
letstorage=NSTextStorage(string:makeRandomText(goalLineEnding))
8477
letlineStorage=TextLineStorage<TextLine>()
8578
lineStorage.buildFromTextStorage(storage, estimatedLineHeight:10)
8679

8780
letdetected=LineEnding.detectLineEnding(lineStorage: lineStorage, textStorage: storage)
88-
XCTAssertTrue(detected== goalLineEnding,"Incorrect detected line ending, expected:\(goalLineEnding.rawValue.debugDescription), got\(detected.rawValue.debugDescription)")
81+
XCTAssertEqual(detected,goalLineEnding)
8982
}
9083
}
91-
92-
// swiftlint:enable all
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
import XCTest
2+
@testableimport CodeEditTextView
3+
4+
classMarkedTextTests:XCTestCase{
5+
func test_markedTextSingleChar(){
6+
lettextView=TextView(string:"")
7+
textView.selectionManager.setSelectedRange(.zero)
8+
9+
textView.setMarkedText("´", selectedRange:.notFound, replacementRange:.notFound)
10+
XCTAssertEqual(textView.string,"´")
11+
12+
textView.insertText("é", replacementRange:.notFound)
13+
XCTAssertEqual(textView.string,"é")
14+
XCTAssertEqual(textView.selectionManager.textSelections.map(\.range),[NSRange(location:1, length:0)])
15+
}
16+
17+
func test_markedTextSingleCharInStrings(){
18+
lettextView=TextView(string:"Lorem Ipsum")
19+
textView.selectionManager.setSelectedRange(NSRange(location:5, length:0))
20+
21+
textView.setMarkedText("´", selectedRange:.notFound, replacementRange:.notFound)
22+
XCTAssertEqual(textView.string,"Lorem´ Ipsum")
23+
24+
textView.insertText("é", replacementRange:.notFound)
25+
XCTAssertEqual(textView.string,"Loremé Ipsum")
26+
XCTAssertEqual(textView.selectionManager.textSelections.map(\.range),[NSRange(location:6, length:0)])
27+
}
28+
29+
func test_markedTextReplaceSelection(){
30+
lettextView=TextView(string:"ABCDE")
31+
textView.selectionManager.setSelectedRange(NSRange(location:4, length:1))
32+
33+
textView.setMarkedText("´", selectedRange:.notFound, replacementRange:.notFound)
34+
XCTAssertEqual(textView.string,"ABCD´")
35+
36+
textView.insertText("é", replacementRange:.notFound)
37+
XCTAssertEqual(textView.string,"ABCDé")
38+
XCTAssertEqual(textView.selectionManager.textSelections.map(\.range),[NSRange(location:5, length:0)])
39+
}
40+
41+
func test_markedTextMultipleSelection(){
42+
lettextView=TextView(string:"ABC")
43+
textView.selectionManager.setSelectedRanges([NSRange(location:1, length:0),NSRange(location:2, length:0)])
44+
45+
textView.setMarkedText("´", selectedRange:.notFound, replacementRange:.notFound)
46+
XCTAssertEqual(textView.string,"A´B´C")
47+
48+
textView.insertText("é", replacementRange:.notFound)
49+
XCTAssertEqual(textView.string,"AéBéC")
50+
XCTAssertEqual(
51+
textView.selectionManager.textSelections.map(\.range).sorted(by:{ $0.location< $1.location}),
52+
[NSRange(location:2, length:0),NSRange(location:4, length:0)]
53+
)
54+
}
55+
56+
func test_markedTextMultipleSelectionReplaceSelection(){
57+
lettextView=TextView(string:"ABCDE")
58+
textView.selectionManager.setSelectedRanges([NSRange(location:0, length:1),NSRange(location:4, length:1)])
59+
60+
textView.setMarkedText("´", selectedRange:.notFound, replacementRange:.notFound)
61+
XCTAssertEqual(textView.string,"´BCD´")
62+
63+
textView.insertText("é", replacementRange:.notFound)
64+
XCTAssertEqual(textView.string,"éBCDé")
65+
XCTAssertEqual(
66+
textView.selectionManager.textSelections.map(\.range).sorted(by:{ $0.location< $1.location}),
67+
[NSRange(location:1, length:0),NSRange(location:5, length:0)]
68+
)
69+
}
70+
71+
func test_markedTextMultipleSelectionMultipleChar(){
72+
lettextView=TextView(string:"ABCDE")
73+
textView.selectionManager.setSelectedRanges([NSRange(location:0, length:1),NSRange(location:4, length:1)])
74+
75+
textView.setMarkedText("´", selectedRange:.notFound, replacementRange:.notFound)
76+
XCTAssertEqual(textView.string,"´BCD´")
77+
78+
textView.setMarkedText("´´´", selectedRange:.notFound, replacementRange:.notFound)
79+
XCTAssertEqual(textView.string,"´´´BCD´´´")
80+
XCTAssertEqual(
81+
textView.selectionManager.textSelections.map(\.range).sorted(by:{ $0.location< $1.location}),
82+
[NSRange(location:3, length:0),NSRange(location:9, length:0)]
83+
)
84+
85+
textView.insertText("é", replacementRange:.notFound)
86+
XCTAssertEqual(textView.string,"éBCDé")
87+
XCTAssertEqual(
88+
textView.selectionManager.textSelections.map(\.range).sorted(by:{ $0.location< $1.location}),
89+
[NSRange(location:1, length:0),NSRange(location:5, length:0)]
90+
)
91+
}
92+
93+
func test_cancelMarkedText(){
94+
lettextView=TextView(string:"")
95+
textView.selectionManager.setSelectedRange(.zero)
96+
97+
textView.setMarkedText("´", selectedRange:.notFound, replacementRange:.notFound)
98+
XCTAssertEqual(textView.string,"´")
99+
100+
// The NSTextInputContext performs the following actions when a marked text segment is ended w/o replacing the
101+
// marked text:
102+
textView.insertText("´", replacementRange:.notFound)
103+
textView.insertText("4", replacementRange:.notFound)
104+
105+
XCTAssertEqual(textView.string,"´4")
106+
XCTAssertEqual(textView.selectionManager.textSelections.map(\.range),[NSRange(location:2, length:0)])
107+
}
108+
109+
func test_cancelMarkedTextMultipleCursor(){
110+
lettextView=TextView(string:"ABC")
111+
textView.selectionManager.setSelectedRanges([NSRange(location:1, length:0),NSRange(location:2, length:0)])
112+
113+
textView.setMarkedText("´", selectedRange:.notFound, replacementRange:.notFound)
114+
XCTAssertEqual(textView.string,"A´B´C")
115+
116+
// The NSTextInputContext performs the following actions when a marked text segment is ended w/o replacing the
117+
// marked text:
118+
textView.insertText("´", replacementRange:.notFound)
119+
textView.insertText("4", replacementRange:.notFound)
120+
121+
XCTAssertEqual(textView.string,"A´4B´4C")
122+
XCTAssertEqual(
123+
textView.selectionManager.textSelections.map(\.range).sorted(by:{ $0.location< $1.location}),
124+
[NSRange(location:3, length:0),NSRange(location:6, length:0)]
125+
)
126+
}
127+
}

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp