-
Notifications
You must be signed in to change notification settings - Fork 59
Expand file tree
/
Copy pathJSTTextView.m
More file actions
857 lines (623 loc) · 31.3 KB
/
JSTTextView.m
File metadata and controls
857 lines (623 loc) · 31.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
//
// JSTTextView.m
// jstalk
//
// Created by August Mueller on 1/18/09.
// Copyright 2009 Flying Meat Inc. All rights reserved.
//
#import "JSTTextView.h"
#import "MarkerLineNumberView.h"
#import "TDParseKit.h"
#import "NoodleLineNumberView.h"
#import "TETextUtils.h"
#import <Carbon/Carbon.h>
static NSString *JSTQuotedStringAttributeName = @"JSTQuotedString";
@interface JSTTextView ()
@property (assign) NSRange currentlyHighlightedRange;
@property (assign) NSRange initialNumberRange;
@property (assign) NSRange initialDragCommandRange;
@property (assign) CGPoint initialDragPoint;
@property (strong) NSNumber *initialNumber;
@property (strong) NSMutableDictionary *numberRanges;
@property (assign) BOOL parsingInResponseToEdit;
@end
@implementation JSTTextView
@synthesize keywords=_keywords;
@synthesize lastAutoInsert=_lastAutoInsert;
- (id)initWithFrame:(NSRect)frameRect textContainer:(NSTextContainer *)container {
self = [super initWithFrame:frameRect textContainer:container];
if (self != nil) {
[self performSelector:@selector(setupLineViewAndStuff) withObject:nil afterDelay:0];
[self setSmartInsertDeleteEnabled:NO];
[self setAutomaticQuoteSubstitutionEnabled:NO];
}
return self;
}
- (id)initWithCoder:(NSCoder *)aDecoder {
self = [super initWithCoder:aDecoder];
if (self != nil) {
// what's the right way to do this?
[self performSelector:@selector(setupLineViewAndStuff) withObject:nil afterDelay:0];
[self setSmartInsertDeleteEnabled:NO];
[self setAutomaticQuoteSubstitutionEnabled:NO];
}
return self;
}
- (void)dealloc {
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
- (void)setupLineViewAndStuff {
NoodleLineNumberView *numberView = [[NoodleLineNumberView alloc] initWithScrollView:[self enclosingScrollView]];
[[self enclosingScrollView] setVerticalRulerView:numberView];
[[self enclosingScrollView] setHasHorizontalRuler:NO];
[[self enclosingScrollView] setHasVerticalRuler:YES];
[[self enclosingScrollView] setRulersVisible:YES];
[[self textStorage] setDelegate:self];
/*
var s = "break case catch continue default delete do else finally for function if in instanceof new return switch this throw try typeof var void while with abstract boolean byte char class const debugger double enum export extends final float goto implements import int interface long native package private protected public short static super synchronized throws transient volatile null true false nil id CGFloat NSInteger NSUInteger bool BOOL"
words = s.split(" ")
var i = 0;
list = ""
while (i < words.length) {
list = list + '@"' + words[i] + '", ';
i++
}
print("NSArray *blueWords = [NSArray arrayWithObjects:" + list + " nil];")
*/
NSArray *blueWords = [NSArray arrayWithObjects:@"break", @"case", @"catch", @"continue", @"default", @"delete", @"do", @"else", @"finally", @"for", @"function", @"if", @"in", @"instanceof", @"new", @"return", @"switch", @"this", @"throw", @"try", @"typeof", @"var", @"void", @"while", @"with", @"abstract", @"boolean", @"byte", @"char", @"class", @"const", @"debugger", @"double", @"enum", @"export", @"extends", @"final", @"float", @"goto", @"implements", @"import", @"int", @"interface", @"long", @"native", @"package", @"private", @"protected", @"public", @"short", @"static", @"super", @"synchronized", @"throws", @"transient", @"volatile", @"null", @"true", @"false", @"nil", @"id", @"CGFloat", @"NSInteger", @"NSUInteger", @"bool", @"BOOL", nil];
NSMutableDictionary *keywords = [NSMutableDictionary dictionary];
for (NSString *word in blueWords) {
[keywords setObject:[NSColor blueColor] forKey:word];
}
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(textViewDidChangeSelection:) name:NSTextViewDidChangeSelectionNotification object:self];
self.keywords = keywords;
self.numberRanges = [NSMutableDictionary new];
[self parseCode:nil];
}
- (void)parseCode:(id)sender {
// we should really do substrings...
NSString *sourceString = [[self textStorage] string];
TDTokenizer *tokenizer = [TDTokenizer tokenizerWithString:sourceString];
tokenizer.commentState.reportsCommentTokens = YES;
tokenizer.whitespaceState.reportsWhitespaceTokens = YES;
TDToken *eof = [TDToken EOFToken];
TDToken *tok = nil;
[[self textStorage] beginEditing];
[self.numberRanges removeAllObjects];
NSUInteger sourceLoc = 0;
while ((tok = [tokenizer nextToken]) != eof) {
NSUInteger strLen = [[tok stringValue] length];
NSRange tokenRange = NSMakeRange(sourceLoc, strLen);
NSColor *fontColor = [NSColor blackColor];
if ([tok isQuotedString]) {
fontColor = [NSColor darkGrayColor];
}
else if ([tok isNumber]) {
fontColor = [NSColor blueColor];
[self setNumberString:[tok stringValue] forRange:tokenRange];
}
else if ([tok isComment]) {
fontColor = [NSColor redColor];
}
else if ([tok isWord]) {
NSColor *c = [_keywords objectForKey:[tok stringValue]];
fontColor = c ? c : fontColor;
}
if (fontColor) {
[[self textStorage] addAttribute:NSForegroundColorAttributeName value:fontColor range:tokenRange];
}
if ([tok isQuotedString]) {
[[self textStorage] addAttribute:JSTQuotedStringAttributeName value:[NSNumber numberWithBool:YES] range:tokenRange];
}
else {
[[self textStorage] removeAttribute:JSTQuotedStringAttributeName range:tokenRange];
}
sourceLoc += strLen;
}
[[self textStorage] endEditing];
}
- (void)textStorage:(NSTextStorage *)textStorage didProcessEditing:(NSTextStorageEditActions)editedMask range:(NSRange)editedRange changeInLength:(NSInteger)delta {
// SD: calling parseCode directly from here was causing the selection to be messed up when deleting characters - seemingly some sort of timing issue.
// deferring the parse has fixed that, but at the expense of causing potential recursion since the parse then seems to register as another edit.
// to avoid this, I've added the parsingInResponseToEdit flag, but it's all a bit clumsy; a better fix might be to sort out the original deletion problem
if (!self.parsingInResponseToEdit) {
dispatch_async(dispatch_get_main_queue(), ^{
self.parsingInResponseToEdit = YES;
[self parseCode:nil];
self.parsingInResponseToEdit = NO;
});
}
}
- (NSArray *)writablePasteboardTypes {
return [[super writablePasteboardTypes] arrayByAddingObject:NSRTFPboardType];
}
- (void)insertTab:(id)sender {
[self insertText:@" "];
}
- (void)autoInsertText:(NSString*)text {
[super insertText:text];
[self setLastAutoInsert:text];
}
- (void)insertText:(id)insertString replacementRange:(NSRange)replacementRange {
if (!([JSTPrefs boolForKey:@"codeCompletionEnabled"])) {
[super insertText:insertString replacementRange:replacementRange];
return;
}
// make sure we're not doing anything fance in a quoted string.
if (NSMaxRange(replacementRange) < [[self textStorage] length] && [[[self textStorage] attributesAtIndex:replacementRange.location effectiveRange:nil] objectForKey:JSTQuotedStringAttributeName]) {
[super insertText:insertString replacementRange:replacementRange];
return;
}
if ([@")" isEqualToString:insertString] && [_lastAutoInsert isEqualToString:@")"]) {
NSRange nextRange = replacementRange;
nextRange.length = 1;
if (NSMaxRange(nextRange) <= [[self textStorage] length]) {
NSString *next = [[[self textStorage] mutableString] substringWithRange:nextRange];
if ([@")" isEqualToString:next]) {
// just move our selection over.
nextRange.length = 0;
nextRange.location++;
[self setSelectedRange:nextRange];
return;
}
}
}
[self setLastAutoInsert:nil];
[super insertText:insertString replacementRange:replacementRange];
NSRange currentRange = [self selectedRange];
NSRange r = [self selectionRangeForProposedRange:currentRange granularity:NSSelectByParagraph];
BOOL atEndOfLine = (NSMaxRange(r) - 1 == NSMaxRange(currentRange));
if (atEndOfLine && [@"{" isEqualToString:insertString]) {
r = [self selectionRangeForProposedRange:currentRange granularity:NSSelectByParagraph];
NSString *myLine = [[[self textStorage] mutableString] substringWithRange:r];
NSMutableString *indent = [NSMutableString string];
int j = 0;
while (j < [myLine length] && ([myLine characterAtIndex:j] == ' ' || [myLine characterAtIndex:j] == '\t')) {
[indent appendFormat:@"%C", [myLine characterAtIndex:j]];
j++;
}
[self autoInsertText:[NSString stringWithFormat:@"\n%@ \n%@}", indent, indent]];
currentRange.location += [indent length] + 5;
[self setSelectedRange:currentRange];
}
else if (atEndOfLine && [@"(" isEqualToString:insertString]) {
[self autoInsertText:@")"];
[self setSelectedRange:currentRange];
}
else if (atEndOfLine && [@"[" isEqualToString:insertString]) {
[self autoInsertText:@"]"];
[self setSelectedRange:currentRange];
}
else if ([@"\"" isEqualToString:insertString]) {
[self autoInsertText:@"\""];
[self setSelectedRange:currentRange];
}
}
- (void)insertNewline:(id)sender {
[super insertNewline:sender];
if ([JSTPrefs boolForKey:@"codeCompletionEnabled"]) {
NSRange r = [self selectedRange];
if (r.location > 0) {
r.location --;
}
r = [self selectionRangeForProposedRange:r granularity:NSSelectByParagraph];
NSString *previousLine = [[[self textStorage] mutableString] substringWithRange:r];
int j = 0;
while (j < [previousLine length] && ([previousLine characterAtIndex:j] == ' ' || [previousLine characterAtIndex:j] == '\t')) {
j++;
}
if (j > 0) {
NSString *foo = [[[self textStorage] mutableString] substringWithRange:NSMakeRange(r.location, j)];
[self insertText:foo];
}
}
}
- (BOOL)xrespondsToSelector:(SEL)aSelector {
debug(@"%@: %@?", [self class], NSStringFromSelector(aSelector));
return [super respondsToSelector:aSelector];
}
- (void)changeSelectedNumberByDelta:(NSInteger)d {
NSRange r = [self selectedRange];
NSRange wr = [self selectionRangeForProposedRange:r granularity:NSSelectByWord];
NSString *s = [[[self textStorage] mutableString] substringWithRange:wr];
NSInteger i = [s integerValue];
if ([s isEqualToString:[NSString stringWithFormat:@"%ld", (long)i]]) {
NSString *newString = [NSString stringWithFormat:@"%ld", (long)(i+d)];
if ([self shouldChangeTextInRange:wr replacementString:newString]) { // auto undo.
[[self textStorage] replaceCharactersInRange:wr withString:newString];
[self didChangeText];
r.length = 0;
[self setSelectedRange:r];
}
}
}
/*
- (void)moveForward:(id)sender {
debug(@"%s:%d", __FUNCTION__, __LINE__);
#pragma message "this really really needs to be a pref"
// defaults write org.jstalk.JSTalkEditor optionNumberIncrement 1
if ([JSTPrefs boolForKey:@"optionNumberIncrement"]) {
[self changeSelectedNumberByDelta:-1];
}
else {
[super moveForward:sender];
}
}
- (void)moveBackward:(id)sender {
if ([JSTPrefs boolForKey:@"optionNumberIncrement"]) {
[self changeSelectedNumberByDelta:1];
}
else {
[super moveBackward:sender];
}
}
- (void)moveToEndOfParagraph:(id)sender {
if (![JSTPrefs boolForKey:@"optionNumberIncrement"] || (([NSEvent modifierFlags] & NSAlternateKeyMask) != 0)) {
[super moveToEndOfParagraph:sender];
}
}
- (void)moveToBeginningOfParagraph:(id)sender {
if (![JSTPrefs boolForKey:@"optionNumberIncrement"] || (([NSEvent modifierFlags] & NSAlternateKeyMask) != 0)) {
[super moveToBeginningOfParagraph:sender];
}
}
*/
/*
- (void)moveDown:(id)sender {
debug(@"%s:%d", __FUNCTION__, __LINE__);
}
- (void)moveDownAndModifySelection:(id)sender {
debug(@"%s:%d", __FUNCTION__, __LINE__);
}
*/
// Mimic BBEdit's option-delete behavior, which is THE WAY IT SHOULD BE DONE
- (void)deleteWordForward:(id)sender {
NSRange r = [self selectedRange];
NSUInteger textLength = [[self textStorage] length];
if (r.length || (NSMaxRange(r) >= textLength)) {
[super deleteWordForward:sender];
return;
}
// delete the whitespace forward.
NSRange paraRange = [self selectionRangeForProposedRange:r granularity:NSSelectByParagraph];
NSUInteger diff = r.location - paraRange.location;
paraRange.location += diff;
paraRange.length -= diff;
NSString *foo = [[[self textStorage] string] substringWithRange:paraRange];
NSUInteger len = 0;
while ([foo characterAtIndex:len] == ' ' && len < paraRange.length) {
len++;
}
if (!len) {
[super deleteWordForward:sender];
return;
}
r.length = len;
if ([self shouldChangeTextInRange:r replacementString:@""]) { // auto undo.
[self replaceCharactersInRange:r withString:@""];
}
}
- (void)deleteBackward:(id)sender {
if ([[self delegate] respondsToSelector:@selector(textView:doCommandBySelector:)]) {
// If the delegate wants a crack at command selectors, give it a crack at the standard selector too.
if ([[self delegate] textView:self doCommandBySelector:@selector(deleteBackward:)]) {
return;
}
}
else {
NSRange charRange = [self rangeForUserTextChange];
if (charRange.location != NSNotFound) {
if (charRange.length > 0) {
// Non-zero selection. Delete normally.
[super deleteBackward:sender];
} else {
if (charRange.location == 0) {
// At beginning of text. Delete normally.
[super deleteBackward:sender];
} else {
NSString *string = [self string];
NSRange paraRange = [string lineRangeForRange:NSMakeRange(charRange.location - 1, 1)];
if (paraRange.location == charRange.location) {
// At beginning of line. Delete normally.
[super deleteBackward:sender];
} else {
unsigned tabWidth = 4; //[[TEPreferencesController sharedPreferencesController] tabWidth];
unsigned indentWidth = 4;// [[TEPreferencesController sharedPreferencesController] indentWidth];
BOOL usesTabs = NO; //[[TEPreferencesController sharedPreferencesController] usesTabs];
NSRange leadingSpaceRange = paraRange;
unsigned leadingSpaces = TE_numberOfLeadingSpacesFromRangeInString(string, &leadingSpaceRange, tabWidth);
if (charRange.location > NSMaxRange(leadingSpaceRange)) {
// Not in leading whitespace. Delete normally.
[super deleteBackward:sender];
} else {
NSTextStorage *text = [self textStorage];
unsigned leadingIndents = leadingSpaces / indentWidth;
NSString *replaceString;
// If we were indented to an fractional level just go back to the last even multiple of indentWidth, if we were exactly on, go back a full level.
if (leadingSpaces % indentWidth == 0) {
leadingIndents--;
}
leadingSpaces = leadingIndents * indentWidth;
replaceString = ((leadingSpaces > 0) ? TE_tabbifiedStringWithNumberOfSpaces(leadingSpaces, tabWidth, usesTabs) : @"");
if ([self shouldChangeTextInRange:leadingSpaceRange replacementString:replaceString]) {
NSDictionary *newTypingAttributes;
if (charRange.location < [string length]) {
newTypingAttributes = [text attributesAtIndex:charRange.location effectiveRange:NULL];
} else {
newTypingAttributes = [text attributesAtIndex:(charRange.location - 1) effectiveRange:NULL];
}
[text replaceCharactersInRange:leadingSpaceRange withString:replaceString];
[self setTypingAttributes:newTypingAttributes];
[self didChangeText];
}
}
}
}
}
}
}
}
- (void)TE_doUserIndentByNumberOfLevels:(int)levels {
// Because of the way paragraph ranges work we will add spaces a final paragraph separator only if the selection is an insertion point at the end of the text.
// We ask for rangeForUserTextChange and extend it to paragraph boundaries instead of asking rangeForUserParagraphAttributeChange because this is not an attribute change and we don't want it to be affected by the usesRuler setting.
NSRange charRange = [[self string] lineRangeForRange:[self rangeForUserTextChange]];
NSRange selRange = [self selectedRange];
if (charRange.location != NSNotFound) {
NSTextStorage *textStorage = [self textStorage];
NSAttributedString *newText;
unsigned tabWidth = 4;
unsigned indentWidth = 4;
BOOL usesTabs = NO;
selRange.location -= charRange.location;
newText = TE_attributedStringByIndentingParagraphs([textStorage attributedSubstringFromRange:charRange], levels, &selRange, [self typingAttributes], tabWidth, indentWidth, usesTabs);
selRange.location += charRange.location;
if ([self shouldChangeTextInRange:charRange replacementString:[newText string]]) {
[[textStorage mutableString] replaceCharactersInRange:charRange withString:[newText string]];
//[textStorage replaceCharactersInRange:charRange withAttributedString:newText];
[self setSelectedRange:selRange];
[self didChangeText];
}
}
}
- (void)shiftLeft:(id)sender {
[self TE_doUserIndentByNumberOfLevels:-1];
}
- (void)shiftRight:(id)sender {
[self TE_doUserIndentByNumberOfLevels:1];
}
- (void)textViewDidChangeSelection:(NSNotification *)notification {
NSTextView *textView = [notification object];
NSRange selRange = [textView selectedRange];
//TEPreferencesController *prefs = [TEPreferencesController sharedPreferencesController];
//if ([prefs selectToMatchingBrace]) {
if (YES) {
// The NSTextViewDidChangeSelectionNotification is sent before the selection granularity is set. Therefore we can't tell a double-click by examining the granularity. Fortunately there's another way. The mouse-up event that ended the selection is still the current event for the app. We'll check that instead. Perhaps, in an ideal world, after checking the length we'd do this instead: ([textView selectionGranularity] == NSSelectByWord).
if ((selRange.length == 1) && ([[NSApp currentEvent] type] == NSLeftMouseUp) && ([[NSApp currentEvent] clickCount] == 2)) {
NSRange matchRange = TE_findMatchingBraceForRangeInString(selRange, [textView string]);
if (matchRange.location != NSNotFound) {
selRange = NSUnionRange(selRange, matchRange);
[textView setSelectedRange:selRange];
[textView scrollRangeToVisible:matchRange];
}
}
}
//if ([prefs showMatchingBrace]) {
if (YES) {
NSRange oldSelRangePtr;
[[[notification userInfo] objectForKey:@"NSOldSelectedCharacterRange"] getValue:&oldSelRangePtr];
// This test will catch typing sel changes, also it will catch right arrow sel changes, which I guess we can live with. MF:??? Maybe we should catch left arrow changes too for consistency...
if ((selRange.length == 0) && (selRange.location > 0) && ([[NSApp currentEvent] type] == NSKeyDown) && (oldSelRangePtr.location == selRange.location - 1)) {
NSRange origRange = NSMakeRange(selRange.location - 1, 1);
unichar origChar = [[textView string] characterAtIndex:origRange.location];
if (TE_isClosingBrace(origChar)) {
NSRange matchRange = TE_findMatchingBraceForRangeInString(origRange, [textView string]);
if (matchRange.location != NSNotFound) {
// do this with a delay, since for some reason it only works when we use the arrow keys otherwise.
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
dispatch_async(dispatch_get_main_queue(), ^{
[self showFindIndicatorForRange:matchRange];
});
});
}
}
}
}
}
- (NSRange)selectionRangeForProposedRange:(NSRange)proposedSelRange granularity:(NSSelectionGranularity)granularity {
// check for cases where we've got: [foo setValue:bar forKey:"x"]; and we double click on setValue. The default way NSTextView does the selection
// is to have it highlight all of setValue:bar, which isn't what we want. So.. we mix it up a bit.
// There's probably a better way to do this, but I don't currently know what it is.
if (granularity == NSSelectByWord && ([[NSApp currentEvent] type] == NSLeftMouseUp || [[NSApp currentEvent] type] == NSLeftMouseDown) && [[NSApp currentEvent] clickCount] > 1) {
NSRange r = [super selectionRangeForProposedRange:proposedSelRange granularity:granularity];
NSString *s = [[[self textStorage] mutableString] substringWithRange:r];
NSRange colLocation = [s rangeOfString:@":"];
if (colLocation.location != NSNotFound) {
if (proposedSelRange.location > (r.location + colLocation.location)) {
r.location = r.location + colLocation.location + 1;
r.length = [s length] - colLocation.location - 1;
}
else {
r.length = colLocation.location;
}
}
return r;
}
return [super selectionRangeForProposedRange:proposedSelRange granularity:granularity];
}
- (void)setInsertionPointFromDragOpertion:(id <NSDraggingInfo>)sender {
NSLayoutManager *layoutManager = [self layoutManager];
NSTextContainer *textContainer = [self textContainer];
NSUInteger glyphIndex, charIndex, length = [[self textStorage] length];
NSPoint point = [self convertPoint:[sender draggingLocation] fromView:nil];
// Convert those coordinates to the nearest glyph index
glyphIndex = [layoutManager glyphIndexForPoint:point inTextContainer:textContainer];
// Convert the glyph index to a character index
charIndex = [layoutManager characterIndexForGlyphAtIndex:glyphIndex];
// put the selection where we have the mouse.
if (charIndex == (length - 1)) {
[self setSelectedRange:NSMakeRange(charIndex+1, 0)];
//[self setSelectedRange:NSMakeRange(charIndex, 0)];
}
else {
[self setSelectedRange:NSMakeRange(charIndex, 0)];
}
}
- (BOOL)performDragOperation:(id <NSDraggingInfo>)sender {
if (([NSEvent modifierFlags] & NSAlternateKeyMask) != 0) {
[self setInsertionPointFromDragOpertion:sender];
NSPasteboard *paste = [sender draggingPasteboard];
NSArray *fileArray = [paste propertyListForType:NSFilenamesPboardType];
for (NSString *path in fileArray) {
[self insertText:[NSString stringWithFormat:@"[NSURL fileURLWithPath:\"%@\"]", path]];
if ([fileArray count] > 1) {
[self insertNewline:nil];
}
}
return YES;
}
return [super performDragOperation:sender];
}
#pragma mark - NSResponder methods
- (void)mouseMoved:(NSEvent *)theEvent {
// only do this if only the command key is down.
if ((GetCurrentKeyModifiers() != cmdKey)) {
[super mouseMoved:theEvent];
return;
}
[[self textStorage] removeAttribute:NSBackgroundColorAttributeName range:self.currentlyHighlightedRange];
NSUInteger character = [self characterIndexForPoint:[NSEvent mouseLocation]];
NSRange range = [self numberStringRangeForCharacterIndex:character];
if (range.location == NSNotFound) {
if (_currentlyHighlightedRange.location != NSNotFound) {
// Only change this when it's not already set... skip some work, I suppose.
self.currentlyHighlightedRange = range;
}
[[NSCursor arrowCursor] set];
return;
}
// Found a number under the cursor
self.currentlyHighlightedRange = range;
NSColor *fontColor = [NSColor colorWithCalibratedRed:0.742 green:0.898 blue:0.397 alpha:1.000];
[[self textStorage] addAttribute:NSBackgroundColorAttributeName value:fontColor range:range];
// Show we can drag the number
[[NSCursor resizeLeftRightCursor] set];
}
- (void)mouseDown:(NSEvent *)theEvent {
if (self.currentlyHighlightedRange.location == NSNotFound) {
[super mouseDown:theEvent];
return;
}
if ((GetCurrentKeyModifiers() != cmdKey)) {
[super mouseDown:theEvent];
return;
}
self.initialDragPoint = [NSEvent mouseLocation];
NSString *initialString = [[self string] substringWithRange:self.currentlyHighlightedRange];
self.initialNumber = [self numberFromString:initialString];
NSString *wholeText = [self string];
self.initialNumberRange = self.currentlyHighlightedRange;
NSRange originalCommandRange = [wholeText lineRangeForRange:self.currentlyHighlightedRange];
self.initialDragCommandRange = originalCommandRange;
}
- (void)mouseDragged:(NSEvent *)theEvent {
// Skip it if we're not currently dragging a number
if (self.currentlyHighlightedRange.location == NSNotFound) {
[super mouseDragged:theEvent];
return;
}
if ((GetCurrentKeyModifiers() != cmdKey)) {
[super mouseDragged:theEvent];
return;
}
NSRange numberRange = [self rangeForNumberNearestToIndex:self.currentlyHighlightedRange.location];
NSString *numberString = [[self string] substringWithRange:numberRange];
NSNumber *number = [self numberFromString:numberString];
if (nil == number) {
NSLog(@"Couldn't parse a number out of :%@", numberString);
return;
}
CGPoint screenPoint = [NSEvent mouseLocation];
CGFloat x = screenPoint.x - self.initialDragPoint.x;
CGFloat y = screenPoint.y - self.initialDragPoint.y;
CGSize offset = CGSizeMake(x, y);
NSInteger offsetValue = [self.initialNumber integerValue] + (NSInteger)offset.width;
NSNumber *updatedNumber = @(offsetValue);
NSString *updatedNumberString = [updatedNumber stringValue];
[[self textStorage] replaceCharactersInRange:self.currentlyHighlightedRange withString:updatedNumberString];
self.currentlyHighlightedRange = NSMakeRange(self.currentlyHighlightedRange.location, [updatedNumberString length]);
if (self.numberDragHandler) {
self.numberDragHandler(self, [self currentLineForRange:self.currentlyHighlightedRange]);
}
}
- (void)mouseUp:(NSEvent *)theEvent {
// Skip it if we're not currently dragging a word
if (self.currentlyHighlightedRange.location == NSNotFound) {
[super mouseUp:theEvent];
return;
}
if ((GetCurrentKeyModifiers() != cmdKey)) {
[super mouseUp:theEvent];
return;
}
// Triggers clearing out our number-dragging state.
[self parseCode:nil];
[self mouseMoved:theEvent];
self.initialNumber = nil;
self.initialDragCommandRange = NSMakeRange(NSNotFound, NSNotFound);
}
#pragma mark - Number dragging helpers
- (void)setNumberString:(NSString *)string forRange:(NSRange)numberRange {
// Just store the start location of the number, because the length might change (if, say, number goes from 100 -> 99)
self.numberRanges[NSStringFromRange(numberRange)] = string;
}
- (NSRange)numberStringRangeForCharacterIndex:(NSUInteger)character {
for (NSString *rangeString in self.numberRanges) {
NSRange range = NSRangeFromString(rangeString);
if (NSLocationInRange(character, range)) {
return range;
}
}
return NSMakeRange(NSNotFound, 0);
}
- (NSNumber *)numberFromString:(NSString *)string {
static NSNumberFormatter *formatter = nil;
if (nil == formatter) {
formatter = [[NSNumberFormatter alloc] init];
[formatter setAllowsFloats:YES];
}
return [formatter numberFromString:string];
}
- (NSRange)rangeForNumberNearestToIndex:(NSUInteger)index {
// parse this out right now...
NSRange originalRange = self.initialDragCommandRange;
// Gets the line in range as it is currently in the textview's string
NSString *currentLine = [self currentLineForRange:originalRange];
TDTokenizer *tokenizer = [TDTokenizer tokenizerWithString:currentLine];
tokenizer.commentState.reportsCommentTokens = YES;
tokenizer.whitespaceState.reportsWhitespaceTokens = YES;
TDToken *eof = [TDToken EOFToken];
TDToken *token = nil;
NSUInteger currentLocation = 0; // in the command!
while ((token = [tokenizer nextToken]) != eof) {
NSRange numberRange = NSMakeRange(currentLocation + originalRange.location, [[token stringValue] length]);
if ([token isNumber]) {
if (NSLocationInRange(index, numberRange)) {
return numberRange;
}
}
currentLocation += [[token stringValue] length];
}
return NSMakeRange(NSNotFound, NSNotFound);
}
- (NSString *)currentLineForRange:(NSRange)originalRange {
NSString *wholeString = [self string];
NSRange lineRange = [wholeString lineRangeForRange:originalRange];
return [wholeString substringWithRange:lineRange];
}
@end
// stolen from NSTextStorage_TETextExtras.m
@implementation NSTextStorage (TETextExtras)
- (BOOL)_usesProgrammingLanguageBreaks {
return YES;
}
@end