Improving performance on NSTextView syntax highlighting via NSAttributedString - syntax-highlighting

I'm working on adding some syntax highlighting to an app. In a testing class, I currently have an NSTextView with the textDidChange notification. Similar to this:
-(void)textDidChange:(NSNotification *)notification
{
[self highlightText];
}
What highlight text does, it grab the string from the NSTextView parse it and create a NSMutableAttributedString and finally displays the string. The code is something similar to this: (I use ParseKit to do my parsing. The below sample just highlights code comments).
- (void) highlightText
{
NSMutableAttributedString * resultString = [[NSMutableAttributedString alloc] initWithString: inputTextView.string];
PKTokenizer *t = [PKTokenizer tokenizerWithString: inputTextView.string];
[t setTokenizerState: t.quoteState from: '[' to: ']'];
// We want comments
t.commentState.reportsCommentTokens = YES;
[t enumerateTokensUsingBlock: ^(PKToken * token, BOOL * stop)
{
// Comments take presidense.
if(token.isComment)
{
[resultString addAttribute: NSForegroundColorAttributeName
value: [self commentColor]
range: NSMakeRange(token.offset, token.stringValue.length)];
}
}];
// Monospace
[resultString addAttribute: NSFontAttributeName
value: [NSFont userFixedPitchFontOfSize:0.0]
range: NSMakeRange(0, inputTextView.string.length)];
[[inputTextView textStorage] setAttributedString: resultString];
}
Now this works fine if I am working with a small amount of text, but I would like to improve its performance when working with larger amounts of text. I had two thoughts on the subject:
Do the processing in the background. As the user types, this means text could be unformatted for a few seconds. I don't really like this idea.
Perform highlighting only on the visible section of text. Do more highlighting as the user scrolls. This still has the problem that as the user scrolls, the text would be unformatted but slowly pop into a formatted style.
Does anyone have any suggestions around this area? Am I missing an alternative way to do this, or should this work fine? Does anyone possibly know of any sample code doing something similar/better? I'm currently thinking of going for option #2.

I found a few resources that helped me out:
http://cocoadev.com/ImplementSyntaxHighlighting
What is the best way to implement syntax highlighting of source code in Cocoa?
NSTextView syntax highlighting
(Updated: broken cocoadev link)

Related

NSTextView undo/redo attribute changes (when not first responder)

I'm building a basic text editor with custom controls. For my text alignment control, I need to cover two user scenarios:
the text view is the first responder - make the paragraph attribute changes to textView.rangesForUserParagraphAttributeChange
the text view is not the first responder - make the paragraph attribute changes to the full text range.
Here's the method:
- (IBAction)changedTextAlignment:(NSSegmentedControl *)sender
{
NSTextAlignment align;
// ....
NSRange fullRange = NSMakeRange(0, self.textView.textStorage.length);
NSArray *changeRanges = [self.textView rangesForUserParagraphAttributeChange];
if (![self.mainWindow.firstResponder isEqual:self.textView])
{
changeRanges = #[[NSValue valueWithRange:fullRange]];
}
[self.textView shouldChangeTextInRanges:changeRanges replacementStrings:nil];
[self.textView.textStorage beginEditing];
for (NSValue *r in changeRanges)
{
#try {
NSDictionary *attrs = [self.textView.textStorage attributesAtIndex:r.rangeValue.location effectiveRange:NULL];
NSMutableParagraphStyle *pStyle = [attrs[NSParagraphStyleAttributeName] mutableCopy];
if (!pStyle)
pStyle = [[NSParagraphStyle defaultParagraphStyle] mutableCopy];
[pStyle setAlignment:align];
[self.textView.textStorage addAttributes:#{NSParagraphStyleAttributeName: pStyle}
range:r.rangeValue];
}
#catch (NSException *exception) {
NSLog(#"%#", exception);
}
}
[self.textView.textStorage endEditing];
[self.textView didChangeText];
// ....
NSMutableDictionary *typingAttrs = [self.textView.typingAttributes mutableCopy];
NSMutableParagraphStyle *pStyle = typingAttrs[NSParagraphStyleAttributeName];
if (!pStyle)
pStyle = [[NSParagraphStyle defaultParagraphStyle] mutableCopy];
[pStyle setAlignment:align];
[typingAttrs setObject:NSParagraphStyleAttributeName forKey:pStyle];
self.textView.typingAttributes = typingAttrs;
}
So both scenarios work fine... BUT undo/redo doesn't work when the change is applied in the 'not-first-responder' scenario. The undo manager pushes something onto its stack (i.e Undo is available in the Edit menu), but invoking undo doesn't change the text. All it does is visibly select the full text range.
How do I appropriately change text view attributes so that undo/redo works regardless of whether the view is first reponder or not?
Thank you in advance!
I'm not sure, but I have two suggestions. One, check the return value from shouldChangeTextInRanges:..., since perhaps the text system is refusing your proposed change; a good idea in any case. Two, I would try to make the not-first-responder case more like the first-responder case in order to try to get it to work; in particular, you might begin by selecting the full range, so that rangesForUserParagraphAttributeChange is then in fact the range that you change the attributes on. A further step in this direction would be to actually momentarily make the textview be the first responder, for the duration of your change. In that case, the two cases should really be identical, I would think. You can restore the first responder as soon as you're done. Not optimal, but it seems that AppKit is making some assumption behind the scenes that you probably just have to work around. Without getting into trying to reproduce the problem and play with it, that's the best I can offer...
The issue is a typo on my part in the code that updates the typingAttributes afterwards. Look here:
//...
NSMutableParagraphStyle *pStyle = typingAttrs[NSParagraphStyleAttributeName];
// ...
Doh! Needs to be really mutable...
//...
NSMutableParagraphStyle *pStyle = [typingAttrs[NSParagraphStyleAttributeName] mutableCopy];
// ...

