NSTextView syntax highlighting - objective-c

I'm working on a Cocoa text editor which uses an NSTextView. Is it possible to change the color of certain portions of the text?

You should add your controller as the delegate of the NSTextStorage object of the NSTextView ([textView textStorage]) and then implement the delegate method ‑textStorageDidProcessEditing:. This is called whenever the text changes.
In the delegate method you need to get the current NSTextStorage object from the text view using the -textStorage method of NSTextView. NSTextStorage is a subclass of NSAttributedString and contains the attributed contents of the view.
Your code must then parse the string and apply coloring to whatever ranges of text are interesting to you. You apply color to a range using something like this, which will apply a yellow color to the whole string:
//get the range of the entire run of text
NSRange area = NSMakeRange(0, [textStorage length]);
//remove existing coloring
[textStorage removeAttribute:NSForegroundColorAttributeName range:area];
//add new coloring
[textStorage addAttribute:NSForegroundColorAttributeName
value:[NSColor yellowColor]
range:area];
How you parse the text is up to you. NSScanner is a useful class to use when parsing text.
Note that this method is by no means the most efficient way of handling syntax coloring. If the documents you are editing are very large you will most likely want to consider offloading the parsing to a separate thread and/or being clever about which sections of text are reparsed.

Rob Keniger's answer is good, but for someone looking for a more concrete example, here's a short syntax highlighter I wrote that should highlight RegEx template syntax. I want \ to be gray, with the character immediately following them to be black. I want $ to be red, with a digit character immediately following the $ to also be red. Everything else should be black. Here's my solution:
I made a template highlighter class, with a header that looks like this:
#interface RMETemplateHighlighter : NSObject <NSTextStorageDelegate>
#end
I initialize it in the nib file as an object and hook it up to my view controller with an outlet. In awakeFromNib of the view controller, I have this (where replacer is my NSTextView outlet and templateHighlighter is the outlet for the class above):
self.replacer.textStorage.delegate = self.templateHighlighter;
And my implementation looks like this:
- (void)textStorageDidProcessEditing:(NSNotification *)notification {
NSTextStorage *textStorage = notification.object;
NSString *string = textStorage.string;
NSUInteger n = string.length;
[textStorage removeAttribute:NSForegroundColorAttributeName range:NSMakeRange(0, n)];
for (NSUInteger i = 0; i < n; i++) {
unichar c = [string characterAtIndex:i];
if (c == '\\') {
[textStorage addAttribute:NSForegroundColorAttributeName value:[NSColor lightGrayColor] range:NSMakeRange(i, 1)];
i++;
} else if (c == '$') {
NSUInteger l = ((i < n - 1) && isdigit([string characterAtIndex:i+1])) ? 2 : 1;
[textStorage addAttribute:NSForegroundColorAttributeName value:[NSColor redColor] range:NSMakeRange(i, l)];
i++;
}
}
}
So there you go, a fully working example. There were a few details that had me tripped up for ~10 minutes, like the fact that you have to take the string out of textStorage to access the individual characters... maybe this save other people a few minutes.

I recommend you to start by reading the CocoaDev page about Syntax Highlighing. A lot of people have come with solutions for various goals.
If you want to perform source code syntax highlighting, I suggest you to take a look at the UKSyntaxColoredTextDocument from Uli Kusterer.

Sure. You can give the NSTextView an NSAttributedString, and some of the stuff you can do with the attributed string is apply colors to certain subranges of the string.
Or you can search on Google and see that a lot of people have done stuff with this before.
I'd probably recommend using OkudaKit.

If you are ok with WebView you can use https://github.com/ACENative/ACEView
It loads ACE editor in WebView

Related

Returning NSTextView's Selection Attributes

