aboutsummaryrefslogtreecommitdiffstats
path: root/Source
diff options
context:
space:
mode:
authorrowanbeentje <rowan@beent.je>2009-04-01 22:59:05 +0000
committerrowanbeentje <rowan@beent.je>2009-04-01 22:59:05 +0000
commit8fd7a25952b3d5317a22e14c83af06b56c32710a (patch)
tree215711b1c3be6b90011ff38c7fa658f7c2e3ae3c /Source
parentac70439744db350206de86e14209fd84e750bb64 (diff)
downloadsequelpro-8fd7a25952b3d5317a22e14c83af06b56c32710a.tar.gz
sequelpro-8fd7a25952b3d5317a22e14c83af06b56c32710a.tar.bz2
sequelpro-8fd7a25952b3d5317a22e14c83af06b56c32710a.zip
- Add autopairing support to CMTextView - many thanks to Hans-Jörg Bibiko for the original patch (see http://code.google.com/p/sequel-pro/issues/detail?id=208 for full details). Applied with slight amendments.
- Further changes to make CMTextView more standalone and reusable - autopairing and autoindenting can now be enabled/disabled and checked. - Autopairing and autoindenting moved to app preferences.
Diffstat (limited to 'Source')
-rw-r--r--Source/CMTextView.h18
-rw-r--r--Source/CMTextView.m281
-rw-r--r--Source/CustomQuery.h3
-rw-r--r--Source/CustomQuery.m5
-rw-r--r--Source/MainController.m2
5 files changed, 299 insertions, 10 deletions
diff --git a/Source/CMTextView.h b/Source/CMTextView.h
index e70c6ea4..761e1189 100644
--- a/Source/CMTextView.h
+++ b/Source/CMTextView.h
@@ -2,7 +2,7 @@
// CMTextView.h
// sequel-pro
//
-// Created by Carsten BlŸm.
+// Created by Carsten Blüm.
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
@@ -24,9 +24,21 @@
#import <Cocoa/Cocoa.h>
@interface CMTextView : NSTextView {
+ BOOL autoindentEnabled;
+ BOOL autopairEnabled;
+ BOOL autoindentIgnoresEnter;
}
--(NSArray *)completionsForPartialWordRange:(NSRange)charRange indexOfSelectedItem:(int *)index;
--(NSArray *)keywords;
+- (BOOL) isNextCharMarkedBy:(id)attribute;
+- (BOOL) areAdjacentCharsLinked;
+- (BOOL) wrapSelectionWithPrefix:(NSString *)prefix suffix:(NSString *)suffix;
+- (NSArray *)completionsForPartialWordRange:(NSRange)charRange indexOfSelectedItem:(int *)index;
+- (NSArray *)keywords;
+- (void)setAutoindent:(BOOL)enableAutoindent;
+- (BOOL)autoindent;
+- (void)setAutoindentIgnoresEnter:(BOOL)enableAutoindentIgnoresEnter;
+- (BOOL)autoindentIgnoresEnter;
+- (void)setAutopair:(BOOL)enableAutopair;
+- (BOOL)autopair;
@end
diff --git a/Source/CMTextView.m b/Source/CMTextView.m
index 7779ba43..2532cf1e 100644
--- a/Source/CMTextView.m
+++ b/Source/CMTextView.m
@@ -25,8 +25,8 @@
#import "SPStringAdditions.h"
/*
-all the extern variables and prototypes required for flex (used for syntax highlighting)
-*/
+ * Include all the extern variables and prototypes required for flex (used for syntax highlighting)
+ */
#import "SPEditorTokens.h"
extern int yylex();
extern int yyuoffset, yyuleng;
@@ -34,9 +34,214 @@ typedef struct yy_buffer_state *YY_BUFFER_STATE;
void yy_switch_to_buffer(YY_BUFFER_STATE);
YY_BUFFER_STATE yy_scan_string (const char *);
+#define kAPlinked @"Linked" // attribute for a via auto-pair inserted char
+#define kAPval @"linked"
+#define kWQquoted @"Quoted" // set via lex to indicate a quoted string
+#define kWQval @"quoted"
+
@implementation CMTextView
+/*
+ * Checks if the char after the current caret position/selection matches a supplied attribute
+ */
+- (BOOL) isNextCharMarkedBy:(id)attribute
+{
+ unsigned int caretPosition = [self selectedRange].location;
+
+ // Perform bounds checking
+ if (caretPosition >= [[self string] length]) return NO;
+
+ // Perform the check
+ if ([[self textStorage] attribute:attribute atIndex:caretPosition effectiveRange:nil])
+ return YES;
+
+ return NO;
+}
+
+
+/*
+ * Checks if the caret is wrapped by auto-paired characters.
+ * e.g. [| := caret]: "|"
+ */
+- (BOOL) areAdjacentCharsLinked
+{
+ unsigned int caretPosition = [self selectedRange].location;
+ unichar leftChar, matchingChar;
+
+ // Perform bounds checking
+ if ([self selectedRange].length) return NO;
+ if (caretPosition < 1) return NO;
+ if (caretPosition >= [[self string] length]) return NO;
+
+ // Check the character to the left of the cursor and set the pairing character if appropriate
+ leftChar = [[self string] characterAtIndex:caretPosition - 1];
+ if (leftChar == '(')
+ matchingChar = ')';
+ else if (leftChar == '"' || leftChar == '`' || leftChar == '\'')
+ matchingChar = leftChar;
+ else
+ return NO;
+
+ // Check that the pairing character exists after the caret, and is tagged with the link attribute
+ if (matchingChar == [[self string] characterAtIndex:caretPosition]
+ && [[self textStorage] attribute:kAPlinked atIndex:caretPosition effectiveRange:nil]) {
+ return YES;
+ }
+
+ return NO;
+}
+
+
+/*
+ * If the textview has a selection, wrap it with the supplied prefix and suffix strings;
+ * return whether or not any wrap was performed.
+ */
+- (BOOL) wrapSelectionWithPrefix:(NSString *)prefix suffix:(NSString *)suffix
+{
+
+ // Only proceed if a selection is active
+ if ([self selectedRange].length == 0)
+ return NO;
+
+ // Replace the current selection with the selected string wrapped in prefix and suffix
+ [self insertText:
+ [NSString stringWithFormat:@"%@%@%@",
+ prefix,
+ [[self string] substringWithRange:[self selectedRange]],
+ suffix
+ ]
+ ];
+ return YES;
+}
+
+
+
+/*
+ * Handle some keyDown events in order to provide autopairing functionality (if enabled).
+ */
+- (void) keyDown:(NSEvent *)theEvent
+{
+ NSString *characters = [theEvent characters];
+
+ // Only process for character autopairing if autopairing is enabled and a single character is being added.
+ if (autopairEnabled && characters && [characters length] == 1) {
+ unichar insertedCharacter = [characters characterAtIndex:0];
+ NSString *matchingCharacter = nil;
+ BOOL processAutopair = NO, skipTypedLinkedCharacter = NO;
+ NSRange currentRange;
+
+ // Check if user pressed ⌥ to allow composing of accented characters.
+ // e.g. for US keyboard "⌥u a" to insert ä
+ if ([theEvent modifierFlags] & NSAlternateKeyMask) {
+ [super keyDown: theEvent];
+ return;
+ }
+
+ // If the caret is inside a text string, without any selection, skip autopairing.
+ // There is one exception to this - if the caret is before a linked pair character,
+ // processing continues in order to check whether the next character should be jumped
+ // over; e.g. [| := caret]: "foo|" and press " => only caret will be moved "foo"|
+ if(![self isNextCharMarkedBy:kAPlinked] && [self isNextCharMarkedBy:kWQquoted] && ![self selectedRange].length) {
+ [super keyDown:theEvent];
+ return;
+ }
+
+ // Check whether the submitted character should trigger autopair processing.
+ switch (insertedCharacter)
+ {
+ case '(':
+ matchingCharacter = @")";
+ processAutopair = YES;
+ break;
+ case '"':
+ matchingCharacter = @"\"";
+ processAutopair = YES;
+ skipTypedLinkedCharacter = YES;
+ break;
+ case '`':
+ matchingCharacter = @"`";
+ processAutopair = YES;
+ skipTypedLinkedCharacter = YES;
+ break;
+ case '\'':
+ matchingCharacter = @"'";
+ processAutopair = YES;
+ skipTypedLinkedCharacter = YES;
+ break;
+ case ')':
+ skipTypedLinkedCharacter = YES;
+ break;
+ }
+
+ // Check to see whether the next character should be compared to the typed character;
+ // if it matches the typed character, and is marked with the is-linked-pair attribute,
+ // select the next character and replace it with the typed character. This allows
+ // a normally quoted string to be typed in full, with the autopair appearing as a hint and
+ // then being automatically replaced when the user types it.
+ if (skipTypedLinkedCharacter) {
+ currentRange = [self selectedRange];
+ if (currentRange.location != NSNotFound && currentRange.length == 0) {
+ if ([self isNextCharMarkedBy:kAPlinked]) {
+ if ([[[self textStorage] string] characterAtIndex:currentRange.location] == insertedCharacter) {
+ currentRange.length = 1;
+ [self setSelectedRange:currentRange];
+ processAutopair = NO;
+ }
+ }
+ }
+ }
+
+ // If an appropriate character has been typed, and a matching character has been set,
+ // some form of autopairing is required.
+ if (processAutopair && matchingCharacter) {
+
+ // Check to see whether several characters are selected, and if so, wrap them with
+ // the auto-paired characters. This returns false if the selection has zero length.
+ if ([self wrapSelectionWithPrefix:characters suffix:matchingCharacter])
+ return;
+
+ // Otherwise, start by inserting the original character - the first half of the autopair.
+ [super keyDown:theEvent];
+
+ // Then process the second half of the autopair - the matching character.
+ currentRange = [self selectedRange];
+ if (currentRange.location != NSNotFound) {
+ NSTextStorage *textStorage = [self textStorage];
+
+ // Register the auto-pairing for undo
+ [self shouldChangeTextInRange:currentRange replacementString:matchingCharacter];
+
+ // Insert the matching character and give it the is-linked-pair-character attribute
+ [self replaceCharactersInRange:currentRange withString:matchingCharacter];
+ currentRange.length = 1;
+ [textStorage addAttribute:kAPlinked value:kAPval range:currentRange];
+
+ // Restore the original selection.
+ currentRange.length=0;
+ [self setSelectedRange:currentRange];
+ }
+ return;
+ }
+ }
+
+ // The default action is to perform the normal key-down action.
+ [super keyDown:theEvent];
+}
+
+
+- (void) deleteBackward:(id)sender
+{
+
+ // If the caret is currently inside a marked auto-pair, delete the characters on both sides
+ // of the caret.
+ NSRange currentRange = [self selectedRange];
+ if (currentRange.length == 0 && currentRange.location > 0 && [self areAdjacentCharsLinked])
+ [self setSelectedRange:NSMakeRange(currentRange.location - 1,2)];
+
+ [super deleteBackward:sender];
+}
+
/*
* Handle special commands - see NSResponder.h for a sample list.
@@ -46,8 +251,8 @@ YY_BUFFER_STATE yy_scan_string (const char *);
- (void) doCommandBySelector:(SEL)aSelector
{
- // Handle newlines, adding any indentation found on the current line to the new line - ignoring the enter key.
- if (aSelector == @selector(insertNewline:) && [[NSApp currentEvent] keyCode] != 0x4C) {
+ // Handle newlines, adding any indentation found on the current line to the new line - ignoring the enter key if appropriate
+ if (aSelector == @selector(insertNewline:) && (!autoindentIgnoresEnter || [[NSApp currentEvent] keyCode] != 0x4C)) {
NSString *textViewString = [[self textStorage] string];
NSString *currentLine, *indentString = nil;
NSScanner *whitespaceScanner;
@@ -407,6 +612,56 @@ it should also be added to the flex file SPEditorTokens.l
nil];
}
+
+/*
+ * Set whether this text view should apply the indentation on the current line to new lines.
+ */
+- (void)setAutoindent:(BOOL)enableAutoindent
+{
+ autoindentEnabled = enableAutoindent;
+}
+
+/*
+ * Retrieve whether this text view applies indentation on the current line to new lines.
+ */
+- (BOOL)autoindent
+{
+ return autoindentEnabled;
+}
+
+/*
+ * Set whether this text view should not autoindent when the Enter key is used, as opposed
+ * to the return key. Also catches function-return.
+ */
+- (void)setAutoindentIgnoresEnter:(BOOL)enableAutoindentIgnoresEnter
+{
+ autoindentIgnoresEnter = enableAutoindentIgnoresEnter;
+}
+
+/*
+ * Retrieve whether this text view should not autoindent when the Enter key is used.
+ */
+- (BOOL)autoindentIgnoresEnter
+{
+ return autoindentIgnoresEnter;
+}
+
+/*
+ * Set whether this text view should automatically create the matching closing char for ", ', ` and ( chars.
+ */
+- (void)setAutopair:(BOOL)enableAutopair
+{
+ autopairEnabled = enableAutopair;
+}
+
+/*
+ * Retrieve whether this text view automatically creates the matching closing char for ", ', ` and ( chars.
+ */
+- (BOOL)autopair
+{
+ return autopairEnabled;
+}
+
/*******************
SYNTAX HIGHLIGHTING!
*******************/
@@ -486,11 +741,27 @@ sets self as delegate for the textView's textStorage to enable syntax highlighti
[textStore addAttribute: NSForegroundColorAttributeName
value: tokenColor
range: tokenRange ];
-
+ // this attr is used in the auto-pairing (keyDown:)
+ // to disable auto-pairing if caret is inside of any token found by lex
+ // maybe change it later (only for quotes) => discussion
+ [textStore addAttribute: kWQquoted
+ value: kWQval
+ range: tokenRange ];
+
}
}
+- (id) init
+{
+ if (self = [super init]) {
+ autoindentEnabled = YES;
+ autopairEnabled = YES;
+ autoindentIgnoresEnter = NO;
+ }
+
+ return self;
+}
@end
diff --git a/Source/CustomQuery.h b/Source/CustomQuery.h
index c3a61b56..1abc5e57 100644
--- a/Source/CustomQuery.h
+++ b/Source/CustomQuery.h
@@ -25,6 +25,7 @@
#import <Cocoa/Cocoa.h>
#import <MCPKit_bundled/MCPKit_bundled.h>
#import "CMCopyTable.h"
+#import "CMTextView.h"
#import "CMMCPConnection.h"
#import "CMMCPResult.h"
@@ -33,7 +34,7 @@
IBOutlet id tableWindow;
IBOutlet id queryFavoritesButton;
IBOutlet id queryHistoryButton;
- IBOutlet id textView;
+ IBOutlet CMTextView *textView;
IBOutlet CMCopyTable *customQueryView;
IBOutlet id errorText;
IBOutlet id affectedRowsText;
diff --git a/Source/CustomQuery.m b/Source/CustomQuery.m
index e8c7af53..115c4d36 100644
--- a/Source/CustomQuery.m
+++ b/Source/CustomQuery.m
@@ -541,7 +541,7 @@ sets the connection (received from TableDocument) and makes things that have to
queryFavorites = [[NSMutableArray array] retain];
}
-//set up interface
+ // Set up the interface
[customQueryView setVerticalMotionCanBeginDrag:NO];
if ( [prefs boolForKey:@"useMonospacedFonts"] ) {
[textView setFont:[NSFont fontWithName:@"Monaco" size:[NSFont smallSystemFontSize]]];
@@ -549,6 +549,9 @@ sets the connection (received from TableDocument) and makes things that have to
[textView setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]];
}
[textView setContinuousSpellCheckingEnabled:NO];
+ [textView setAutoindent:[prefs boolForKey:@"CustomQueryAutoindent"]];
+ [textView setAutoindentIgnoresEnter:YES];
+ [textView setAutopair:[prefs boolForKey:@"CustomQueryAutopair"]];
[queryFavoritesView registerForDraggedTypes:[NSArray arrayWithObjects:@"SequelProPasteboard", nil]];
while ( (column = [enumerator nextObject]) )
{
diff --git a/Source/MainController.m b/Source/MainController.m
index 257d43c3..4e21c4d5 100644
--- a/Source/MainController.m
+++ b/Source/MainController.m
@@ -671,6 +671,8 @@ checks for updates and opens download page in default browser
[NSNumber numberWithInt:10], @"connectionTimeout",
[NSNumber numberWithInt:60], @"keepAliveInterval",
[NSNumber numberWithInt:0], @"lastUsedVersion",
+ [NSNumber numberWithBool:YES], @"CustomQueryAutopair",
+ [NSNumber numberWithBool:YES], @"CustomQueryAutoindent",
nil]];
// For versions prior to r336, where column widths have been saved, walk through them and remove