NSMutableParagraphStyle ignores NSWritingDirectionNatural, is defaulting to LTR for arabic text

I'm using the NSAttributedString UIKit Additions to draw an attributed string in a UIView. The problem I have is that despite using a value of NSWritingDirectionNatural for the baseWritingDirection property of my paragraph style, text always defaults to left-to-right.
Here's how I form the attributed string (simplified):
NSString *arabic = #"العاصمة الليبية لتأمينها تنفيذا لقرار المؤتمر الوطني العام. يأتي ذلك بعدما أعلن اللواء الليبي المتقاعد خليفة حفتر أنه طلب من المجلس الأعلى للقض الدولة حتى الانتخابات النيابية القادمة";
NSMutableParagraphStyle *paragraph = [[NSMutableParagraphStyle alloc] init];
paragraph.baseWritingDirection = NSWritingDirectionNatural;
paragraph.lineBreakMode = NSLineBreakByWordWrapping;
NSMutableDictionary *attributes = [[NSMutableDictionary alloc] init];
attributes[NSParagraphStyleAttributeName] = paragraph;
NSAttributedString *string = [[NSAttributedString alloc]
initWithString:arabic
attributes:attributes];
And here's how I draw the text:
- (void)drawRect:(CGRect)rect {
[self.attributedText drawWithRect:rect
options:NSStringDrawingUsesLineFragmentOrigin
context:nil];
}
And yet it still flows from left to right:
What am I missing?
I don't believe the writing direction will be automatically set for you using baseWritingDirection unless you switch languages on the device:
"If you specify NSWritingDirectionNaturalDirection, the receiver resolves the writing direction to either NSWritingDirectionLeftToRight or NSWritingDirectionRightToLeft, depending on the direction for the user’s language preference setting."
For some reason the text you have still doesn't seem to work even with arabic selected without adding the language to your supported localizations. This character seemed to work without doing that for me: كتب
Also, it looks like Xcode reverses the characters in hardcoded arabic strings so that may be screwing with some of this copy and paste.
You can use agiletortoises's suggestion or NSLinguisticTagger's Language tag scheme to manually set the language.
I can't explain why it does not work the way you have it written, but I've been using a solution to explicitly set the direction based on known RTL languages, which used this as a starting point:
https://stackoverflow.com/a/16309559
No idea if this is related or not, but when I tested the right-to-left support in Auto Layout, it didn't work until I added a localization for that language (Arabic, Hebrew, etc) to the app.

NSTextView live text highlighting

