Subclassing NSTextStorage breaks list editing
I have a basic Mac app with a standard NSTextView
. I'm trying to implement and use a subclass of NSTextStorage
, but even a very basic implementation breaks list editing behavior:
- I add a bulleted list with two items
- I copy & paste that list further down into the document
- Pressing Enter in the pasted list breaks formatting for the last list item.
Here's a quick video:
Two issues:
- The bullet points of the pasted list use a smaller font size
- Pressing Enter after the second list item breaks the third item
This works fine when I don't replace the text storage.
Here's my code:
ViewController.swift
@IBOutlet var textView:NSTextView!
override func viewDidLoad() {
[...]
textView.layoutManager?.replaceTextStorage(TestTextStorage())
}
TestTextStorage.swift
class TestTextStorage: NSTextStorage {
let backingStore = NSMutableAttributedString()
override var string: String {
return backingStore.string
}
override func attributes(at location: Int, effectiveRange range: NSRangePointer?) -> [NSAttributedString.Key:Any] {
return backingStore.attributes(at: location, effectiveRange: range)
}
override func replaceCharacters(in range: NSRange, with str: String) {
beginEditing()
backingStore.replaceCharacters(in: range, with:str)
edited(.editedCharacters, range: range,
changeInLength: (str as NSString).length - range.length)
endEditing()
}
override func setAttributes(_ attrs: [NSAttributedString.Key: Any]?, range: NSRange) {
beginEditing()
backingStore.setAttributes(attrs, range: range)
edited(.editedAttributes, range: range, changeInLength: 0)
endEditing()
}
}
cocoa nstextview textkit nstextstorage
add a comment |
I have a basic Mac app with a standard NSTextView
. I'm trying to implement and use a subclass of NSTextStorage
, but even a very basic implementation breaks list editing behavior:
- I add a bulleted list with two items
- I copy & paste that list further down into the document
- Pressing Enter in the pasted list breaks formatting for the last list item.
Here's a quick video:
Two issues:
- The bullet points of the pasted list use a smaller font size
- Pressing Enter after the second list item breaks the third item
This works fine when I don't replace the text storage.
Here's my code:
ViewController.swift
@IBOutlet var textView:NSTextView!
override func viewDidLoad() {
[...]
textView.layoutManager?.replaceTextStorage(TestTextStorage())
}
TestTextStorage.swift
class TestTextStorage: NSTextStorage {
let backingStore = NSMutableAttributedString()
override var string: String {
return backingStore.string
}
override func attributes(at location: Int, effectiveRange range: NSRangePointer?) -> [NSAttributedString.Key:Any] {
return backingStore.attributes(at: location, effectiveRange: range)
}
override func replaceCharacters(in range: NSRange, with str: String) {
beginEditing()
backingStore.replaceCharacters(in: range, with:str)
edited(.editedCharacters, range: range,
changeInLength: (str as NSString).length - range.length)
endEditing()
}
override func setAttributes(_ attrs: [NSAttributedString.Key: Any]?, range: NSRange) {
beginEditing()
backingStore.setAttributes(attrs, range: range)
edited(.editedAttributes, range: range, changeInLength: 0)
endEditing()
}
}
cocoa nstextview textkit nstextstorage
(Using Xcode 10.1) Your code throws an error and stack trace, might be related to your unexpected results...
– CRD
Nov 22 '18 at 21:28
If you translate your SwiftTestTextStorage
to Objective-C and use that your code works. You could create a test app, the Swift & Objective-C extensions toNSTextStorage
with debugging output (print()
&NSLog()
respectively) in each and perform the same operations in each text field and see where the two versions diverge (i.e when the Swift version goes wrong). HTH
– CRD
Nov 22 '18 at 22:07
add a comment |
I have a basic Mac app with a standard NSTextView
. I'm trying to implement and use a subclass of NSTextStorage
, but even a very basic implementation breaks list editing behavior:
- I add a bulleted list with two items
- I copy & paste that list further down into the document
- Pressing Enter in the pasted list breaks formatting for the last list item.
Here's a quick video:
Two issues:
- The bullet points of the pasted list use a smaller font size
- Pressing Enter after the second list item breaks the third item
This works fine when I don't replace the text storage.
Here's my code:
ViewController.swift
@IBOutlet var textView:NSTextView!
override func viewDidLoad() {
[...]
textView.layoutManager?.replaceTextStorage(TestTextStorage())
}
TestTextStorage.swift
class TestTextStorage: NSTextStorage {
let backingStore = NSMutableAttributedString()
override var string: String {
return backingStore.string
}
override func attributes(at location: Int, effectiveRange range: NSRangePointer?) -> [NSAttributedString.Key:Any] {
return backingStore.attributes(at: location, effectiveRange: range)
}
override func replaceCharacters(in range: NSRange, with str: String) {
beginEditing()
backingStore.replaceCharacters(in: range, with:str)
edited(.editedCharacters, range: range,
changeInLength: (str as NSString).length - range.length)
endEditing()
}
override func setAttributes(_ attrs: [NSAttributedString.Key: Any]?, range: NSRange) {
beginEditing()
backingStore.setAttributes(attrs, range: range)
edited(.editedAttributes, range: range, changeInLength: 0)
endEditing()
}
}
cocoa nstextview textkit nstextstorage
I have a basic Mac app with a standard NSTextView
. I'm trying to implement and use a subclass of NSTextStorage
, but even a very basic implementation breaks list editing behavior:
- I add a bulleted list with two items
- I copy & paste that list further down into the document
- Pressing Enter in the pasted list breaks formatting for the last list item.
Here's a quick video:
Two issues:
- The bullet points of the pasted list use a smaller font size
- Pressing Enter after the second list item breaks the third item
This works fine when I don't replace the text storage.
Here's my code:
ViewController.swift
@IBOutlet var textView:NSTextView!
override func viewDidLoad() {
[...]
textView.layoutManager?.replaceTextStorage(TestTextStorage())
}
TestTextStorage.swift
class TestTextStorage: NSTextStorage {
let backingStore = NSMutableAttributedString()
override var string: String {
return backingStore.string
}
override func attributes(at location: Int, effectiveRange range: NSRangePointer?) -> [NSAttributedString.Key:Any] {
return backingStore.attributes(at: location, effectiveRange: range)
}
override func replaceCharacters(in range: NSRange, with str: String) {
beginEditing()
backingStore.replaceCharacters(in: range, with:str)
edited(.editedCharacters, range: range,
changeInLength: (str as NSString).length - range.length)
endEditing()
}
override func setAttributes(_ attrs: [NSAttributedString.Key: Any]?, range: NSRange) {
beginEditing()
backingStore.setAttributes(attrs, range: range)
edited(.editedAttributes, range: range, changeInLength: 0)
endEditing()
}
}
cocoa nstextview textkit nstextstorage
cocoa nstextview textkit nstextstorage
asked Nov 21 '18 at 11:08
MarkMark
2,86212964
2,86212964
(Using Xcode 10.1) Your code throws an error and stack trace, might be related to your unexpected results...
– CRD
Nov 22 '18 at 21:28
If you translate your SwiftTestTextStorage
to Objective-C and use that your code works. You could create a test app, the Swift & Objective-C extensions toNSTextStorage
with debugging output (print()
&NSLog()
respectively) in each and perform the same operations in each text field and see where the two versions diverge (i.e when the Swift version goes wrong). HTH
– CRD
Nov 22 '18 at 22:07
add a comment |
(Using Xcode 10.1) Your code throws an error and stack trace, might be related to your unexpected results...
– CRD
Nov 22 '18 at 21:28
If you translate your SwiftTestTextStorage
to Objective-C and use that your code works. You could create a test app, the Swift & Objective-C extensions toNSTextStorage
with debugging output (print()
&NSLog()
respectively) in each and perform the same operations in each text field and see where the two versions diverge (i.e when the Swift version goes wrong). HTH
– CRD
Nov 22 '18 at 22:07
(Using Xcode 10.1) Your code throws an error and stack trace, might be related to your unexpected results...
– CRD
Nov 22 '18 at 21:28
(Using Xcode 10.1) Your code throws an error and stack trace, might be related to your unexpected results...
– CRD
Nov 22 '18 at 21:28
If you translate your Swift
TestTextStorage
to Objective-C and use that your code works. You could create a test app, the Swift & Objective-C extensions to NSTextStorage
with debugging output (print()
& NSLog()
respectively) in each and perform the same operations in each text field and see where the two versions diverge (i.e when the Swift version goes wrong). HTH– CRD
Nov 22 '18 at 22:07
If you translate your Swift
TestTextStorage
to Objective-C and use that your code works. You could create a test app, the Swift & Objective-C extensions to NSTextStorage
with debugging output (print()
& NSLog()
respectively) in each and perform the same operations in each text field and see where the two versions diverge (i.e when the Swift version goes wrong). HTH– CRD
Nov 22 '18 at 22:07
add a comment |
1 Answer
1
active
oldest
votes
You have found a bug in Swift (and maybe not just in the Swift libraries, maybe in something a bit more fundamental).
So what is going on?
You will be able to see this a bit better if you create a numbered list rather than a bulleted one. You don't need to do any copy and paste, just:
- Type "aa", hit return, type "bb"
- Do select all and format as a numbered list
- Place cursor at the end of "aa" and hit return...
What you see is a mess, but you can see the two original numbers are still there and the new middle list item you started by hitting return is where all the mess is.
When you hit return the text system has to renumber the list items, as you've just inserted a new item. First, it turns out that it performs this "renumbering" even if it is a bulleted list, which is why you see the mess in your example. Second, it does this renumbering by starting at the beginning of the list and renumbering every list item and inserting a new number for the just created item.
The Process in Objective-C
If you translate your Swift code into the equivalent Objective-C and trace through you can watch the process. Starting with:
1) aa
2) bb
the internal buffer is something like:
t1)taant2)tbb
first the return is inserted:
t1)taannt2)tbb
and then an internal routine _reformListAtIndex:
is called and it starts "renumbering". First it replaces t1)t
with t1)
- the number hasn't changed. Then it inserts t2)t
between the two new lines, as at this point we have:
t1)taant2)tnt2)tbb
and then it replaces the original t2)t
with t3)t
giving:
t1)taant2)tnt3)tbb
and it's job is done. All these replacements are based on specifying the range of characters to replace, the insertion uses a range of length 0, and go through:
- (void)replaceCharactersInRange:(NSRange)range withString:(NSString * _Nonnull)str
which in Swift is replaced by:
override func replaceCharacters(in range: NSRange, with str: String)
The Process in Swift
In Objective-C strings have reference semantics, change a string and all parts of the code with a reference to the string see the change. In Swift strings have value semantics and strings are copied (notionally at least) on being passed to functions etc.; if the copy is changed in called function the caller won't see that change in its copy.
The text system was written in (or for) Objective-C and it is reasonable to assume it may take advantage of the reference semantics. When you replace part of its code with Swift the Swift code has to do a little dance, during the list renumbering stage when replaceCharacters()
gets called the stack will look something like:
#0 0x0000000100003470 in SwiftTextStorage.replaceCharacters(in:with:)
#1 0x0000000100003a00 in @objc SwiftTextStorage.replaceCharacters(in:with:) ()
#2 0x00007fff2cdc30c7 in -[NSMutableAttributedString replaceCharactersInRange:withAttributedString:] ()
#3 0x00007fff28998c41 in -[NSTextView(NSKeyBindingCommands) _reformListAtIndex:] ()
#4 0x00007fff284fd555 in -[NSTextView(NSKeyBindingCommands) insertNewline:] ()
Frame #4 is the Objective-C code called when return was hit, after inserting the newline it calls the internal routine _reformListAtIndex:
, frame #3, to do the renumbering. This calls another Objective-C routine in frame #2, which in turn calls, frame #1, what it thinks is the Objective-C method replaceCharactersInRange:withString:
, but is in fact a Swift replacement. This replacement does a little dance converting Objective-C reference semantic strings to Swift value semantics strings and then calls, frame #0, the Swift replaceCharacters()
.
Dancing is Hard
If you trace through your Swift code just as you did the Objective-C translation when the renumbering gets to the stage of changing the original t2)t
to t3)t
you will see a misstep, the range given for the original t2)t
is what is was before the new t2)t
was inserted in the previous step (i.e. it is off by 4 positions)... and you end up with a mess and a few more dance steps later the code crashes with a string referring error as the indices are all wrong.
This suggests that the Objective-C code is relying on reference semantics, and the choreographer of the Swift dance converting reference to value and back to reference semantics has failed to meet the Objective-C code's expectations: so either when the Objective-C code, or some Swift code which has replaced it, calculates the range of the original t2)t
it is doing so on string which hasn't been altered by the previous insertion of the new t2)t
.
Confused? Well dancing can make you dizzy at times ;-)
Fix?
Code your subclass of NSTextStorage
in Objective-C and go to bugreport.apple.com
and report the bug.
HTH (more than it makes you dizzy)
Wow, thanks a lot for this insightful answer! I filed a bug as you suggested. How did you know where to start debugging this (I'd like to level up my debugging skills)? Since you seem to know your way around TextKit very well, would you also have an opinion on my question here: stackoverflow.com/questions/53415525/… Thanks!
– Mark
Nov 24 '18 at 9:37
@Mark - I don't know TextKit that well, a lot of the answer comes from analysis during debug. Which brings us to how to "level up [your] debugging skills" - the key isn't knowing a lot of specific details but understanding how the language(s) you are looking at, and the computer system underlying them, work at the basic level - spend times understanding basic data types, value vs. reference types & semantics (there is a difference between value types and value semantics and understanding this will help a lot with Swift), how constructs such as functions, loops, recursion etc. translate [cont]
– CRD
Nov 30 '18 at 17:26
[cont] to the underlying computer architecture etc. I.e. aim to I mprove your understanding of language semantics & implementation in general rather than a specific language. In your particular case here, Obj-C is a more mature language than Swift, and TextKit is a key part of Cocoa - that suggests the Swift is more likely to be at fault so I suspected it first. Then knowing the different semantic models a mismatch across the language boundary seemed likely. A transliteration of your code into Obj-C supported Swift being the bad actor, it was then just looking into that boundary. HTH
– CRD
Nov 30 '18 at 17:34
add a comment |
Your Answer
StackExchange.ifUsing("editor", function () {
StackExchange.using("externalEditor", function () {
StackExchange.using("snippets", function () {
StackExchange.snippets.init();
});
});
}, "code-snippets");
StackExchange.ready(function() {
var channelOptions = {
tags: "".split(" "),
id: "1"
};
initTagRenderer("".split(" "), "".split(" "), channelOptions);
StackExchange.using("externalEditor", function() {
// Have to fire editor after snippets, if snippets enabled
if (StackExchange.settings.snippets.snippetsEnabled) {
StackExchange.using("snippets", function() {
createEditor();
});
}
else {
createEditor();
}
});
function createEditor() {
StackExchange.prepareEditor({
heartbeatType: 'answer',
autoActivateHeartbeat: false,
convertImagesToLinks: true,
noModals: true,
showLowRepImageUploadWarning: true,
reputationToPostImages: 10,
bindNavPrevention: true,
postfix: "",
imageUploader: {
brandingHtml: "Powered by u003ca class="icon-imgur-white" href="https://imgur.com/"u003eu003c/au003e",
contentPolicyHtml: "User contributions licensed under u003ca href="https://creativecommons.org/licenses/by-sa/3.0/"u003ecc by-sa 3.0 with attribution requiredu003c/au003e u003ca href="https://stackoverflow.com/legal/content-policy"u003e(content policy)u003c/au003e",
allowUrls: true
},
onDemand: true,
discardSelector: ".discard-answer"
,immediatelyShowMarkdownHelp:true
});
}
});
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
StackExchange.ready(
function () {
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fstackoverflow.com%2fquestions%2f53410817%2fsubclassing-nstextstorage-breaks-list-editing%23new-answer', 'question_page');
}
);
Post as a guest
Required, but never shown
1 Answer
1
active
oldest
votes
1 Answer
1
active
oldest
votes
active
oldest
votes
active
oldest
votes
You have found a bug in Swift (and maybe not just in the Swift libraries, maybe in something a bit more fundamental).
So what is going on?
You will be able to see this a bit better if you create a numbered list rather than a bulleted one. You don't need to do any copy and paste, just:
- Type "aa", hit return, type "bb"
- Do select all and format as a numbered list
- Place cursor at the end of "aa" and hit return...
What you see is a mess, but you can see the two original numbers are still there and the new middle list item you started by hitting return is where all the mess is.
When you hit return the text system has to renumber the list items, as you've just inserted a new item. First, it turns out that it performs this "renumbering" even if it is a bulleted list, which is why you see the mess in your example. Second, it does this renumbering by starting at the beginning of the list and renumbering every list item and inserting a new number for the just created item.
The Process in Objective-C
If you translate your Swift code into the equivalent Objective-C and trace through you can watch the process. Starting with:
1) aa
2) bb
the internal buffer is something like:
t1)taant2)tbb
first the return is inserted:
t1)taannt2)tbb
and then an internal routine _reformListAtIndex:
is called and it starts "renumbering". First it replaces t1)t
with t1)
- the number hasn't changed. Then it inserts t2)t
between the two new lines, as at this point we have:
t1)taant2)tnt2)tbb
and then it replaces the original t2)t
with t3)t
giving:
t1)taant2)tnt3)tbb
and it's job is done. All these replacements are based on specifying the range of characters to replace, the insertion uses a range of length 0, and go through:
- (void)replaceCharactersInRange:(NSRange)range withString:(NSString * _Nonnull)str
which in Swift is replaced by:
override func replaceCharacters(in range: NSRange, with str: String)
The Process in Swift
In Objective-C strings have reference semantics, change a string and all parts of the code with a reference to the string see the change. In Swift strings have value semantics and strings are copied (notionally at least) on being passed to functions etc.; if the copy is changed in called function the caller won't see that change in its copy.
The text system was written in (or for) Objective-C and it is reasonable to assume it may take advantage of the reference semantics. When you replace part of its code with Swift the Swift code has to do a little dance, during the list renumbering stage when replaceCharacters()
gets called the stack will look something like:
#0 0x0000000100003470 in SwiftTextStorage.replaceCharacters(in:with:)
#1 0x0000000100003a00 in @objc SwiftTextStorage.replaceCharacters(in:with:) ()
#2 0x00007fff2cdc30c7 in -[NSMutableAttributedString replaceCharactersInRange:withAttributedString:] ()
#3 0x00007fff28998c41 in -[NSTextView(NSKeyBindingCommands) _reformListAtIndex:] ()
#4 0x00007fff284fd555 in -[NSTextView(NSKeyBindingCommands) insertNewline:] ()
Frame #4 is the Objective-C code called when return was hit, after inserting the newline it calls the internal routine _reformListAtIndex:
, frame #3, to do the renumbering. This calls another Objective-C routine in frame #2, which in turn calls, frame #1, what it thinks is the Objective-C method replaceCharactersInRange:withString:
, but is in fact a Swift replacement. This replacement does a little dance converting Objective-C reference semantic strings to Swift value semantics strings and then calls, frame #0, the Swift replaceCharacters()
.
Dancing is Hard
If you trace through your Swift code just as you did the Objective-C translation when the renumbering gets to the stage of changing the original t2)t
to t3)t
you will see a misstep, the range given for the original t2)t
is what is was before the new t2)t
was inserted in the previous step (i.e. it is off by 4 positions)... and you end up with a mess and a few more dance steps later the code crashes with a string referring error as the indices are all wrong.
This suggests that the Objective-C code is relying on reference semantics, and the choreographer of the Swift dance converting reference to value and back to reference semantics has failed to meet the Objective-C code's expectations: so either when the Objective-C code, or some Swift code which has replaced it, calculates the range of the original t2)t
it is doing so on string which hasn't been altered by the previous insertion of the new t2)t
.
Confused? Well dancing can make you dizzy at times ;-)
Fix?
Code your subclass of NSTextStorage
in Objective-C and go to bugreport.apple.com
and report the bug.
HTH (more than it makes you dizzy)
Wow, thanks a lot for this insightful answer! I filed a bug as you suggested. How did you know where to start debugging this (I'd like to level up my debugging skills)? Since you seem to know your way around TextKit very well, would you also have an opinion on my question here: stackoverflow.com/questions/53415525/… Thanks!
– Mark
Nov 24 '18 at 9:37
@Mark - I don't know TextKit that well, a lot of the answer comes from analysis during debug. Which brings us to how to "level up [your] debugging skills" - the key isn't knowing a lot of specific details but understanding how the language(s) you are looking at, and the computer system underlying them, work at the basic level - spend times understanding basic data types, value vs. reference types & semantics (there is a difference between value types and value semantics and understanding this will help a lot with Swift), how constructs such as functions, loops, recursion etc. translate [cont]
– CRD
Nov 30 '18 at 17:26
[cont] to the underlying computer architecture etc. I.e. aim to I mprove your understanding of language semantics & implementation in general rather than a specific language. In your particular case here, Obj-C is a more mature language than Swift, and TextKit is a key part of Cocoa - that suggests the Swift is more likely to be at fault so I suspected it first. Then knowing the different semantic models a mismatch across the language boundary seemed likely. A transliteration of your code into Obj-C supported Swift being the bad actor, it was then just looking into that boundary. HTH
– CRD
Nov 30 '18 at 17:34
add a comment |
You have found a bug in Swift (and maybe not just in the Swift libraries, maybe in something a bit more fundamental).
So what is going on?
You will be able to see this a bit better if you create a numbered list rather than a bulleted one. You don't need to do any copy and paste, just:
- Type "aa", hit return, type "bb"
- Do select all and format as a numbered list
- Place cursor at the end of "aa" and hit return...
What you see is a mess, but you can see the two original numbers are still there and the new middle list item you started by hitting return is where all the mess is.
When you hit return the text system has to renumber the list items, as you've just inserted a new item. First, it turns out that it performs this "renumbering" even if it is a bulleted list, which is why you see the mess in your example. Second, it does this renumbering by starting at the beginning of the list and renumbering every list item and inserting a new number for the just created item.
The Process in Objective-C
If you translate your Swift code into the equivalent Objective-C and trace through you can watch the process. Starting with:
1) aa
2) bb
the internal buffer is something like:
t1)taant2)tbb
first the return is inserted:
t1)taannt2)tbb
and then an internal routine _reformListAtIndex:
is called and it starts "renumbering". First it replaces t1)t
with t1)
- the number hasn't changed. Then it inserts t2)t
between the two new lines, as at this point we have:
t1)taant2)tnt2)tbb
and then it replaces the original t2)t
with t3)t
giving:
t1)taant2)tnt3)tbb
and it's job is done. All these replacements are based on specifying the range of characters to replace, the insertion uses a range of length 0, and go through:
- (void)replaceCharactersInRange:(NSRange)range withString:(NSString * _Nonnull)str
which in Swift is replaced by:
override func replaceCharacters(in range: NSRange, with str: String)
The Process in Swift
In Objective-C strings have reference semantics, change a string and all parts of the code with a reference to the string see the change. In Swift strings have value semantics and strings are copied (notionally at least) on being passed to functions etc.; if the copy is changed in called function the caller won't see that change in its copy.
The text system was written in (or for) Objective-C and it is reasonable to assume it may take advantage of the reference semantics. When you replace part of its code with Swift the Swift code has to do a little dance, during the list renumbering stage when replaceCharacters()
gets called the stack will look something like:
#0 0x0000000100003470 in SwiftTextStorage.replaceCharacters(in:with:)
#1 0x0000000100003a00 in @objc SwiftTextStorage.replaceCharacters(in:with:) ()
#2 0x00007fff2cdc30c7 in -[NSMutableAttributedString replaceCharactersInRange:withAttributedString:] ()
#3 0x00007fff28998c41 in -[NSTextView(NSKeyBindingCommands) _reformListAtIndex:] ()
#4 0x00007fff284fd555 in -[NSTextView(NSKeyBindingCommands) insertNewline:] ()
Frame #4 is the Objective-C code called when return was hit, after inserting the newline it calls the internal routine _reformListAtIndex:
, frame #3, to do the renumbering. This calls another Objective-C routine in frame #2, which in turn calls, frame #1, what it thinks is the Objective-C method replaceCharactersInRange:withString:
, but is in fact a Swift replacement. This replacement does a little dance converting Objective-C reference semantic strings to Swift value semantics strings and then calls, frame #0, the Swift replaceCharacters()
.
Dancing is Hard
If you trace through your Swift code just as you did the Objective-C translation when the renumbering gets to the stage of changing the original t2)t
to t3)t
you will see a misstep, the range given for the original t2)t
is what is was before the new t2)t
was inserted in the previous step (i.e. it is off by 4 positions)... and you end up with a mess and a few more dance steps later the code crashes with a string referring error as the indices are all wrong.
This suggests that the Objective-C code is relying on reference semantics, and the choreographer of the Swift dance converting reference to value and back to reference semantics has failed to meet the Objective-C code's expectations: so either when the Objective-C code, or some Swift code which has replaced it, calculates the range of the original t2)t
it is doing so on string which hasn't been altered by the previous insertion of the new t2)t
.
Confused? Well dancing can make you dizzy at times ;-)
Fix?
Code your subclass of NSTextStorage
in Objective-C and go to bugreport.apple.com
and report the bug.
HTH (more than it makes you dizzy)
Wow, thanks a lot for this insightful answer! I filed a bug as you suggested. How did you know where to start debugging this (I'd like to level up my debugging skills)? Since you seem to know your way around TextKit very well, would you also have an opinion on my question here: stackoverflow.com/questions/53415525/… Thanks!
– Mark
Nov 24 '18 at 9:37
@Mark - I don't know TextKit that well, a lot of the answer comes from analysis during debug. Which brings us to how to "level up [your] debugging skills" - the key isn't knowing a lot of specific details but understanding how the language(s) you are looking at, and the computer system underlying them, work at the basic level - spend times understanding basic data types, value vs. reference types & semantics (there is a difference between value types and value semantics and understanding this will help a lot with Swift), how constructs such as functions, loops, recursion etc. translate [cont]
– CRD
Nov 30 '18 at 17:26
[cont] to the underlying computer architecture etc. I.e. aim to I mprove your understanding of language semantics & implementation in general rather than a specific language. In your particular case here, Obj-C is a more mature language than Swift, and TextKit is a key part of Cocoa - that suggests the Swift is more likely to be at fault so I suspected it first. Then knowing the different semantic models a mismatch across the language boundary seemed likely. A transliteration of your code into Obj-C supported Swift being the bad actor, it was then just looking into that boundary. HTH
– CRD
Nov 30 '18 at 17:34
add a comment |
You have found a bug in Swift (and maybe not just in the Swift libraries, maybe in something a bit more fundamental).
So what is going on?
You will be able to see this a bit better if you create a numbered list rather than a bulleted one. You don't need to do any copy and paste, just:
- Type "aa", hit return, type "bb"
- Do select all and format as a numbered list
- Place cursor at the end of "aa" and hit return...
What you see is a mess, but you can see the two original numbers are still there and the new middle list item you started by hitting return is where all the mess is.
When you hit return the text system has to renumber the list items, as you've just inserted a new item. First, it turns out that it performs this "renumbering" even if it is a bulleted list, which is why you see the mess in your example. Second, it does this renumbering by starting at the beginning of the list and renumbering every list item and inserting a new number for the just created item.
The Process in Objective-C
If you translate your Swift code into the equivalent Objective-C and trace through you can watch the process. Starting with:
1) aa
2) bb
the internal buffer is something like:
t1)taant2)tbb
first the return is inserted:
t1)taannt2)tbb
and then an internal routine _reformListAtIndex:
is called and it starts "renumbering". First it replaces t1)t
with t1)
- the number hasn't changed. Then it inserts t2)t
between the two new lines, as at this point we have:
t1)taant2)tnt2)tbb
and then it replaces the original t2)t
with t3)t
giving:
t1)taant2)tnt3)tbb
and it's job is done. All these replacements are based on specifying the range of characters to replace, the insertion uses a range of length 0, and go through:
- (void)replaceCharactersInRange:(NSRange)range withString:(NSString * _Nonnull)str
which in Swift is replaced by:
override func replaceCharacters(in range: NSRange, with str: String)
The Process in Swift
In Objective-C strings have reference semantics, change a string and all parts of the code with a reference to the string see the change. In Swift strings have value semantics and strings are copied (notionally at least) on being passed to functions etc.; if the copy is changed in called function the caller won't see that change in its copy.
The text system was written in (or for) Objective-C and it is reasonable to assume it may take advantage of the reference semantics. When you replace part of its code with Swift the Swift code has to do a little dance, during the list renumbering stage when replaceCharacters()
gets called the stack will look something like:
#0 0x0000000100003470 in SwiftTextStorage.replaceCharacters(in:with:)
#1 0x0000000100003a00 in @objc SwiftTextStorage.replaceCharacters(in:with:) ()
#2 0x00007fff2cdc30c7 in -[NSMutableAttributedString replaceCharactersInRange:withAttributedString:] ()
#3 0x00007fff28998c41 in -[NSTextView(NSKeyBindingCommands) _reformListAtIndex:] ()
#4 0x00007fff284fd555 in -[NSTextView(NSKeyBindingCommands) insertNewline:] ()
Frame #4 is the Objective-C code called when return was hit, after inserting the newline it calls the internal routine _reformListAtIndex:
, frame #3, to do the renumbering. This calls another Objective-C routine in frame #2, which in turn calls, frame #1, what it thinks is the Objective-C method replaceCharactersInRange:withString:
, but is in fact a Swift replacement. This replacement does a little dance converting Objective-C reference semantic strings to Swift value semantics strings and then calls, frame #0, the Swift replaceCharacters()
.
Dancing is Hard
If you trace through your Swift code just as you did the Objective-C translation when the renumbering gets to the stage of changing the original t2)t
to t3)t
you will see a misstep, the range given for the original t2)t
is what is was before the new t2)t
was inserted in the previous step (i.e. it is off by 4 positions)... and you end up with a mess and a few more dance steps later the code crashes with a string referring error as the indices are all wrong.
This suggests that the Objective-C code is relying on reference semantics, and the choreographer of the Swift dance converting reference to value and back to reference semantics has failed to meet the Objective-C code's expectations: so either when the Objective-C code, or some Swift code which has replaced it, calculates the range of the original t2)t
it is doing so on string which hasn't been altered by the previous insertion of the new t2)t
.
Confused? Well dancing can make you dizzy at times ;-)
Fix?
Code your subclass of NSTextStorage
in Objective-C and go to bugreport.apple.com
and report the bug.
HTH (more than it makes you dizzy)
You have found a bug in Swift (and maybe not just in the Swift libraries, maybe in something a bit more fundamental).
So what is going on?
You will be able to see this a bit better if you create a numbered list rather than a bulleted one. You don't need to do any copy and paste, just:
- Type "aa", hit return, type "bb"
- Do select all and format as a numbered list
- Place cursor at the end of "aa" and hit return...
What you see is a mess, but you can see the two original numbers are still there and the new middle list item you started by hitting return is where all the mess is.
When you hit return the text system has to renumber the list items, as you've just inserted a new item. First, it turns out that it performs this "renumbering" even if it is a bulleted list, which is why you see the mess in your example. Second, it does this renumbering by starting at the beginning of the list and renumbering every list item and inserting a new number for the just created item.
The Process in Objective-C
If you translate your Swift code into the equivalent Objective-C and trace through you can watch the process. Starting with:
1) aa
2) bb
the internal buffer is something like:
t1)taant2)tbb
first the return is inserted:
t1)taannt2)tbb
and then an internal routine _reformListAtIndex:
is called and it starts "renumbering". First it replaces t1)t
with t1)
- the number hasn't changed. Then it inserts t2)t
between the two new lines, as at this point we have:
t1)taant2)tnt2)tbb
and then it replaces the original t2)t
with t3)t
giving:
t1)taant2)tnt3)tbb
and it's job is done. All these replacements are based on specifying the range of characters to replace, the insertion uses a range of length 0, and go through:
- (void)replaceCharactersInRange:(NSRange)range withString:(NSString * _Nonnull)str
which in Swift is replaced by:
override func replaceCharacters(in range: NSRange, with str: String)
The Process in Swift
In Objective-C strings have reference semantics, change a string and all parts of the code with a reference to the string see the change. In Swift strings have value semantics and strings are copied (notionally at least) on being passed to functions etc.; if the copy is changed in called function the caller won't see that change in its copy.
The text system was written in (or for) Objective-C and it is reasonable to assume it may take advantage of the reference semantics. When you replace part of its code with Swift the Swift code has to do a little dance, during the list renumbering stage when replaceCharacters()
gets called the stack will look something like:
#0 0x0000000100003470 in SwiftTextStorage.replaceCharacters(in:with:)
#1 0x0000000100003a00 in @objc SwiftTextStorage.replaceCharacters(in:with:) ()
#2 0x00007fff2cdc30c7 in -[NSMutableAttributedString replaceCharactersInRange:withAttributedString:] ()
#3 0x00007fff28998c41 in -[NSTextView(NSKeyBindingCommands) _reformListAtIndex:] ()
#4 0x00007fff284fd555 in -[NSTextView(NSKeyBindingCommands) insertNewline:] ()
Frame #4 is the Objective-C code called when return was hit, after inserting the newline it calls the internal routine _reformListAtIndex:
, frame #3, to do the renumbering. This calls another Objective-C routine in frame #2, which in turn calls, frame #1, what it thinks is the Objective-C method replaceCharactersInRange:withString:
, but is in fact a Swift replacement. This replacement does a little dance converting Objective-C reference semantic strings to Swift value semantics strings and then calls, frame #0, the Swift replaceCharacters()
.
Dancing is Hard
If you trace through your Swift code just as you did the Objective-C translation when the renumbering gets to the stage of changing the original t2)t
to t3)t
you will see a misstep, the range given for the original t2)t
is what is was before the new t2)t
was inserted in the previous step (i.e. it is off by 4 positions)... and you end up with a mess and a few more dance steps later the code crashes with a string referring error as the indices are all wrong.
This suggests that the Objective-C code is relying on reference semantics, and the choreographer of the Swift dance converting reference to value and back to reference semantics has failed to meet the Objective-C code's expectations: so either when the Objective-C code, or some Swift code which has replaced it, calculates the range of the original t2)t
it is doing so on string which hasn't been altered by the previous insertion of the new t2)t
.
Confused? Well dancing can make you dizzy at times ;-)
Fix?
Code your subclass of NSTextStorage
in Objective-C and go to bugreport.apple.com
and report the bug.
HTH (more than it makes you dizzy)
edited Nov 23 '18 at 11:42
answered Nov 23 '18 at 8:34
CRDCRD
44.9k44870
44.9k44870
Wow, thanks a lot for this insightful answer! I filed a bug as you suggested. How did you know where to start debugging this (I'd like to level up my debugging skills)? Since you seem to know your way around TextKit very well, would you also have an opinion on my question here: stackoverflow.com/questions/53415525/… Thanks!
– Mark
Nov 24 '18 at 9:37
@Mark - I don't know TextKit that well, a lot of the answer comes from analysis during debug. Which brings us to how to "level up [your] debugging skills" - the key isn't knowing a lot of specific details but understanding how the language(s) you are looking at, and the computer system underlying them, work at the basic level - spend times understanding basic data types, value vs. reference types & semantics (there is a difference between value types and value semantics and understanding this will help a lot with Swift), how constructs such as functions, loops, recursion etc. translate [cont]
– CRD
Nov 30 '18 at 17:26
[cont] to the underlying computer architecture etc. I.e. aim to I mprove your understanding of language semantics & implementation in general rather than a specific language. In your particular case here, Obj-C is a more mature language than Swift, and TextKit is a key part of Cocoa - that suggests the Swift is more likely to be at fault so I suspected it first. Then knowing the different semantic models a mismatch across the language boundary seemed likely. A transliteration of your code into Obj-C supported Swift being the bad actor, it was then just looking into that boundary. HTH
– CRD
Nov 30 '18 at 17:34
add a comment |
Wow, thanks a lot for this insightful answer! I filed a bug as you suggested. How did you know where to start debugging this (I'd like to level up my debugging skills)? Since you seem to know your way around TextKit very well, would you also have an opinion on my question here: stackoverflow.com/questions/53415525/… Thanks!
– Mark
Nov 24 '18 at 9:37
@Mark - I don't know TextKit that well, a lot of the answer comes from analysis during debug. Which brings us to how to "level up [your] debugging skills" - the key isn't knowing a lot of specific details but understanding how the language(s) you are looking at, and the computer system underlying them, work at the basic level - spend times understanding basic data types, value vs. reference types & semantics (there is a difference between value types and value semantics and understanding this will help a lot with Swift), how constructs such as functions, loops, recursion etc. translate [cont]
– CRD
Nov 30 '18 at 17:26
[cont] to the underlying computer architecture etc. I.e. aim to I mprove your understanding of language semantics & implementation in general rather than a specific language. In your particular case here, Obj-C is a more mature language than Swift, and TextKit is a key part of Cocoa - that suggests the Swift is more likely to be at fault so I suspected it first. Then knowing the different semantic models a mismatch across the language boundary seemed likely. A transliteration of your code into Obj-C supported Swift being the bad actor, it was then just looking into that boundary. HTH
– CRD
Nov 30 '18 at 17:34
Wow, thanks a lot for this insightful answer! I filed a bug as you suggested. How did you know where to start debugging this (I'd like to level up my debugging skills)? Since you seem to know your way around TextKit very well, would you also have an opinion on my question here: stackoverflow.com/questions/53415525/… Thanks!
– Mark
Nov 24 '18 at 9:37
Wow, thanks a lot for this insightful answer! I filed a bug as you suggested. How did you know where to start debugging this (I'd like to level up my debugging skills)? Since you seem to know your way around TextKit very well, would you also have an opinion on my question here: stackoverflow.com/questions/53415525/… Thanks!
– Mark
Nov 24 '18 at 9:37
@Mark - I don't know TextKit that well, a lot of the answer comes from analysis during debug. Which brings us to how to "level up [your] debugging skills" - the key isn't knowing a lot of specific details but understanding how the language(s) you are looking at, and the computer system underlying them, work at the basic level - spend times understanding basic data types, value vs. reference types & semantics (there is a difference between value types and value semantics and understanding this will help a lot with Swift), how constructs such as functions, loops, recursion etc. translate [cont]
– CRD
Nov 30 '18 at 17:26
@Mark - I don't know TextKit that well, a lot of the answer comes from analysis during debug. Which brings us to how to "level up [your] debugging skills" - the key isn't knowing a lot of specific details but understanding how the language(s) you are looking at, and the computer system underlying them, work at the basic level - spend times understanding basic data types, value vs. reference types & semantics (there is a difference between value types and value semantics and understanding this will help a lot with Swift), how constructs such as functions, loops, recursion etc. translate [cont]
– CRD
Nov 30 '18 at 17:26
[cont] to the underlying computer architecture etc. I.e. aim to I mprove your understanding of language semantics & implementation in general rather than a specific language. In your particular case here, Obj-C is a more mature language than Swift, and TextKit is a key part of Cocoa - that suggests the Swift is more likely to be at fault so I suspected it first. Then knowing the different semantic models a mismatch across the language boundary seemed likely. A transliteration of your code into Obj-C supported Swift being the bad actor, it was then just looking into that boundary. HTH
– CRD
Nov 30 '18 at 17:34
[cont] to the underlying computer architecture etc. I.e. aim to I mprove your understanding of language semantics & implementation in general rather than a specific language. In your particular case here, Obj-C is a more mature language than Swift, and TextKit is a key part of Cocoa - that suggests the Swift is more likely to be at fault so I suspected it first. Then knowing the different semantic models a mismatch across the language boundary seemed likely. A transliteration of your code into Obj-C supported Swift being the bad actor, it was then just looking into that boundary. HTH
– CRD
Nov 30 '18 at 17:34
add a comment |
Thanks for contributing an answer to Stack Overflow!
- Please be sure to answer the question. Provide details and share your research!
But avoid …
- Asking for help, clarification, or responding to other answers.
- Making statements based on opinion; back them up with references or personal experience.
To learn more, see our tips on writing great answers.
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
StackExchange.ready(
function () {
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fstackoverflow.com%2fquestions%2f53410817%2fsubclassing-nstextstorage-breaks-list-editing%23new-answer', 'question_page');
}
);
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
(Using Xcode 10.1) Your code throws an error and stack trace, might be related to your unexpected results...
– CRD
Nov 22 '18 at 21:28
If you translate your Swift
TestTextStorage
to Objective-C and use that your code works. You could create a test app, the Swift & Objective-C extensions toNSTextStorage
with debugging output (print()
&NSLog()
respectively) in each and perform the same operations in each text field and see where the two versions diverge (i.e when the Swift version goes wrong). HTH– CRD
Nov 22 '18 at 22:07