mirror of
https://github.com/MacDownApp/macdown.git
synced 2026-06-16 12:44:38 +00:00
731 lines
25 KiB
Objective-C
731 lines
25 KiB
Objective-C
//
|
|
// NSTextView+Autocomplete.m
|
|
// MacDown
|
|
//
|
|
// Created by Tzu-ping Chung on 11/06/2014.
|
|
// Copyright (c) 2014 Tzu-ping Chung . All rights reserved.
|
|
//
|
|
|
|
#import "NSTextView+Autocomplete.h"
|
|
#import "NSString+Lookup.h"
|
|
#import "MPUtilities.h"
|
|
|
|
|
|
static const unichar kMPLeftSingleQuotation = L'\u2018';
|
|
static const unichar kMPRightSingleQuotation = L'\u2019';
|
|
static const unichar kMPLeftDoubleQuotation = L'\u201c';
|
|
static const unichar kMPRightDoubleQuotation = L'\u201d';
|
|
static const unichar kMPLeftAngleSingleQuotation = L'\u2039';
|
|
static const unichar kMPRightAngleSingleQuotation = L'\u203a';
|
|
static const unichar kMPLeftAngleDoubleQuotation = L'\u00ab';
|
|
static const unichar kMPRightAngleDoubleQuotation = L'\u00bb';
|
|
static const unichar kMPLeftAngleSingleBracket = L'\u3008';
|
|
static const unichar kMPRightAngleSingleBracket = L'\u3009';
|
|
static const unichar kMPLeftAngleDoubleBracket = L'\u300a';
|
|
static const unichar kMPRightAngleDoubleBracket = L'\u300b';
|
|
|
|
static const unichar kMPMatchingCharactersMap[][2] = {
|
|
{L'(', L')'},
|
|
{L'[', L']'},
|
|
{L'{', L'}'},
|
|
{L'<', L'>'},
|
|
{L'\'', L'\''},
|
|
{L'\"', L'\"'},
|
|
{L'\uff08', L'\uff09'}, // full-width parentheses
|
|
{L'\u300c', L'\u300d'}, // corner brackets
|
|
{L'\u300e', L'\u300f'}, // white corner brackets
|
|
{kMPLeftSingleQuotation, kMPRightSingleQuotation},
|
|
{kMPLeftDoubleQuotation, kMPRightDoubleQuotation},
|
|
{kMPLeftAngleSingleQuotation, kMPRightAngleSingleQuotation}, // Latin Single Guillemet
|
|
{kMPLeftAngleDoubleQuotation, kMPRightAngleDoubleQuotation}, // Latin Double Guillemet
|
|
{kMPLeftAngleSingleBracket, kMPRightAngleSingleBracket}, // East Asian Single Guillemet
|
|
{kMPLeftAngleDoubleBracket, kMPRightAngleDoubleBracket}, // East Asian Double Guillemet
|
|
{L'\0', L'\0'},
|
|
};
|
|
|
|
static const unichar kMPStrikethroughCharacter = L'~';
|
|
|
|
static const unichar kMPMarkupCharacters[] = {
|
|
L'*', L'_', L'`', L'=', L'\0',
|
|
};
|
|
|
|
static NSString * const kMPListLineHeadPattern =
|
|
@"^(\\s*)((?:(?:\\*|\\+|-|)\\s+)?)((?:\\d+\\.\\s+)?)(\\S)?";
|
|
static NSString * const kMPBlockquoteLinePattern = @"^((?:\\> ?)+).*$";
|
|
|
|
|
|
@implementation NSTextView (Autocomplete)
|
|
|
|
- (BOOL)substringInRange:(NSRange)range isSurroundedByPrefix:(NSString *)prefix
|
|
suffix:(NSString *)suffix
|
|
{
|
|
NSString *content = self.string;
|
|
NSUInteger location = range.location;
|
|
NSUInteger length = range.length;
|
|
if (content.length < location + length + suffix.length)
|
|
return NO;
|
|
if (location < prefix.length)
|
|
return NO;
|
|
|
|
if (![[content substringFromIndex:location + length] hasPrefix:suffix]
|
|
|| ![[content substringToIndex:location] hasSuffix:prefix])
|
|
return NO;
|
|
|
|
// Emphasis (*) requires special treatment because we need to eliminate
|
|
// strong (**) but not strong-emphasis (***).
|
|
if (![prefix isEqualToString:@"*"] || ![suffix isEqualToString:@"*"])
|
|
return YES;
|
|
if ([self substringInRange:range isSurroundedByPrefix:@"***" suffix:@"***"])
|
|
return YES;
|
|
if ([self substringInRange:range isSurroundedByPrefix:@"**" suffix:@"**"])
|
|
return NO;
|
|
return YES;
|
|
}
|
|
|
|
|
|
- (void)insertSpacesForTab
|
|
{
|
|
NSString *spaces = @" ";
|
|
NSUInteger currentLocation = self.selectedRange.location;
|
|
NSInteger p = [self.string locationOfFirstNewlineBefore:currentLocation];
|
|
|
|
// Calculate how deep we need to go.
|
|
NSUInteger offset = (currentLocation - p - 1) % 4;
|
|
if (offset)
|
|
spaces = [spaces substringFromIndex:offset];
|
|
[self insertText:spaces];
|
|
}
|
|
|
|
- (BOOL)completeMatchingCharactersForTextInRange:(NSRange)range
|
|
withString:(NSString *)str
|
|
strikethroughEnabled:(BOOL)strikethrough
|
|
{
|
|
NSUInteger stringLength = str.length;
|
|
|
|
// Character insert without selection.
|
|
if (range.length == 0 && stringLength == 1)
|
|
{
|
|
NSUInteger location = range.location;
|
|
if ([self completeMatchingCharacterForText:str
|
|
atLocation:location])
|
|
return YES;
|
|
}
|
|
// Character insert with selection (i.e. select and replace).
|
|
else if (range.length > 0 && stringLength == 1)
|
|
{
|
|
unichar character = [str characterAtIndex:0];
|
|
if ([self wrapMatchingCharactersOfCharacter:character
|
|
aroundTextInRange:range
|
|
strikethroughEnabled:strikethrough])
|
|
return YES;
|
|
}
|
|
return NO;
|
|
}
|
|
|
|
- (BOOL)completeMatchingCharacterForText:(NSString *)string
|
|
atLocation:(NSUInteger)location
|
|
{
|
|
static NSCharacterSet *boundaryCharacters = nil;
|
|
static dispatch_once_t onceToken;
|
|
dispatch_once(&onceToken, ^{
|
|
NSMutableCharacterSet *s =
|
|
[NSMutableCharacterSet whitespaceAndNewlineCharacterSet];
|
|
[s formUnionWithCharacterSet:[NSCharacterSet punctuationCharacterSet]];
|
|
boundaryCharacters = [s copy];
|
|
});
|
|
NSString *content = self.string;
|
|
NSUInteger contentLength = content.length;
|
|
|
|
BOOL hasMarkedText = self.hasMarkedText;
|
|
unichar c = [string characterAtIndex:0];
|
|
unichar n = ' ';
|
|
unichar p = ' ';
|
|
if (location < contentLength)
|
|
n = [content characterAtIndex:location];
|
|
if (location > 0 && location <= contentLength)
|
|
p = [content characterAtIndex:location - 1];
|
|
|
|
for (const unichar *cs = kMPMatchingCharactersMap[0]; *cs != 0; cs += 2)
|
|
{
|
|
// Ignore IM input of ASCII charaters.
|
|
if (hasMarkedText && cs[0] < L'\u0100')
|
|
continue;
|
|
|
|
// First part of matching characters.
|
|
if ([boundaryCharacters characterIsMember:n] && c == cs[0]
|
|
&& ([boundaryCharacters characterIsMember:p] || cs[0] != cs[1]))
|
|
{
|
|
NSRange range = NSMakeRange(location, 0);
|
|
NSString *completion = [NSString stringWithCharacters:cs length:2];
|
|
// Mimic OS X's quote substitution if it's on.
|
|
if (self.isAutomaticQuoteSubstitutionEnabled)
|
|
{
|
|
unichar c = L'\0';
|
|
switch (cs[0])
|
|
{
|
|
case L'\"':
|
|
c = kMPLeftDoubleQuotation;
|
|
break;
|
|
case L'\'':
|
|
c = kMPLeftSingleQuotation;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
if (c != L'\0')
|
|
completion = [NSString stringWithCharacters:&c length:1];
|
|
}
|
|
[self insertText:completion replacementRange:range];
|
|
|
|
range.location += string.length;
|
|
self.selectedRange = range;
|
|
return YES;
|
|
}
|
|
// Second part of matching characters (shift without really inserting).
|
|
else if (c == cs[1] && n == cs[1])
|
|
{
|
|
NSRange range = NSMakeRange(location + 1, 0);
|
|
self.selectedRange = range;
|
|
return YES;
|
|
}
|
|
}
|
|
return NO;
|
|
}
|
|
|
|
- (void)wrapTextInRange:(NSRange)range withPrefix:(unichar)prefix
|
|
suffix:(unichar)suffix
|
|
{
|
|
NSString *string = [self.string substringWithRange:range];
|
|
NSString *p = [NSString stringWithCharacters:&prefix length:1];
|
|
NSString *s = [NSString stringWithCharacters:&suffix length:1];
|
|
NSString *wrapped = [NSString stringWithFormat:@"%@%@%@", p, string, s];
|
|
[self insertText:wrapped replacementRange:range];
|
|
|
|
range.location += 1;
|
|
self.selectedRange = range;
|
|
}
|
|
|
|
- (BOOL)wrapMatchingCharactersOfCharacter:(unichar)character
|
|
aroundTextInRange:(NSRange)range
|
|
strikethroughEnabled:(BOOL)isStrikethroughEnabled
|
|
{
|
|
for (const unichar *cs = kMPMatchingCharactersMap[0]; *cs != 0; cs += 2)
|
|
{
|
|
if (character == cs[0])
|
|
{
|
|
[self wrapTextInRange:range withPrefix:cs[0] suffix:cs[1]];
|
|
return YES;
|
|
}
|
|
}
|
|
for (size_t i = 0; kMPMarkupCharacters[i] != 0; i++)
|
|
{
|
|
if (character == kMPMarkupCharacters[i])
|
|
{
|
|
[self wrapTextInRange:range withPrefix:character suffix:character];
|
|
return YES;
|
|
}
|
|
}
|
|
if (isStrikethroughEnabled && character == kMPStrikethroughCharacter)
|
|
{
|
|
[self wrapTextInRange:range withPrefix:character suffix:character];
|
|
return YES;
|
|
}
|
|
return NO;
|
|
}
|
|
|
|
- (BOOL)deleteMatchingCharactersAround:(NSUInteger)location
|
|
{
|
|
NSString *string = self.string;
|
|
if (location == 0 || location >= string.length)
|
|
return NO;
|
|
|
|
unichar f = [string characterAtIndex:location - 1];
|
|
unichar b = [string characterAtIndex:location];
|
|
|
|
for (const unichar *cs = kMPMatchingCharactersMap[0]; *cs != 0; cs += 2)
|
|
{
|
|
if (f == cs[0] && b == cs[1])
|
|
{
|
|
NSRange range = NSMakeRange(location - 1, 2);
|
|
[self shouldChangeTextInRange:range replacementString:@""];
|
|
[self replaceCharactersInRange:range withString:@""];
|
|
return YES;
|
|
}
|
|
}
|
|
return NO;
|
|
}
|
|
|
|
- (BOOL)unindentForSpacesBefore:(NSUInteger)location
|
|
{
|
|
NSString *string = self.string;
|
|
|
|
NSUInteger whitespaceCount = 0;
|
|
while (location - whitespaceCount > 0
|
|
&& [string characterAtIndex:location - whitespaceCount - 1] == L' ')
|
|
{
|
|
whitespaceCount++;
|
|
if (whitespaceCount >= 4)
|
|
break;
|
|
}
|
|
if (whitespaceCount < 2)
|
|
return NO;
|
|
|
|
NSUInteger lineStart = [string locationOfFirstNewlineBefore:location] + 1;
|
|
if (location <= lineStart)
|
|
return NO;
|
|
|
|
NSUInteger offset = (location - lineStart) % 4;
|
|
if (offset == 0)
|
|
offset = 4;
|
|
if (whitespaceCount < offset)
|
|
offset = whitespaceCount;
|
|
|
|
NSRange range = NSMakeRange(location - offset, offset);
|
|
[self shouldChangeTextInRange:range replacementString:@""];
|
|
[self replaceCharactersInRange:range withString:@""];
|
|
return YES;
|
|
}
|
|
|
|
- (BOOL)toggleForMarkupPrefix:(NSString *)prefix suffix:(NSString *)suffix
|
|
{
|
|
NSRange range = self.selectedRange;
|
|
NSString *selection = [self.string substringWithRange:range];
|
|
BOOL isOn = NO;
|
|
|
|
// Selection is already marked-up. Clear markup and maintain selection.
|
|
NSUInteger poff = prefix.length;
|
|
if ([self substringInRange:range isSurroundedByPrefix:prefix
|
|
suffix:suffix])
|
|
{
|
|
NSRange sub = NSMakeRange(range.location - poff,
|
|
selection.length + poff + suffix.length);
|
|
[self insertText:selection replacementRange:sub];
|
|
range.location = sub.location;
|
|
isOn = NO;
|
|
}
|
|
// Selection is normal. Mark it up and maintain selection.
|
|
else
|
|
{
|
|
NSString *text = [NSString stringWithFormat:@"%@%@%@",
|
|
prefix, selection, suffix];
|
|
[self insertText:text replacementRange:range];
|
|
range.location += poff;
|
|
isOn = YES;
|
|
}
|
|
self.selectedRange = range;
|
|
return isOn;
|
|
}
|
|
|
|
- (void)toggleBlockWithPattern:(NSString *)pattern prefix:(NSString *)prefix
|
|
{
|
|
NSRegularExpression *regex =
|
|
[[NSRegularExpression alloc] initWithPattern:pattern options:0
|
|
error:NULL];
|
|
NSString *content = self.string;
|
|
NSRange selectedRange = self.selectedRange;
|
|
NSRange lineRange = [content lineRangeForRange:selectedRange];
|
|
|
|
NSString *toProcess = [content substringWithRange:lineRange];
|
|
BOOL hasTrailingNewline = NO;
|
|
if ([toProcess hasSuffix:@"\n"])
|
|
{
|
|
toProcess = [toProcess substringToIndex:(toProcess.length - 1)];
|
|
hasTrailingNewline = YES;
|
|
}
|
|
|
|
NSArray *lines = [toProcess componentsSeparatedByString:@"\n"];
|
|
|
|
BOOL isMarked = YES;
|
|
for (NSString *line in lines)
|
|
{
|
|
NSRange matchRange =
|
|
[regex rangeOfFirstMatchInString:line options:0
|
|
range:NSMakeRange(0, line.length)];
|
|
if (matchRange.location == NSNotFound)
|
|
{
|
|
isMarked = NO;
|
|
break;
|
|
}
|
|
}
|
|
|
|
NSUInteger prefixLength = prefix.length;
|
|
NSMutableArray *modLines = [NSMutableArray arrayWithCapacity:lines.count];
|
|
|
|
__block NSUInteger totalShift = 0;
|
|
[lines enumerateObjectsUsingBlock:^(id obj, NSUInteger index, BOOL *stop) {
|
|
NSString *line = obj;
|
|
if (line.length)
|
|
totalShift += prefixLength;
|
|
if (!isMarked)
|
|
line = [prefix stringByAppendingString:line];
|
|
else
|
|
line = [line substringFromIndex:prefixLength];
|
|
[modLines addObject:line];
|
|
}];
|
|
|
|
NSString *processed = [modLines componentsJoinedByString:@"\n"];
|
|
if (hasTrailingNewline)
|
|
processed = [NSString stringWithFormat:@"%@\n", processed];
|
|
[self insertText:processed replacementRange:lineRange];
|
|
|
|
if (!isMarked)
|
|
{
|
|
selectedRange.location += prefixLength;
|
|
if (selectedRange.length + totalShift >= prefixLength)
|
|
selectedRange.length += totalShift - prefixLength;
|
|
else // Underflow.
|
|
selectedRange.length = 0;
|
|
}
|
|
else
|
|
{
|
|
if (prefixLength <= selectedRange.location)
|
|
selectedRange.location -= prefixLength;
|
|
else // Underflow.
|
|
selectedRange.location = 0;
|
|
if (totalShift - prefixLength <= selectedRange.length)
|
|
selectedRange.length -= totalShift - prefixLength;
|
|
else // Underflow.
|
|
selectedRange.length = 0;
|
|
|
|
if (selectedRange.location < lineRange.location)
|
|
{
|
|
selectedRange.length -= lineRange.location - selectedRange.location;
|
|
selectedRange.location = lineRange.location;
|
|
}
|
|
}
|
|
self.selectedRange = selectedRange;
|
|
}
|
|
|
|
- (void)indentSelectedLinesWithPadding:(NSString *)padding
|
|
{
|
|
NSString *content = self.string;
|
|
NSRange selectedRange = self.selectedRange;
|
|
NSRange lineRange = [content lineRangeForRange:selectedRange];
|
|
|
|
NSString *toProcess = [content substringWithRange:lineRange];
|
|
NSArray *lines = [toProcess componentsSeparatedByString:@"\n"];
|
|
NSMutableArray *modLines = [NSMutableArray arrayWithCapacity:lines.count];
|
|
NSUInteger paddingLength = padding.length;
|
|
|
|
__block NSUInteger totalShift = 0;
|
|
[lines enumerateObjectsUsingBlock:^(id obj, NSUInteger index, BOOL *stop) {
|
|
NSString *line = obj;
|
|
if (line.length)
|
|
totalShift += paddingLength;
|
|
[modLines addObject:[padding stringByAppendingString:line]];
|
|
}];
|
|
if ([modLines.lastObject isEqualToString:padding])
|
|
{
|
|
[modLines removeLastObject];
|
|
[modLines addObject:@""];
|
|
}
|
|
NSString *processed = [modLines componentsJoinedByString:@"\n"];
|
|
[self insertText:processed replacementRange:lineRange];
|
|
|
|
selectedRange.location += paddingLength;
|
|
selectedRange.length +=
|
|
(totalShift > paddingLength) ? totalShift - paddingLength : 0;
|
|
self.selectedRange = selectedRange;
|
|
}
|
|
|
|
- (void)unindentSelectedLines
|
|
{
|
|
NSString *content = self.string;
|
|
NSRange selectedRange = self.selectedRange;
|
|
NSRange lineRange = [content lineRangeForRange:selectedRange];
|
|
|
|
// Get the lines to unindent.
|
|
NSString *toProcess = [content substringWithRange:lineRange];
|
|
NSArray *lines = [toProcess componentsSeparatedByString:@"\n"];
|
|
|
|
// This will hold the modified lines.
|
|
NSMutableArray *modLines = [NSMutableArray arrayWithCapacity:lines.count];
|
|
|
|
// Unindent the lines one by one, and put them in the new array.
|
|
__block NSUInteger firstShift = 0; // Indentation of the first line.
|
|
__block NSUInteger totalShift = 0; // Indents removed in total.
|
|
[lines enumerateObjectsUsingBlock:^(id obj, NSUInteger index, BOOL *stop) {
|
|
NSString *line = obj;
|
|
NSUInteger lineLength = line.length;
|
|
NSUInteger shift = 0;
|
|
|
|
for (shift = 0; shift < 4; shift++)
|
|
{
|
|
if (shift >= lineLength)
|
|
break;
|
|
unichar c = [line characterAtIndex:shift];
|
|
if (c == '\t')
|
|
shift++;
|
|
if (c != ' ')
|
|
break;
|
|
}
|
|
if (index == 0)
|
|
firstShift += shift;
|
|
totalShift += shift;
|
|
if (shift && shift < lineLength)
|
|
line = [line substringFromIndex:shift];
|
|
[modLines addObject:line];
|
|
}];
|
|
|
|
// Join the processed lines, and replace the original with them.
|
|
NSString *processed = [modLines componentsJoinedByString:@"\n"];
|
|
[self insertText:processed replacementRange:lineRange];
|
|
|
|
// Modify the selection range so that the same text (minus removed spaces)
|
|
// are selected.
|
|
selectedRange.location -= firstShift;
|
|
selectedRange.length -= totalShift - firstShift;
|
|
self.selectedRange = selectedRange;
|
|
}
|
|
|
|
- (BOOL)insertMappedContent
|
|
{
|
|
NSString *content = self.string;
|
|
NSUInteger contentLength = content.length;
|
|
if (contentLength > 20)
|
|
return NO;
|
|
|
|
static NSDictionary *map = nil;
|
|
static dispatch_once_t onceToken;
|
|
dispatch_once(&onceToken, ^{
|
|
map = MPGetDataMap(@"data");
|
|
});
|
|
NSData *mapped = map[content];
|
|
if (!mapped)
|
|
return NO;
|
|
NSArray *components = @[NSTemporaryDirectory(),
|
|
[NSString stringWithFormat:@"%lu", content.hash]];
|
|
NSString *path = [NSString pathWithComponents:components];
|
|
[mapped writeToFile:path atomically:NO];
|
|
NSString *text = [NSString stringWithFormat:@"", content, path];
|
|
[self insertText:text replacementRange:NSMakeRange(0, contentLength)];
|
|
self.selectedRange = NSMakeRange(2, contentLength);
|
|
return YES;
|
|
}
|
|
|
|
- (BOOL)completeNextListItem:(BOOL)autoIncrement
|
|
{
|
|
NSRange selectedRange = self.selectedRange;
|
|
NSUInteger location = selectedRange.location;
|
|
NSString *content = self.string;
|
|
if (selectedRange.length || !content.length)
|
|
return NO;
|
|
|
|
NSInteger start = [content locationOfFirstNewlineBefore:location] + 1;
|
|
NSUInteger end = location;
|
|
NSUInteger nonwhitespace =
|
|
[content locationOfFirstNonWhitespaceCharacterInLineBefore:location];
|
|
|
|
// No non-whitespace character at this line.
|
|
if (nonwhitespace == location)
|
|
return NO;
|
|
|
|
NSRange range = NSMakeRange(start, end - start);
|
|
NSString *line = [self.string substringWithRange:range];
|
|
|
|
NSRegularExpressionOptions options = NSRegularExpressionAnchorsMatchLines;
|
|
NSRegularExpression *regex =
|
|
[[NSRegularExpression alloc] initWithPattern:kMPListLineHeadPattern
|
|
options:options
|
|
error:NULL];
|
|
NSTextCheckingResult *result =
|
|
[regex firstMatchInString:line options:0
|
|
range:NSMakeRange(0, line.length)];
|
|
if (!result || result.range.location == NSNotFound)
|
|
return NO;
|
|
|
|
NSString *t = nil;
|
|
BOOL isUl = ([result rangeAtIndex:2].length != 0);
|
|
BOOL isOl = ([result rangeAtIndex:3].length != 0);
|
|
BOOL previousLineEmpty = ([result rangeAtIndex:4].length == 0);
|
|
if (previousLineEmpty)
|
|
{
|
|
NSRange replaceRange = NSMakeRange(NSNotFound, 0);
|
|
if (isUl)
|
|
replaceRange = [result rangeAtIndex:2];
|
|
else if (isOl)
|
|
replaceRange = [result rangeAtIndex:3];
|
|
if (replaceRange.length)
|
|
{
|
|
replaceRange.location += start;
|
|
[self shouldChangeTextInRange:range replacementString:@""];
|
|
[self replaceCharactersInRange:range withString:@""];
|
|
}
|
|
t = @"";
|
|
}
|
|
else if (isUl)
|
|
{
|
|
NSRange range = [result rangeAtIndex:2];
|
|
range.length -= 1; // Exclude trailing whitespace.
|
|
t = [line substringWithRange:range];
|
|
}
|
|
else if (isOl)
|
|
{
|
|
NSRange range = [result rangeAtIndex:3];
|
|
range.length -= 1; // Exclude trailing space.
|
|
NSString *captured = [line substringWithRange:range];
|
|
NSInteger i = captured.integerValue;
|
|
if (autoIncrement)
|
|
i += 1;
|
|
t = [NSString stringWithFormat:@"%ld.", i];
|
|
}
|
|
if (!t)
|
|
return NO;
|
|
|
|
[self insertNewline:self];
|
|
location += 1; // Shift for inserted newline.
|
|
|
|
NSString *indent = [line substringWithRange:[result rangeAtIndex:1]];
|
|
NSUInteger contentLength = content.length;
|
|
|
|
// Has matching list item. Only insert indent.
|
|
NSRange r = NSMakeRange(location, t.length);
|
|
if (contentLength > location + t.length
|
|
&& [[content substringWithRange:r] isEqualToString:t])
|
|
{
|
|
[self insertText:indent];
|
|
return YES;
|
|
}
|
|
|
|
NSString *it = [NSString stringWithFormat:@"%@%@", indent, t];
|
|
|
|
// Has indent and matching list item. Accept it.
|
|
r = NSMakeRange(location, it.length);
|
|
if (contentLength > location + it.length
|
|
&& [[content substringWithRange:r] isEqualToString:it])
|
|
return YES;
|
|
|
|
// Insert completion for normal cases.
|
|
if (t.length)
|
|
it = [NSString stringWithFormat:@"%@ ", it];
|
|
[self insertText:it];
|
|
return YES;
|
|
}
|
|
|
|
- (BOOL)completeNextBlockquoteLine
|
|
{
|
|
NSRange selectedRange = self.selectedRange;
|
|
NSString *content = self.string;
|
|
NSUInteger contentLength = content.length;
|
|
if (selectedRange.length || !contentLength)
|
|
return NO;
|
|
|
|
NSRange lineRange = [content lineRangeForRange:selectedRange];
|
|
NSString *line = [content substringWithRange:lineRange];
|
|
|
|
NSRegularExpressionOptions options = NSRegularExpressionAnchorsMatchLines;
|
|
NSRegularExpression *regex =
|
|
[[NSRegularExpression alloc] initWithPattern:kMPBlockquoteLinePattern
|
|
options:options error:NULL];
|
|
NSTextCheckingResult *result =
|
|
[regex firstMatchInString:line options:0
|
|
range:NSMakeRange(0, lineRange.length)];
|
|
if (!result || result.range.location == NSNotFound)
|
|
return NO;
|
|
|
|
[self insertNewline:self];
|
|
|
|
NSRange markersRange = [result rangeAtIndex:1];
|
|
NSString *markers = [line substringWithRange:markersRange];
|
|
NSUInteger nextLineStart = selectedRange.location + 1;
|
|
|
|
// Has identical markers. Accept this.
|
|
NSRange nextMarkersRange = NSMakeRange(nextLineStart, markersRange.length);
|
|
if (contentLength > nextLineStart + markersRange.length)
|
|
{
|
|
NSString *nextMarkers = [content substringWithRange:nextMarkersRange];
|
|
if ([nextMarkers isEqualToString:markers])
|
|
return YES;
|
|
}
|
|
|
|
// Insert completion.
|
|
[self insertText:markers];
|
|
return YES;
|
|
}
|
|
|
|
- (BOOL)completeNextIndentedLine
|
|
{
|
|
NSRange selectedRange = self.selectedRange;
|
|
if (selectedRange.length)
|
|
return NO;
|
|
|
|
NSString *content = self.string;
|
|
NSUInteger start = [content lineRangeForRange:selectedRange].location;
|
|
NSUInteger end = [content locationOfFirstNonWhitespaceCharacterInLineBefore:
|
|
selectedRange.location];
|
|
if (end <= start)
|
|
return NO;
|
|
|
|
[self insertNewline:self];
|
|
NSRange indentRange = NSMakeRange(start, end - start);
|
|
[self insertText:[content substringWithRange:indentRange]];
|
|
return YES;
|
|
}
|
|
|
|
- (void)makeHeaderForSelectedLinesWithLevel:(NSUInteger)level
|
|
{
|
|
NSAssert(level <= 6, @"Should be 1-6, or 0 (convert to paragraph).");
|
|
NSString *content = self.string;
|
|
NSRange selectedRange = self.selectedRange;
|
|
NSRange lineRange = [content lineRangeForRange:selectedRange];
|
|
|
|
NSString *header = [@"###### " substringFromIndex:6 - level];
|
|
if (level == 0)
|
|
header = [header substringToIndex:header.length - 1];
|
|
NSUInteger headerLength = header.length;
|
|
NSUInteger options = NSRegularExpressionDotMatchesLineSeparators;
|
|
NSRegularExpression *regex =
|
|
[[NSRegularExpression alloc] initWithPattern:@"^(#+ )*.*?$"
|
|
options:options error:NULL];
|
|
|
|
NSMutableArray *processedLines = [NSMutableArray array];
|
|
NSString *toProcess = [content substringWithRange:lineRange];
|
|
NSArray *lines = [toProcess componentsSeparatedByString:@"\n"];
|
|
NSUInteger lineCount = lines.count;
|
|
__block NSInteger firstShift = 0;
|
|
__block NSInteger totalShift = 0;
|
|
[lines enumerateObjectsUsingBlock:^(id obj, NSUInteger index, BOOL *stop) {
|
|
NSString *line = obj;
|
|
NSTextCheckingResult *result =
|
|
[regex firstMatchInString:line options:0
|
|
range:NSMakeRange(0, line.length)];
|
|
NSUInteger rangeCount = result.numberOfRanges;
|
|
|
|
// Don't process empty/whitespace-only lines unless it's the only line.
|
|
if (lineCount > 1)
|
|
{
|
|
NSUInteger sentinel = [line
|
|
locationOfFirstNonWhitespaceCharacterInLineBefore:line.length];
|
|
if (sentinel == line.length)
|
|
{
|
|
[processedLines addObject:line];
|
|
return;
|
|
}
|
|
}
|
|
|
|
NSString *lineContent = line;
|
|
NSRange headerRange = NSMakeRange(0, 0);
|
|
if (rangeCount > 1)
|
|
headerRange = [result rangeAtIndex:1];
|
|
if (headerRange.location != NSNotFound)
|
|
{
|
|
NSUInteger start = headerRange.location + headerRange.length;
|
|
lineContent = [line substringFromIndex:start];
|
|
}
|
|
[processedLines addObject:[header stringByAppendingString:lineContent]];
|
|
|
|
NSInteger shift = headerLength - headerRange.length;
|
|
if (index == 0)
|
|
firstShift += shift;
|
|
totalShift += shift;
|
|
}];
|
|
NSString *processed = [processedLines componentsJoinedByString:@"\n"];
|
|
[self insertText:processed replacementRange:lineRange];
|
|
|
|
selectedRange.location += firstShift;
|
|
selectedRange.length += totalShift - firstShift;
|
|
self.selectedRange = selectedRange;
|
|
}
|
|
|
|
@end
|