I want to create a text view which will support highlighting of basic things, like links and hashtags. The similar features can be found in Twitter.app:
It is not necessary to support clicking on those links, just need to highlight all things properly while user is editing contents of text view.
The question is, what is the best way to do that? I don't really want to use heavy-weight syntax highlighting libraries, but I didn't find any simple and small libraries to highlight only a few things.
Should I parse text and highlight it by myself? If I should, what libraries can I use to tokenise text, and what libraries will allow me to make live highlighting?
Yes, if you want that light-weight use your own parsing to find relevant parts and then use the textStorage of NSTextView to change text attributes for the found range.
Have you tried using Regular Expressions to match your text (in background, when text is updated)? After you find matches, it is pretty simple to set required attributes (of a NSAttributedString).
You can have a look at Regular expressions in an Objective-C Cocoa application
Here is a small example:
you need to implement this delegate method, textview1 is outlet of TextView:
- (BOOL)textView:(NSTextView *)textView shouldChangeTextInRange:(NSRange)affectedCharRange replacementString:(NSString *)replacementString
{
NSLog(#"string is this");
NSString *allTheText =[textView1 string];
NSArray *lines = [allTheText componentsSeparatedByString:#""];
NSString *str=[[NSString alloc]init];
NSMutableAttributedString *attr;
BOOL isNext=YES;
[textView1 setString:#""];
for (str in lines)
{
attr=[[NSMutableAttributedString alloc]initWithString:str];
if ([str length] > 0)
{
NSRange range=NSMakeRange(0, [str length]);
[attr addAttribute:NSLinkAttributeName value:[NSColor greenColor] range:range];
[textView1 .textStorage appendAttributedString:attr];
isNext=YES;
}
else
{
NSString *str=#"";
NSAttributedString *attr=[[NSAttributedString alloc]initWithString:str];
[textView1 .textStorage appendAttributedString:attr];
isNext=NO;
}
}
}
this will give you text in blue color with hyperlink;.

When I paste text to a NSTextView, how to paste plain text only?

When I paste text to a NSTextView, I wish I can paste plain text only. All the rich text formats should be removed, include: font, color, link, and paragraph style. All the text pasted should be displayed with the default font and style of the text view. NSTextView accepts rich text by default, how to disable it?
Use isRichText = false to disable rich text.
Override this method in your NSTextView:
- (NSString *)preferredPasteboardTypeFromArray:(NSArray *)availableTypes restrictedToTypesFromArray:(NSArray *)allowedTypes {
if ([availableTypes containsObject:NSPasteboardTypeString]) {
return NSPasteboardTypeString;
}
return [super preferredPasteboardTypeFromArray:availableTypes restrictedToTypesFromArray:allowedTypes];
}
For me this worked both for pasting and drag-and-drop.
Define a custom NSTextView class with following method:
- (NSArray *)readablePasteboardTypes {
return [NSArray arrayWithObjects:NSStringPboardType,
nil];
}
Note: As of Mac OS X the new typedef for the pasteboard type is given as NSPasteboardTypeString instead of NSStringPBoardType:
- (NSArray *)readablePasteboardTypes {
return [NSArray arrayWithObjects:NSPasteboardTypeString,
nil];
}
The above solutions do resolve the questioner's issue of pasted-in text, but I think that the questioner probably wanted more than that. At least I did when I came here.
I simply want all the characters in my text field to always have the same font, regardless of whether they are inserted programatically, from the nib, pasted in, dragged in, typed in, or dropped in by Santa Claus. I searched Stack Overflow for this broader issue but did not find any questions (or answers).
Instead of the solutions given here, use this idea. In detail, give the text field a delegate which implements this…
- (void)textViewDidChangeSelection:(NSNotification *)note {
NSTextView* textView = [note object] ;
[textView setFont:[self fontIWant]] ;
}
Done. This works for all of the edge cases I could think of to test. It's a little weird, to observe a change in the selection for this. Seems like observing the string value of the view's text object, or registering for NSTextDidChangeNotification, would be more logical, but since I already had a delegate set up, and since the above was tested and given the thumbs-up by Nick Zitzmann, I went with it.

Dynamic Results and Covering Data

Today I have a question that sprouted off of this one: Database Results in Cocoa. It's regarding using the data that was returned by a database to create a certain number of questions. I am using a form of the following code (this was posted in the question).
NSMutableDictionary * interfaceElements = [[NSMutableDictionary alloc] init];
for (NSInteger i = 0; i < numberOfTextFields; ++i) {
//this is just to make a frame that's indented 10px
//and has 10px between it and the previous NSTextField (or window edge)
NSRect frame = NSMakeRect(10, (i*22 + (i+1)*10), 100, 22);
NSTextField * newField = [[NSTextField alloc] initWithFrame:frame];
//configure newField appropriately
[[myWindow contentView] addSubview:newField];
[interfaceElements setObject:newField forKey:#"someUniqueIdentifier"];
[newField release];
}
However, now when I attempt to use IFVerticallyExpandingTextfield (from http://www.cocoadev.com/index.pl?IFVerticallyExpandingTextField), or create any large amount of text, it simply goes over the other drawn content. I looked into using setAutosizingMask: on the object, but it has not worked so far.
Thanks for any help.
EDIT: What I want the effect to look like is called "Correct TextField" and what happens is called "StackOverflow Sample" - http://www.mediafire.com/?sharekey=bd476ea483deded875a4fc82078ae6c8e04e75f6e8ebb871.
EDIT 2: And if no one knows how to use this IFVerticallyExpandingTextfield class, would anyone know if there is another way to accomplish the effect?
Do you mean this?
http://developer.apple.com/documentation/Cocoa/Conceptual/CocoaDrawingGuide/GraphicsContexts/GraphicsContexts.html
Your question is not very clear to me but this might help ^^^.
Look at 'Modifying the Current Graphics State' on that page.
What about just exactly copying the code from the 'Correct textfield' example and use it in your application? Or start your application from the 'Correct texfield' example.
Also
A comment on http://www.cocoadev.com/index.pl?IFVerticallyExpandingTextField says:
Give it a try! You should be able to
throw this into a project, read the
files in Interface Builder, and use
the custom class pane to make an
NSTextField into an
IFVerticallyExpandingTextField. You'll
need to set the layout and
linebreaking attributes for word
wrapping for this to work.
Although expansion should work
properly when the textfield is
embedded in a subview, I'm having some
trouble dealing with NSScrollViews.
The field expands into the
scrollview's content view, but none of
the controls on the scrollbar appear.
Some help here would be appreciated.

Resources