Skip to content

Commit

Permalink
#473: Automatically insert a closing quote for string literals in lan…
Browse files Browse the repository at this point in the history
…guages that support it
  • Loading branch information
bobbylight committed Dec 10, 2022
1 parent 875dbab commit 0c12bfd
Show file tree
Hide file tree
Showing 4 changed files with 523 additions and 6 deletions.
Expand Up @@ -124,8 +124,8 @@ public class RSyntaxTextAreaEditorKit extends RTextAreaEditorKit {
new InsertPairedCharacterAction(rstaOpenParenAction, '(', ')'),
new InsertPairedCharacterAction(rstaOpenSquareBracketAction, '[', ']'),
new InsertPairedCharacterAction(rstaOpenCurlyAction, '{', '}'),
new InsertPairedCharacterAction(rstaDoubleQuoteAction, '"', '"'),
new InsertPairedCharacterAction(rstaSingleQuoteAction, '\'', '\''),
new InsertQuoteAction(rstaDoubleQuoteAction, InsertQuoteAction.QuoteType.DOUBLE_QUOTE),
new InsertQuoteAction(rstaSingleQuoteAction, InsertQuoteAction.QuoteType.SINGLE_QUOTE),
new InsertTabAction(),
new NextWordAction(nextWordAction, false),
new NextWordAction(selectionNextWordAction, true),
Expand Down Expand Up @@ -1700,6 +1700,101 @@ private void wrapSelection(RTextArea textArea) {
}


/**
* Inserts a quote character. If the current language supports string literals with this
* quote character, the following additional logic occurs:
* <ul>
* <li>If the caret is not in a string literal or comment, both the opening and closing
* quotes are entered</li>
* <li>If the caret is at the end (the closing quote) of a valid quoted literal, the
* existing closing quote character is overwritten, rather than a new quote
* character being entered</li>
* </ul>
* This feature is meant to simplify the common case of typing single-line strings.
*/
public static class InsertQuoteAction extends InsertPairedCharacterAction {

public enum QuoteType {
DOUBLE_QUOTE('"', TokenTypes.LITERAL_STRING_DOUBLE_QUOTE, TokenTypes.ERROR_STRING_DOUBLE),
SINGLE_QUOTE('\'', TokenTypes.LITERAL_CHAR, TokenTypes.ERROR_CHAR),
BACKTICK('`', TokenTypes.LITERAL_BACKQUOTE, -1);

private final char ch;
private final int validTokenType;
private final int invalidTokenType;

QuoteType(char ch, int validTokenType, int invalidTokenType) {
this.ch = ch;
this.validTokenType = validTokenType;
this.invalidTokenType = invalidTokenType;
}
}

private final QuoteType quoteType;
private final String stringifiedQuoteTypeCh;

public InsertQuoteAction(String actionName, QuoteType quoteType) {
super(actionName, quoteType.ch, quoteType.ch);
this.quoteType = quoteType;
stringifiedQuoteTypeCh = String.valueOf(quoteType.ch);
}

@Override
public void actionPerformedImpl(ActionEvent e, RTextArea textArea) {

if (!textArea.isEditable() || !textArea.isEnabled()) {
UIManager.getLookAndFeel().provideErrorFeedback(textArea);
return;
}

RSyntaxTextArea rsta = (RSyntaxTextArea) textArea;

if (!rsta.getInsertPairedCharacters() ||
textArea.getSelectionStart() != textArea.getSelectionEnd() ||
textArea.getTextMode() == RTextArea.OVERWRITE_MODE) {
super.actionPerformedImpl(e, textArea);
return;
}

int offs = rsta.getCaretPosition();
Token t = RSyntaxUtilities.getTokenAtOffsetOrLastTokenIfEndOfLine(rsta, offs);
int tokenType = t != null ? t.getType() : TokenTypes.NULL;
boolean isComment = t != null && t.isComment();

if (tokenType == quoteType.validTokenType) {
if (offs == t.getEndOffset() - 1) {
textArea.moveCaretPosition(offs + 1); // Force a replacement to ensure undo is contiguous
textArea.replaceSelection(stringifiedQuoteTypeCh);
textArea.setCaretPosition(offs + 1);
}
else {
super.actionPerformedImpl(e, textArea);
}
}
else if (isComment || tokenType == quoteType.invalidTokenType) {
// We could be smarter here for invalid quoted literals - if we knew whether the language
// used '\' as an escape character, and the caret is NOT between a '\' and the closing
// quote, we could then assume it's an invalid string due to e.g. a bad escape char, and
// overwrite the closing quote. But for now we're just doing nothing in this case
super.actionPerformedImpl(e, textArea); // Just insert the character
}
else {
insertEmptyQuoteLiteral(rsta);
}
}

private void insertEmptyQuoteLiteral(RSyntaxTextArea textArea) {
textArea.beginAtomicEdit();
try {
textArea.replaceSelection(stringifiedQuoteTypeCh + quoteType.ch);
textArea.setCaretPosition(textArea.getCaretPosition() - 1);
} finally {
textArea.endAtomicEdit();
}
}
}


/**
* Action for inserting tabs. This is extended to "block indent" a
* group of contiguous lines if they are selected.
Expand Down
Expand Up @@ -964,14 +964,33 @@ public static Token getPreviousImportantTokenFromOffs(
* @return The token, or <code>null</code> if the offset is not valid.
* @see #getTokenAtOffset(RSyntaxDocument, int)
* @see #getTokenAtOffset(Token, int)
* @see #getTokenAtOffsetOrLastTokenIfEndOfLine(RSyntaxTextArea, int)
*/
public static Token getTokenAtOffset(RSyntaxTextArea textArea,
int offset) {
public static Token getTokenAtOffset(RSyntaxTextArea textArea, int offset) {
RSyntaxDocument doc = (RSyntaxDocument)textArea.getDocument();
return RSyntaxUtilities.getTokenAtOffset(doc, offset);
}


/**
* Returns the token at the specified offset. If the offset
* is at the very end of a line, the "last" token in that line is returned
* instead (which may be {@code null} if the line is empty).
*
* @param textArea The text area.
* @param offset The offset at which to get the token.
* @return The token at <code>offset</code>, or <code>null</code> if
* the offset is invalid or there is no token at that offset.
* @see #getTokenAtOffset(RSyntaxTextArea, int)
* @see #getTokenAtOffset(RSyntaxDocument, int)
* @see #getTokenAtOffset(Token, int)
*/
public static Token getTokenAtOffsetOrLastTokenIfEndOfLine(RSyntaxTextArea textArea, int offset) {
RSyntaxDocument doc = (RSyntaxDocument)textArea.getDocument();
return RSyntaxUtilities.getTokenAtOffsetOrLastTokenIfEndOfLine(doc, offset);
}


/**
* Returns the token at the specified offset.
*
Expand All @@ -980,16 +999,35 @@ public static Token getTokenAtOffset(RSyntaxTextArea textArea,
* @return The token, or <code>null</code> if the offset is not valid.
* @see #getTokenAtOffset(RSyntaxTextArea, int)
* @see #getTokenAtOffset(Token, int)
* @see #getTokenAtOffsetOrLastTokenIfEndOfLine(RSyntaxDocument, int)
*/
public static Token getTokenAtOffset(RSyntaxDocument doc,
int offset) {
public static Token getTokenAtOffset(RSyntaxDocument doc, int offset) {
Element root = doc.getDefaultRootElement();
int lineIndex = root.getElementIndex(offset);
Token t = doc.getTokenListForLine(lineIndex);
return RSyntaxUtilities.getTokenAtOffset(t, offset);
}


/**
* Returns the token at the specified offset.
*
* @param doc The document.
* @param offset The offset of the token.
* @return The token, or <code>null</code> if the offset is not valid or
* there is no token at that offset.
* @see #getTokenAtOffset(RSyntaxTextArea, int)
* @see #getTokenAtOffset(RSyntaxDocument, int)
* @see #getTokenAtOffset(Token, int)
*/
public static Token getTokenAtOffsetOrLastTokenIfEndOfLine(RSyntaxDocument doc, int offset) {
Element root = doc.getDefaultRootElement();
int lineIndex = root.getElementIndex(offset);
Token t = doc.getTokenListForLine(lineIndex);
return RSyntaxUtilities.getTokenAtOffsetOrLastTokenIfEndOfLine(t, offset);
}


/**
* Returns the token at the specified index, or <code>null</code> if
* the given offset isn't in this token list's range.<br>
Expand All @@ -1002,6 +1040,7 @@ public static Token getTokenAtOffset(RSyntaxDocument doc,
* none of the tokens are at that offset.
* @see #getTokenAtOffset(RSyntaxTextArea, int)
* @see #getTokenAtOffset(RSyntaxDocument, int)
* @see #getTokenAtOffsetOrLastTokenIfEndOfLine(Token, int)
*/
public static Token getTokenAtOffset(Token tokenList, int offset) {
for (Token t=tokenList; t!=null && t.isPaintable(); t=t.getNextToken()){
Expand All @@ -1013,6 +1052,33 @@ public static Token getTokenAtOffset(Token tokenList, int offset) {
}


/**
* Returns the token at the specified index, or <code>null</code> if
* the given offset isn't in this token list's range. If the offset
* is at the very end of the token list, the "last" token is returned
* (which may be {@code null} if the token list is empty).<br>
* Note that this method does NOT check to see if <code>tokenList</code>
* is null; callers should check for themselves.
*
* @param tokenList The list of tokens in which to search.
* @param offset The offset at which to get the token.
* @return The token at <code>offset</code>, or <code>null</code> if
* none of the tokens are at that offset.
* @see #getTokenAtOffset(RSyntaxTextArea, int)
* @see #getTokenAtOffset(RSyntaxDocument, int)
* @see #getTokenAtOffset(Token, int)
*/
public static Token getTokenAtOffsetOrLastTokenIfEndOfLine(Token tokenList, int offset) {
for (Token t=tokenList; t!=null && t.isPaintable(); t=t.getNextToken()){
if (t.containsPosition(offset) ||
(offset == t.getEndOffset() && (t.getNextToken() == null || !t.getNextToken().isPaintable()))) {
return t;
}
}
return null;
}


/**
* Returns the end of the word at the given offset.
*
Expand Down

0 comments on commit 0c12bfd

Please sign in to comment.