When you use TextEdit and have a selection of string, it will give you the selection color, font, size and other attributes as you see above. How do you get those text selection attributes? I'm certain that I need to use the selectedTextAttributes method. I have the following lines of code.
- (void)textViewDidChangeSelection:(NSNotification *)notification {
if ([notification object] == textView1) {
...
...
NSMutableDictionary *dict = [[textView1 selectedTextAttributes] mutableCopy];
NSLog(#"%#",dict);
}
}
If I run it, the result is not quite like what I expect.
NSBackgroundColor = "NSNamedColorSpace System selectedTextBackgroundColor";
NSColor = "NSNamedColorSpace System selectedTextColor";
There aren't really useful values that I can use to get the text color of the string selection and other attributes. If I ask Google about selectedTextColor, I don't get much luck.
Thank you for your help.
selectedTextAttributes describe what the selection highlighting looks like, not the attributes of selected text. I looked for quite some time for the answer to this question, and finally found it here:
Attribute String Programming Guide
Some example code. For an NSTextView* named editingView, this gathers an array of NSDictionary objects for all the differently formatted ranges in the selection.
NSMutableArray* attributes = [NSMutableArray array];
NSRange selRange = editingView.selectedRange;
NSRange effectiveRange = NSMakeRange(selRange.location, 0);
while (NSMaxRange(effectiveRange) < NSMaxRange(selRange)) {
[attributes addObject: [editingView.textStorage attributesAtIndex: NSMaxRange(effectiveRange) longestEffectiveRange: &effectiveRange inRange: selRange]];
}

Strange behavior in NSTextView when pressing tabs (Even when tabStops are set)

I'm observing a strange behavior in my NSTextView.
Assume there are multiple lines (separated by enter key presses) and when I keep pressing tabs, the whole paragraph turns into bulleted lines.
I did set the tabStops and enabled the Ruler to see the tabStops as mentioned in
Premature line wrapping in NSTextView when tabs are used
For an empty NSTextView it works fine, but when I apply it to an existing text, even though the tabStops are properly set, there is this strange behavior of turning into bulleted paragraph when pressing tabs.
Here is my code used to retrieve the existing string in the NSTextView and to set the tabStops
- (IBAction)formatTextView:(EditorTextView *)editorTextView tableWidth:(double) width
{
int cnt;
int numStops;
int tabInterval = 30;
NSTextTab *tabStop;
//attributes for attributed String of TextView
NSMutableDictionary* attrs = [[[editorTextView textStorage] attributesAtIndex:0 effectiveRange:NULL] mutableCopy];
NSParagraphStyle *paraStyle = [attrs objectForKey:NSParagraphStyleAttributeName];
NSMutableParagraphStyle *paraStyleM = [paraStyle mutableCopy];
// This first clears all tab stops, then adds tab stops, at desired intervals...
[paraStyle setTabStops:[NSArray array]];
for (cnt = 1; cnt <= numStops; cnt++) {
tabStop = [[NSTextTab alloc] initWithType:NSLeftTabStopType location: tabInterval * (cnt)];
[paraStyleM addTabStop:tabStop];
}
[attrs setObject:paraStyleM forKey:NSParagraphStyleAttributeName];
[[editorTextView textStorage] addAttributes:attrs range:NSMakeRange(0, [[[editorTextView textStorage] string] length])];
}
Is your existing text from HTML? I’m guessing it’s got some kind of <ul> thing going on in it.
They only recently (last six years?) hacked HTML ordered list and unordered list support into NSTextView to support richer messages in Mail, and it’s still pretty ugly.

Can a drag select on a NSTextView (with a NSTextTable) only select one column?

How do I make a drag select on a NSTextView with an NSAttributedString that contains a 2 column NSTextTable, only select the text in the 1st column?
i.e.
[hi | 10:00 AM]
[hello | 10:01 AM]
[what are you doing? | 10:02 AM]
[nothing, you? | 10:03 AM]
When you click the drag, the times are not selected, only the conversation. You can see skype do this here:
https://dl.dropboxusercontent.com/u/2510380/skype.mov
Update:
I think skype is using a WebView and the CSS:
-webkit-user-select: none
and then
-webkit-user-select: text
for the parts that are selectable.
Don't know how to solve this problem using NSTextView, but I found one solution using WebView + HTML5 + canvas + javascript
Names are created as canvas objects and text is just normal text in html:
When text is selected, it looks like this:
And after copy+paste you get only conversation as result:
I used canvas because using "-webkit-user-select: none;" in div, text will be copied if you select text around that div. And javascript is needed to create new canvas dynamically.
var canvas = document.createElement("canvas");
canvas.width = 400;
canvas.height = 20;
canvas.setAttribute("style", "-webkit-user-select: none;");
var context=canvas.getContext("2d");
context.font="15px Arial";
context.fillText(text,0,20);
Calling javascript function from objective-c:
[_webView stringByEvaluatingJavaScriptFromString:#"myFunction()"];
So this solution is very easy to implement.
You may create multiple tables with one column each and arrange them to give a feel of one single table.
This will allow selection of single column in a table.
Here is something that may help you in the right direction, it will only allow selection of text in between your two delimeters '[' and '|'. The only thing is once a line is partially selected it will select the whole line. This can be changed if needed by using the overlap value. I have not fully tested it as I have yet to create the .xib to match your set up.
- (NSArray *)textView:(NSTextView *)aTextView willChangeSelectionFromCharacterRanges:(NSArray *)oldSelectedCharRanges toCharacterRanges:(NSArray *)newSelectedCharRanges
{
NSMutableArray *newRanges = [[NSMutableArray alloc] init];
NSString *fullText = aTextView.string;
//Regex to find text between [ and |, the only text we should highlight
NSString *pattern = #"\[(.*?)\|";
NSRegularExpression *expression = [NSRegularExpression regularExpressionWithPattern:pattern options:0 error:nil];
NSRange range = NSMakeRange(0,[fullText length]);
[expression enumerateMatchesInString:fullText options:0 range:range usingBlock:^(NSTextCheckingResult *result, NSMatchingFlags flags, BOOL *stop)
{
NSRange matchedRange = [result rangeAtIndex:1];
//Loop through all ranges to see if they contain any allowable text
for (int i=0; i<newSelectedCharRanges.count; i++)
{
NSRange rangeToCheck = [[newSelectedCharRanges objectAtIndex:i] rangeValue];
NSRange overlap = NSIntersectionRange(rangeToCheck, matchedRange);
if (overlap.length > 0)
{
//If text has been partially selected, select whole allowable range
[newRanges addObject:[NSValue valueWithRange:matchedRange]];
}
}
}];
return newRanges;
}

extrapolate word from TextView by location

I have a TextView and I need to insert a word in a string from the location. Use this method to derive the location
NSInteger location = textView.selectedRange.location;
But I do not know how to put this into a string.
Thanks a lot
Is it what you looking for?
NSInteger location = textView.selectedRange.location;
UITextView yourTxtView;
yourTxtView.text = [NSString stringWithFormat:#"%d",location];
If you want to replace the selection with a word, the simplest would be something like this:
NSRange range = textView.selectedRange;
if (range.location != NSNotFound) {
NSString *replacement = #"some word";
textView.text = [textView.text stringByReplacingCharactersInRange:range
withString:replacement];
}
For an empty selection, this would insert the text at the caret position, if text is selected, it would be replaced. On iOS 5, using the methods in the UITextInput protocol would probably be more efficient, but also more cumbersome to use.

Truncate a string

I have a NSTableView that shows the path of files in one column. When the user resizes the tableview I want the pathname (e.g. /Users/name/testfile.m) to be resized, but I want the end of the pathname (e.g. ...name/testfile.m) to be visible and not the start (e.g. /Users/test/te...) of the path as happens by default. I wrote a function that successfully does what I want to do, but the tableview flickers while redrawing as the user scales the tableview. I think there must be a better, more elegant algorithm for doing this, but I have looked into the documentation for NSString and on Stackoverflow and I cant find anything that gives a better solution. If anyone has a more elegant solution to this problem that would be greatly appreciated. Thanks! Cheers, Trond
My current function:
-(NSString *) truncateString:(NSString *) myString withFontSize:(int) myFontSize withMaxWidth:(NSInteger) maxWidth
{
// Get the width of the current string for a given font
NSFont *font = [NSFont systemFontOfSize:myFontSize];
CGSize textSize = NSSizeToCGSize([myString sizeWithAttributes:[NSDictionary dictionaryWithObject:font forKey: NSFontAttributeName]]);
NSInteger lenURL =(int)textSize.width;
// Prepare for new truncated string
NSString *myStringShort;
NSMutableString *truncatedString = [[myString mutableCopy] autorelease];
// If the available width is smaller than the string, start truncating from first character
if (lenURL > maxWidth)
{
// Get range for first character in string
NSRange range = {0, 1};
while ([truncatedString sizeWithAttributes:[NSDictionary dictionaryWithObject:font forKey: NSFontAttributeName]].width > MAX(TKstringPad,maxWidth))
{
// Delete character at start of string
[truncatedString deleteCharactersInRange:range];
}
myStringShort = [NSString stringWithFormat:#"...%#",truncatedString];
}
else
{
myStringShort=myString;
}
return myStringShort;
}
The typical approach would be simply:
[tableViewCell setLineBreakMode:NSLineBreakByTruncatingHead];
As Dondragmer noted, this property may also be set in Xcode's NIB editor.