blob: 6cee3acc1e85ce7a163c1976e111d6a4b2a015da [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2008, 2016 xored software, Inc.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* xored software, Inc. - initial API and Implementation
* Alex Panchenko <alex@xored.com>
*******************************************************************************/
package org.eclipse.dltk.ruby.internal.ui.text;
import java.util.Arrays;
import org.eclipse.dltk.core.IScriptProject;
import org.eclipse.dltk.core.PreferencesLookupDelegate;
import org.eclipse.dltk.ruby.internal.ui.RubyUI;
import org.eclipse.dltk.ui.CodeFormatterConstants;
import org.eclipse.dltk.ui.DLTKUIPlugin;
import org.eclipse.dltk.ui.text.util.AutoEditUtils;
import org.eclipse.jface.preference.IPreferenceStore;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.DefaultIndentLineAutoEditStrategy;
import org.eclipse.jface.text.DocumentCommand;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.TextUtilities;
public class RubyAutoEditStrategy extends DefaultIndentLineAutoEditStrategy {
private static final int[] INDENT_TO_BLOCK_TOKENS = {
IRubySymbols.TokenELSE, IRubySymbols.TokenELSIF,
IRubySymbols.TokenEND, IRubySymbols.TokenENSURE,
IRubySymbols.TokenRESCUE, IRubySymbols.TokenWHEN,
IRubySymbols.TokenRBRACE };
private static final int[] CONTINUATION_TOKENS = {
IRubySymbols.TokenBACKSLASH, IRubySymbols.TokenCOMMA,
IRubySymbols.TokenSLASH, IRubySymbols.TokenPLUS,
IRubySymbols.TokenMINUS, IRubySymbols.TokenSTAR };
private static final int[] REMOVE_IDENTATION_TOKENS = {
IRubySymbols.TokenRDOCBEGIN, IRubySymbols.TokenRDOCEND };
static {
Arrays.sort(INDENT_TO_BLOCK_TOKENS);
Arrays.sort(CONTINUATION_TOKENS);
Arrays.sort(REMOVE_IDENTATION_TOKENS);
}
private boolean fIsSmartMode;
private boolean fCloseBlocks = true;
private RubyPreferenceInterpreter fPreferences;
public RubyAutoEditStrategy(String partitioning) {
this(partitioning, RubyUI.getDefault().getPreferenceStore());
}
public RubyAutoEditStrategy(String partitioning, IPreferenceStore store) {
fPreferences = new RubyPreferenceInterpreter(store);
}
private void clearCachedValues() {
fCloseBlocks = fPreferences.closeBlocks();
fIsSmartMode = fPreferences.isSmartMode();
}
private void closeBlock(IDocument d, DocumentCommand c, String indent,
String afterCursor, RubyHeuristicScanner scanner)
throws BadLocationException {
c.caretOffset = c.offset + c.text.length();
c.length = afterCursor.length();
c.shiftsCaret = false;
String delimiter = TextUtilities.getDefaultLineDelimiter(d);
c.text += afterCursor.trim() + delimiter + indent
+ getApropriateBlockEnding(d, scanner, c.offset);
}
private String getApropriateBlockEnding(IDocument d,
RubyHeuristicScanner scanner, int offset)
throws BadLocationException {
int beginning = scanner.findBlockBeginningOffset(offset);
if (beginning == RubyHeuristicScanner.NOT_FOUND)
throw new BadLocationException();
IRegion line = d.getLineInformationOfOffset(beginning);
int ending = Math.min(line.getOffset() + line.getLength(), offset);
int blockOffset = scanner.findBlockBeginningOffset(ending);
int token = scanner.nextToken(blockOffset, ending);
if (token == IRubySymbols.TokenLBRACE) {
return "}"; //$NON-NLS-1$
} else {
return "end"; //$NON-NLS-1$
}
}
private boolean isSmartMode() {
return fIsSmartMode;
}
@Override
public void customizeDocumentCommand(IDocument d, DocumentCommand c) {
if (c.doit == false)
return;
clearCachedValues();
if (!isSmartMode()) {
super.customizeDocumentCommand(d, c);
return;
}
try {
if (c.length == 0 && c.text != null && isLineDelimiter(d, c.text))
smartIndentAfterNewLine(d, c);
else if (c.length == 0 && c.text != null && isSpace(c.text))
smartInsertEndOnSpace(d, c);
else if (isRepresentingTab(c.text))
smartTab(d, c);
else if (c.text.length() == 1)
smartIndentOnKeypress(d, c);
else if (c.text.length() > 1 && fPreferences.isSmartPaste())
smartPaste(d, c); // no smart backspace for paste
else
super.customizeDocumentCommand(d, c);
} catch (BadLocationException e) {
DLTKUIPlugin.log(e);
}
}
/**
* @param document
* @param c
* @throws BadLocationException
*/
private void smartInsertEndOnSpace(IDocument document, DocumentCommand c)
throws BadLocationException {
IRegion line = document.getLineInformationOfOffset(c.offset);
RubyHeuristicScanner scanner = new RubyHeuristicScanner(document);
int prevToken = scanner.previousToken(c.offset - 1, line.getOffset());
if (c.offset > 1 && prevToken == ISymbols.TokenEOF) {
return;
}
int prevTokenOffset = scanner.getPosition();
if (prevTokenOffset < 0)
prevTokenOffset = 0;
String previous = document.get(prevTokenOffset,
c.offset - prevTokenOffset).trim();
int hasOffset = line.getOffset();
int hasLength = (prevTokenOffset - line.getOffset());
boolean hasPrefixContent = ((hasLength > 0) && (document.get(hasOffset,
hasLength).trim().length() > 0));
hasOffset = (prevTokenOffset + previous.length() + 1);
hasLength = (line.getLength() - (hasOffset - line.getOffset()));
boolean hasSuffixContent = ((hasLength > 0)
&& ((hasOffset + hasLength) <= document.getLength()) && (document
.get(hasOffset, hasLength).trim().length() > 0));
if (!"case".equals(previous) && !"class".equals(previous) //$NON-NLS-1$ //$NON-NLS-2$
&& !"def".equals(previous) && !"do".equals(previous) //$NON-NLS-1$ //$NON-NLS-2$
&& !"if".equals(previous) && !"module".equals(previous) //$NON-NLS-1$ //$NON-NLS-2$
&& !"unless".equals(previous) //$NON-NLS-1$
&& !"while".equals(previous)) //$NON-NLS-1$
return;
if ((hasPrefixContent && !"do".equals(previous)) //$NON-NLS-1$
|| hasSuffixContent)
return;
if ((prevTokenOffset + previous.length()) < (c.offset - 1))
return;
if (fCloseBlocks && !isBlockClosed(document, c.offset)) {
c.caretOffset = c.offset + 1;
c.shiftsCaret = false;
c.text = c.text + TextUtilities.getDefaultLineDelimiter(document)
+ getBlockIndent(document, c.offset, scanner) + "end"; //$NON-NLS-1$
}
}
/**
* Tells whether the given inserted string represents hitting the Tab key.
*
* @param text
* the text to check
* @return <code>true</code> if the text represents hitting the Tab key
* @since 3.5
*/
private boolean isRepresentingTab(String text) {
if (text == null)
return false;
if (isInsertingSpacesForTab()) {
if (text.length() == 0
|| text.length() > getVisualTabLengthPreference())
return false;
for (int i = 0; i < text.length(); i++) {
if (text.charAt(i) != ' ')
return false;
}
return true;
} else
return text.length() == 1 && text.charAt(0) == '\t';
}
/**
* The preference setting that tells whether to insert spaces when pressing
* the Tab key.
*
* @return <code>true</code> if spaces are inserted when pressing the Tab
* key
* @since 3.5
*/
private boolean isInsertingSpacesForTab() {
return CodeFormatterConstants.SPACE.equals(getOption(RubyUI.PLUGIN_ID,
CodeFormatterConstants.FORMATTER_TAB_CHAR));
}
private IScriptProject getProject() {
// TODO implement getProject()
return null;
}
/**
* @param project
* @param qualifier
* @param key
* @return
*/
private String getOption(String qualifier, String key) {
return new PreferencesLookupDelegate(getProject()).getString(qualifier,
key);
}
/**
* @param project
* @param qualifier
* @param key
* @return
*/
private int getIntOption(String qualifier, String key) {
return new PreferencesLookupDelegate(getProject()).getInt(qualifier,
key);
}
private int getVisualTabLengthPreference() {
return getIntOption(RubyUI.PLUGIN_ID,
CodeFormatterConstants.FORMATTER_TAB_SIZE);
}
private boolean isSpace(String text) {
return text.length() == 1 && text.charAt(0) == ' ';
}
private boolean isLineDelimiter(IDocument document, String text) {
String[] delimiters = document.getLegalLineDelimiters();
return delimiters != null
&& TextUtilities.equals(delimiters, text) > -1;
}
private void smartTab(IDocument d, DocumentCommand c)
throws BadLocationException {
IRegion info = d.getLineInformationOfOffset(c.offset);
int endOffset = info.getOffset() + info.getLength();
String line = d.get(info.getOffset(), info.getLength());
String linePrefix = line.substring(0, c.offset - info.getOffset());
final String linePostfix = line.substring(c.offset - info.getOffset(),
endOffset - info.getOffset());
String postfixIndent = AutoEditUtils.getLineIndent(linePostfix);
RubyHeuristicScanner scanner = new RubyHeuristicScanner(d);
String rightIndent;
if (nextIsIdentToBlockToken(scanner, c.offset, endOffset)) {
rightIndent = getBlockIndent(d, c.offset, scanner);
} else {
rightIndent = getLineIndent(d, c.offset, scanner);
}
if (linePrefix.trim().length() != 0
|| (linePostfix.trim().length() != 0
&& postfixIndent.length() == 0 && computeVisualLength(linePrefix) >= computeVisualLength(rightIndent))) {
c.text = fPreferences.getIndent();
return;
}
c.text = rightIndent + linePostfix.trim();
c.offset = info.getOffset();
c.length = info.getLength();
c.caretOffset = info.getOffset() + rightIndent.length();
c.shiftsCaret = false;
}
private void smartIndentOnKeypress(IDocument d, DocumentCommand c)
throws BadLocationException {
RubyHeuristicScanner scanner = new RubyHeuristicScanner(d);
IRegion info = d.getLineInformationOfOffset(c.offset);
int token = scanner.previousTokenAfterInput(c.offset, c.text);
if (Arrays.binarySearch(INDENT_TO_BLOCK_TOKENS, token) >= 0) {
String indent = ""; //$NON-NLS-1$
indent = getBlockIndent(d, info.getOffset(), scanner);
// ssanders: If Block was opened on same line, add extra indent
int blockStart = scanner.findBlockBeginningOffset(c.offset);
int prevBlockStart = scanner.findBlockBeginningOffset(info
.getOffset());
if (blockStart >= info.getOffset() && prevBlockStart != -1)
indent += fPreferences.getIndent();
int pos = scanner.findNonWhitespaceForwardInAnyPartition(info
.getOffset(), c.offset);
String line = ""; //$NON-NLS-1$
if (pos != RubyHeuristicScanner.NOT_FOUND) {
line = d.get(pos, c.offset - pos);
}
c.text = indent + line + c.text;
c.length = c.offset - info.getOffset();
c.offset = info.getOffset();
} else if (Arrays.binarySearch(REMOVE_IDENTATION_TOKENS, token) >= 0) {
int start = scanner.findNonWhitespaceForward(info.getOffset(),
c.offset);
c.text = d.get(start, c.offset - start) + c.text;
c.length = c.offset - info.getOffset();
c.offset = info.getOffset();
} else {
// if previous was indented to block, restore original indentation
int wsPos = scanner.findNonIdentifierBackward(c.offset, info
.getOffset());
int previosToken = scanner.previousToken(c.offset, wsPos);
if (Arrays.binarySearch(INDENT_TO_BLOCK_TOKENS, previosToken) >= 0
&& Character.isJavaIdentifierPart(c.text.charAt(0))) {
String indent = getPreviousLineIndent(d, info.getOffset() - 1,
scanner);
int pos = scanner.findNonWhitespaceForwardInAnyPartition(info
.getOffset(), c.offset);
String line = ""; //$NON-NLS-1$
if (pos != RubyHeuristicScanner.NOT_FOUND) {
line = d.get(pos, c.offset - pos);
}
c.text = indent + line + c.text;
c.length = c.offset - info.getOffset();
c.offset = info.getOffset();
}
}
}
private String getLineIndent(IDocument d, int offset,
RubyHeuristicScanner scanner) {
int blockOffset = scanner.findBlockBeginningOffset(offset);
if (blockOffset != RubyHeuristicScanner.NOT_FOUND) {
try {
return AutoEditUtils.getLineIndent(d, d
.getLineOfOffset(blockOffset))
+ fPreferences.getIndent();
} catch (BadLocationException e) {
DLTKUIPlugin.log(e);
}
}
return ""; //$NON-NLS-1$
}
private String getBlockIndent(IDocument d, int offset,
RubyHeuristicScanner scanner) {
int blockOffset = scanner.findBlockBeginningOffset(offset);
if (blockOffset != RubyHeuristicScanner.NOT_FOUND) {
try {
return AutoEditUtils.getLineIndent(d, d
.getLineOfOffset(blockOffset));
} catch (BadLocationException e) {
DLTKUIPlugin.log(e);
}
}
return ""; //$NON-NLS-1$
}
private void smartIndentAfterNewLine(IDocument d, DocumentCommand c)
throws BadLocationException {
IRegion line = d.getLineInformationOfOffset(c.offset);
int lineEnd = line.getOffset() + line.getLength();
RubyHeuristicScanner scanner = new RubyHeuristicScanner(d);
// eat pending whitespace
int nonWsPos = scanner.findNonWhitespaceForwardInAnyPartition(c.offset,
lineEnd);
if (nonWsPos != RubyHeuristicScanner.NOT_FOUND) {
c.length = nonWsPos - c.offset;
}
// if pending statement is end, else etc. then indent it to block
// beginning
if (nextIsIdentToBlockToken(scanner, c.offset, lineEnd)) {
c.text += getBlockIndent(d, c.offset, scanner);
return;
}
// else
String indent = getPreviousLineIndent(d, c.offset, scanner);
c.text += indent;
if (previousIsBlockBeginning(d, scanner, c.offset)) {
// if this line was beginning of the block
c.text += fPreferences.getIndent();
// Auto close blocks
if (fCloseBlocks
&& scanner.isBlockBeginning(line.getOffset(), lineEnd)
&& !isBlockClosed(d, c.offset)) {
closeBlock(d, c, indent, d.get(c.offset, lineEnd - c.offset),
scanner);
}
} else if (previousIsFirstContinuation(d, scanner, c.offset, line
.getOffset())) {
// or if this line was the first line ending with one of
// continuation symbols
c.text += fPreferences.getIndent();
} else if (hasUnclosedParen(scanner, c.offset, line.getOffset())) {
// or if this line contains unclosed paren
c.text += fPreferences.getIndent();
}
}
private boolean hasUnclosedParen(RubyHeuristicScanner scanner, int offset,
int bound) {
int pos = scanner.findOpeningPeer(offset, bound, '(', ')');
return pos != RubyHeuristicScanner.NOT_FOUND;
}
private boolean previousIsFirstContinuation(IDocument d,
RubyHeuristicScanner scanner, int offset, int bound)
throws BadLocationException {
IRegion previousLine = null;
int line = d.getLineOfOffset(offset);
if (line > 0) {
previousLine = d.getLineInformation(line - 1);
}
return previousIsContinuation(scanner, offset, bound)
&& (previousLine == null || !previousIsContinuation(scanner,
previousLine.getOffset() + previousLine.getLength(),
previousLine.getOffset()));
}
private boolean previousIsContinuation(RubyHeuristicScanner scanner,
int offset, int bound) {
int token = scanner.previousToken(offset, bound);
return Arrays.binarySearch(CONTINUATION_TOKENS, token) >= 0;
}
private boolean previousIsBlockBeginning(IDocument d,
RubyHeuristicScanner scanner, int offset)
throws BadLocationException {
int previousLineOffset = scanner.findPrecedingNotEmptyLine(offset);
IRegion previousLine = d.getLineInformationOfOffset(previousLineOffset);
int previousLineEnd = Math.min(previousLine.getOffset()
+ previousLine.getLength(), offset);
boolean previousIsBlockBeginning = scanner.isBlockBeginning(
previousLine.getOffset(), previousLineEnd)
|| scanner.isBlockMiddle(previousLine.getOffset(),
previousLineEnd);
return previousIsBlockBeginning;
}
private boolean nextIsIdentToBlockToken(RubyHeuristicScanner scanner,
int offset, int bound) {
int token = scanner.nextToken(offset, bound);
return Arrays.binarySearch(INDENT_TO_BLOCK_TOKENS, token) >= 0;
}
private void smartPaste(IDocument d, DocumentCommand c)
throws BadLocationException {
// fix first line whitespace
IRegion info = d.getLineInformationOfOffset(c.offset);
String line = d.get(info.getOffset(), c.offset - info.getOffset());
int startFixFrom = 1;
if (line.trim().length() == 0) {
c.length += line.length();
c.offset -= line.length();
startFixFrom = 0;
}
RubyHeuristicScanner scanner = new RubyHeuristicScanner(d);
String indent = getLineIndent(d, c.offset, scanner);
String delimiter = TextUtilities.getDefaultLineDelimiter(d);
boolean addLastDelimiter = c.text.endsWith(delimiter);
String[] lines = c.text.split(delimiter);
if (lines.length > startFixFrom) {
String currentIndent = ""; //$NON-NLS-1$
for (int i = startFixFrom; i < lines.length; i++) {
if (lines[i].trim().length() != 0) {
currentIndent = AutoEditUtils.getLineIndent(lines[i]);
break;
}
}
int shift = computeVisualLength(indent)
- computeVisualLength(currentIndent);
StringBuffer result = new StringBuffer();
for (int i = 0; i < startFixFrom; i++) {
result.append(lines[i]).append(delimiter);
}
for (int i = startFixFrom; i < lines.length - 1; i++) {
result.append(shiftIdentation(lines[i], shift)).append(
delimiter);
}
result.append(shiftIdentation(lines[lines.length - 1], shift));
if (addLastDelimiter) {
result.append(delimiter);
}
c.text = result.toString();
}
}
private String shiftIdentation(String line, int shift) {
if (shift > 0) {
return fPreferences.getIndentByVirtualSize(shift) + line;
} else {
int pos = 0;
while (shift < 0 && pos < line.length()
&& Character.isWhitespace(line.charAt(pos))) {
shift += computeVisualLength(line.substring(pos, pos + 1));
pos++;
}
return line.substring(pos);
}
}
/**
* Computes the length of a <code>CharacterSequence</code>, counting a tab
* character as the size until the next tab stop and every other character
* as one.
*
* @param indent
* the string to measure
* @return the visual length in characters
*/
private int computeVisualLength(CharSequence indent) {
final int tabSize = fPreferences.getTabSize();
int length = 0;
for (int i = 0; i < indent.length(); i++) {
char ch = indent.charAt(i);
switch (ch) {
case '\t':
if (tabSize > 0) {
int reminder = length % tabSize;
length += tabSize - reminder;
}
break;
case ' ':
length++;
break;
}
}
return length;
}
/**
* Computes the indentation at <code>offset</code>.
*
* @param scanner
*
* @param offset
* the offset in the document
* @return a String which reflects the correct indentation for the line in
* which offset resides, or <code>null</code> if it cannot be
* determined
* @throws BadLocationException
*/
private String getPreviousLineIndent(IDocument d, int offset,
RubyHeuristicScanner scanner) throws BadLocationException {
StringBuffer result = new StringBuffer();
if (offset < 0 || d.getLength() == 0)
return result.toString();
// find start of line
int start = scanner.findPrecedingNotEmptyLine(offset);
IRegion info = d.getLineInformationOfOffset(start);
int end = scanner.findNonWhitespaceForwardInAnyPartition(start, start
+ info.getLength());
if (end > start) {
// append to input
result.append(d.get(start, end - start));
}
return result.toString();
}
private boolean isBlockClosed(IDocument document, int offset)
throws BadLocationException {
// TODO: Remove this comment when Ruby parser become able to report
// unclosed blocks
//
// RubyHeuristicScanner scanner = new RubyHeuristicScanner(document);
// IRegion sourceRange = scanner.findSurroundingBlock(offset);
// if (sourceRange != null) {
// String source = document.get(sourceRange.getOffset(), sourceRange
// .getLength());
// char[] buffer = source.toCharArray();
//
// SyntaxErrorListener listener = new SyntaxErrorListener();
// ISourceParser parser;
// try {
// parser = DLTKLanguageManager
// .getSourceParser(RubyNature.NATURE_ID);
// parser.parse(null, buffer, listener);
// if (listener.errorOccured())
// return false;
// } catch (CoreException e) {
// DLTKUIPlugin.log(e);
// }
// }
return getBlockBalance(document, offset) <= 0;
}
/**
* Returns the block balance, i.e. zero if the blocks are balanced at
* <code>offset</code>, a negative number if there are more closing than
* opening braces, and a positive number if there are more opening than
* closing braces.
*
* @param document
* @param offset
* @param partitioning
* @return the block balance
*/
private static int getBlockBalance(IDocument document, int offset) {
if (offset < 1)
return -1;
if (offset >= document.getLength())
return 1;
int begin = offset;
int end = offset - 1;
RubyHeuristicScanner scanner = new RubyHeuristicScanner(document);
while (true) {
begin = scanner.findBlockBeginningOffset(begin);
end = scanner.findBlockEndingOffset(end);
if (begin == RubyHeuristicScanner.NOT_FOUND
&& end == RubyHeuristicScanner.NOT_FOUND)
return 0;
if (begin == RubyHeuristicScanner.NOT_FOUND)
return -1;
if (end == RubyHeuristicScanner.NOT_FOUND)
return 1;
}
}
// TODO: Remove this comment when Ruby parser become able to report
// unclosed blocks
//
// private static class SyntaxErrorListener implements IProblemReporter {
// private boolean fError = false;
//
// public void clearMarkers() {
// }
//
// public IMarker reportProblem(IProblem problem) throws CoreException {
// int id = problem.getID();
// if ((id & IProblem.Syntax) != 0 || id == IProblem.Unclassified) {
// fError = true;
// }
// return null;
// }
//
// public boolean errorOccured() {
// return fError;
// }
// }
}