Problem
I need to understand how TextKit works and how I can use it to build a text editor. I need to figure out how to draw ONLY the visible text the end-user interacts with or determine how I would go about lazily applying attributes to the visible text only without applying attributes to the entire changed range of text in the processEditing method.
Background
iOS 7 came out with TextKit. I have a tokenizer and code that fully implements TextKit (refer to Apple's TextKitDemo project -- a link is provided below)... and it works. However, it is REALLY slow. As text is parsed, by the NSTextStorage, it requires you to colorize the ENTIRE range of the edited text, in the processEditing method, on the same thread. Offloading the work to a thread doesn't help. It's simply too slow. I got to the point where I can re-attribute only the modified ranges, but if the range is too big the process will be slow. In some conditions the entire document could be invalidated after a change has been made.
Here are some ideas I have. Please let me know if any of these will work or maybe nudge me in the right direction.
1) Multiple NSTextContainers
Reading the docs it appears that I can add multiple NSTextContainers within an NSLayoutManager. I'm assuming that by doing this I should be able to define not only the number of lines that can be drawn in the NSTextContainer, but I should also be able to know which NSTextContainer is visible to the end-user. I know that if I go this route I will need to invest a LOT of time just to see if it's feasible. Initial testing suggests that you only need one NSTextContainer. So I would have to subclass NSLayout or create a wrapper where the layout manager determines which text goes into which text container. Yuck. Also, I have NO idea how TextKit lets me know that it is time to draw a particular NSTextContainer... maybe this isn't how it works!
2) Invalidating Ranges w/ NSLayoutManager
Invalidating the layoutManager using the invalidateLayoutForCharacterRange:actualCharacterRange:. But what does this actually do and how will it offload the text attribution phase? When does it let me know that a particular text needs to be highlighted? Also, I see that the NSLayoutManager will lazily draw glyphs... how? when? How does this help me? How do I tap into this call so that I can attribute the backing string before it actually lays out the text?
3) Over-riding the NSLayoutManager drawGlyphsForGlyphRange:atPoint: method.
I really do not want to do this. That being said, in Mac OS X, NSAttributedStrings have this concept of temporary attributes where the style information is used only for presentation. This speeds up the process of highlighting GREATLY! The problem is, it doesn't exist in the iOS 7 TextKit framework (or it's there and I just don't know about it). I believe that by over-riding this method it will give me the same type of speeds you would get by using temporary attributes... as I could answer all of the layout, color and formatting questions in this method without ever touching the NSTextStorage attributed string. The only problem is, I don't know how this method works in relation to other methods provided in the NSLayoutManager class. Does it keep state of the width and height? Does it modify the size of the NSTextContainer when it's too small? Also, it only draws glyphs for characters that have been added in the text buffer. It doesn't re-draw the whole screen. only a tiny section of it... and that's perfectly fine. I have some ideas of how I could work with this... but I really have no desire to layout the glyphs. That is WAY too much work and I haven't found a good example that does this.
I would greatly appreciate any help you have to offer.
As a thank you, I'm listing all of the frameworks and references I have used over the last few years that have helped me get to where I am now in the hopes that they are helpful to you.
Syntax highlighting frameworks:
- http://colorer.sourceforge.net/
- https://github.com/MikeJ1971/Glint (Does not support strings, comments, etc.)
- https://projects.gnome.org/gtksourceview/features.html (Provides a decent lib for token generation. Works with GTK but could potentially be re-written for a different layout manager)
- http://parsekit.com/ (Good parser. However, you have to wrap the API to create a state machine when needing to repair ranges)
- http://svn.gna.org/viewcvs/etoile/trunk/Etoile/Languages/LanguageKit/
- https://github.com/CodaFi/IDEKit (AMAZING work. There is a ton of good ideas in this code. My problem is that I have absolutely no idea how it repairs ranges, or even if it does)
- http://www.crimsoneditor.com/ (An old Windows code editor. It has a very good tokenizer -- albeit a little difficult to read. It doesn't use regex expressions. That being said, I based my token logic off of this code and it is MUCH faster than any of the frameworks listed above)
Resources:
- http://cocoafactory.com/blog/2012/10/29/how-to-use-custom-nsattributedstring-attributes/
- https://github.com/objcio/issue-5-textkit
- http://alexgorbatchev.com/SyntaxHighlighter/
- http://docs.xamarin.com/samples/TextKitDemo/ (Apple's demo)
- http://cocoadev.com/ImplementSyntaxHighlighting (Be sure to read all of the child articles. Great stuff)
Most of these frameworks are the same. They either do not account for context switching (or you have to write the wrapper to provide context) for ranges or they do not repair context ranges as a user modifies the text (such as strings, multi-line comments, etc.). The last requirement is VERY important. Because if a tokenizer can't determine which ranges are affected by a change you will end up having to parse and attribute the entire string again. The only exception to this is the Crimson Editor. The issue with this tokenizer is that it doesn't save state at the time of tokenizing. At draw time, the algorithm uses the tokens to determine the state of drawing. It starts from the top of the document, up until it gets to the visible range of text. Needless to say, I have optimized this by cacheing the state of the document in certain parts of the document.
The other issue is that the frameworks do not follow the same MVC pattern that Apple does -- which is to be expected. The frameworks that have a complete working editor all use hooks, provided by the API they are built on (ie GTK, Windows, etc.), that provides them with information of where and when to draw to what part of the screen. In my case, TextKit appears to require you to attribute the entire changed range in processEditing.
Maybe my observations are wrong. (I hope they are!!) Maybe, ParseKit for instance, will work for what I need it to do and I simply don't understand how to use it. If so, please let me know! And thanks again!