/******************************************************************************* * Copyright (c) 2014, 2015 Cisco Systems, Inc. and others. All rights reserved. * * This program and the accompanying materials are made available under the * terms of the Eclipse Public License v1.0 which accompanies this distribution, * and is available at http://www.eclipse.org/legal/epl-v10.html * *******************************************************************************/ package com.cisco.yangide.editor.editors; import org.eclipse.core.runtime.Assert; import org.eclipse.jdt.ui.PreferenceConstants; import org.eclipse.jface.preference.IPreferenceStore; import org.eclipse.jface.text.BadLocationException; import org.eclipse.jface.text.DefaultIndentLineAutoEditStrategy; import org.eclipse.jface.text.Document; import org.eclipse.jface.text.DocumentCommand; import org.eclipse.jface.text.DocumentRewriteSession; import org.eclipse.jface.text.DocumentRewriteSessionType; import org.eclipse.jface.text.IDocument; import org.eclipse.jface.text.IRegion; import org.eclipse.jface.text.TextUtilities; import org.eclipse.jface.text.source.ISourceViewer; import org.eclipse.ui.texteditor.AbstractDecoratedTextEditorPreferenceConstants; import com.cisco.yangide.editor.YangEditorPlugin; import com.cisco.yangide.editor.editors.text.Symbols; import com.cisco.yangide.editor.editors.text.YangHeuristicScanner; import com.cisco.yangide.editor.editors.text.YangIndenter; import com.cisco.yangide.editor.preferences.YangDocumentSetupParticipant; import com.cisco.yangide.ui.YangUIPlugin; import com.cisco.yangide.ui.preferences.YangPreferenceConstants; /** * Auto indent strategy sensitive to brackets. * * @author Alexey Kholupko */ public class YangAutoIndentStrategy extends DefaultIndentLineAutoEditStrategy { /** The line comment introducer. Value is "{@value} " */ private static final String LINE_COMMENT = "//"; //$NON-NLS-1$ private boolean fCloseBrace; private boolean fIsSmartIndentAfterNewline; private String fPartitioning; /** * The viewer. */ private final ISourceViewer fViewer; /** * Creates a new YANG auto indent strategy for the given document partitioning. * * @param partitioning the document partitioning * @param viewer the source viewer that this strategy is attached to */ public YangAutoIndentStrategy(String partitioning, ISourceViewer viewer) { fPartitioning = partitioning; fViewer = viewer; } private int getBracketCount(IDocument d, int startOffset, int endOffset, boolean ignoreCloseBrackets) throws BadLocationException { int bracketCount = 0; while (startOffset < endOffset) { char curr = d.getChar(startOffset); startOffset++; switch (curr) { case '/': if (startOffset < endOffset) { char next = d.getChar(startOffset); if (next == '*') { // a comment starts, advance to the comment end startOffset = getCommentEnd(d, startOffset + 1, endOffset); } else if (next == '/') { // '//'-comment: nothing to do anymore on this line startOffset = endOffset; } } break; case '*': if (startOffset < endOffset) { char next = d.getChar(startOffset); if (next == '/') { // we have been in a comment: forget what we read before bracketCount = 0; startOffset++; } } break; case '{': bracketCount++; ignoreCloseBrackets = false; break; case '}': if (!ignoreCloseBrackets) { bracketCount--; } break; case '"': case '\'': startOffset = getStringEnd(d, startOffset, endOffset, curr); break; default: } } return bracketCount; } // ----------- bracket counting ------------------------------------------------------ private int getCommentEnd(IDocument d, int offset, int endOffset) throws BadLocationException { while (offset < endOffset) { char curr = d.getChar(offset); offset++; if (curr == '*') { if (offset < endOffset && d.getChar(offset) == '/') { return offset + 1; } } } return endOffset; } private String getIndentOfLine(IDocument d, int line) throws BadLocationException { if (line > -1) { int start = d.getLineOffset(line); int end = start + d.getLineLength(line) - 1; int whiteEnd = findEndOfWhiteSpace(d, start, end); return d.get(start, whiteEnd - start); } else { return ""; //$NON-NLS-1$ } } private int getStringEnd(IDocument d, int offset, int endOffset, char ch) throws BadLocationException { while (offset < endOffset) { char curr = d.getChar(offset); offset++; if (curr == '\\') { // ignore escaped characters offset++; } else if (curr == ch) { return offset; } } return endOffset; } private void smartIndentAfterClosingBracket(IDocument d, DocumentCommand c) { if (c.offset == -1 || d.getLength() == 0) { return; } try { int p = (c.offset == d.getLength() ? c.offset - 1 : c.offset); int line = d.getLineOfOffset(p); int start = d.getLineOffset(line); int whiteend = findEndOfWhiteSpace(d, start, c.offset); YangHeuristicScanner scanner = new YangHeuristicScanner(d); YangIndenter indenter = new YangIndenter(d, scanner); // shift only when line does not contain any text up to the closing bracket if (whiteend == c.offset) { // evaluate the line with the opening bracket that matches // out closing bracket int reference = indenter.findReferencePosition(c.offset, false, true, false, false); int indLine = d.getLineOfOffset(reference); if (indLine != -1 && indLine != line) { // take the indent of the found line StringBuffer replaceText = new StringBuffer(getIndentOfLine(d, indLine)); // add the rest of the current line including the just added close bracket replaceText.append(d.get(whiteend, c.offset - whiteend)); replaceText.append(c.text); // modify document command c.length += c.offset - start; c.offset = start; c.text = replaceText.toString(); } } } catch (BadLocationException e) { YangEditorPlugin.log(e); } } private void smartIndentAfterOpeningBracket(IDocument d, DocumentCommand c) { if (c.offset < 1 || d.getLength() == 0) { return; } YangHeuristicScanner scanner = new YangHeuristicScanner(d); int p = (c.offset == d.getLength() ? c.offset - 1 : c.offset); try { // current line int line = d.getLineOfOffset(p); int lineOffset = d.getLineOffset(line); // make sure we don't have any leading comments etc. if (d.get(lineOffset, p - lineOffset).trim().length() != 0) { return; } // line of last Java code int pos = scanner.findNonWhitespaceBackward(p, YangHeuristicScanner.UNBOUND); if (pos == -1) { return; } int lastLine = d.getLineOfOffset(pos); // only shift if the last java line is further up and is a braceless block candidate if (lastLine < line) { YangIndenter indenter = new YangIndenter(d, scanner); StringBuffer indent = indenter.computeIndentation(p, true); String toDelete = d.get(lineOffset, c.offset - lineOffset); if (indent != null && !indent.toString().equals(toDelete)) { c.text = indent.append(c.text).toString(); c.length += c.offset - lineOffset; c.offset = lineOffset; } } } catch (BadLocationException e) { YangEditorPlugin.log(e); } } private void smartIndentAfterNewLine(IDocument d, DocumentCommand c) { YangHeuristicScanner scanner = new YangHeuristicScanner(d); YangIndenter indenter = new YangIndenter(d, scanner); StringBuffer indent = indenter.computeIndentation(c.offset); if (indent == null) { indent = new StringBuffer(); } // indent.append(" "); int docLength = d.getLength(); if (c.offset == -1 || docLength == 0) { return; } try { int p = (c.offset == docLength ? c.offset - 1 : c.offset); int line = d.getLineOfOffset(p); StringBuffer buf = new StringBuffer(c.text + indent); IRegion reg = d.getLineInformation(line); int lineEnd = reg.getOffset() + reg.getLength(); int contentStart = findEndOfWhiteSpace(d, c.offset, lineEnd); c.length = Math.max(contentStart - c.offset, 0); int start = reg.getOffset(); // insert closing brace on new line after an unclosed opening brace if (getBracketCount(d, start, c.offset, true) > 0 && closeBrace() && !isClosed(d, c.offset, c.length)) { c.caretOffset = c.offset + buf.length(); c.shiftsCaret = false; // copy old content of line behind insertion point to new line if (c.offset == 0) { if (lineEnd - contentStart > 0) { c.length = lineEnd - c.offset; buf.append(d.get(contentStart, lineEnd - contentStart).toCharArray()); } } buf.append(TextUtilities.getDefaultLineDelimiter(d)); StringBuffer reference = null; int nonWS = findEndOfWhiteSpace(d, start, lineEnd); if (nonWS < c.offset && d.getChar(nonWS) == '{') { reference = new StringBuffer(d.get(start, nonWS - start)); } else { reference = indenter.getReferenceIndentation(c.offset); } if (reference != null) { buf.append(reference); } buf.append('}'); } // insert extra line upon new line between two braces else if (c.offset > start && contentStart < lineEnd && d.getChar(contentStart) == '}') { int firstCharPos = scanner.findNonWhitespaceBackward(c.offset - 1, start); if (firstCharPos != YangHeuristicScanner.NOT_FOUND && d.getChar(firstCharPos) == '{') { c.caretOffset = c.offset + buf.length(); c.shiftsCaret = false; StringBuffer reference = null; int nonWS = findEndOfWhiteSpace(d, start, lineEnd); if (nonWS < c.offset && d.getChar(nonWS) == '{') { reference = new StringBuffer(d.get(start, nonWS - start)); } else { reference = indenter.getReferenceIndentation(c.offset); } buf.append(TextUtilities.getDefaultLineDelimiter(d)); if (reference != null) { buf.append(reference); } } } c.text = buf.toString(); } catch (BadLocationException e) { YangEditorPlugin.log(e); } } /* * @see org.eclipse.jdt.internal.ui.text.java.JavaAutoIndentStrategy#isClosed */ private boolean isClosed(IDocument document, int offset, int length) { return getBlockBalance(document, offset, fPartitioning) <= 0; } private void smartPaste(IDocument document, DocumentCommand command) { int newOffset = command.offset; int newLength = command.length; String newText = command.text; try { YangHeuristicScanner scanner = new YangHeuristicScanner(document); YangIndenter indenter = new YangIndenter(document, scanner); int offset = newOffset; // reference position to get the indent from int refOffset = indenter.findReferencePosition(offset); if (refOffset == YangHeuristicScanner.NOT_FOUND) { return; } int peerOffset = getPeerPosition(document, command); peerOffset = indenter.findReferencePosition(peerOffset); if (peerOffset != YangHeuristicScanner.NOT_FOUND) { refOffset = Math.min(refOffset, peerOffset); } // eat any WS before the insertion to the beginning of the line int firstLine = 1; // don't format the first line per default, as it has other content // before it IRegion line = document.getLineInformationOfOffset(offset); String notSelected = document.get(line.getOffset(), offset - line.getOffset()); if (notSelected.trim().length() == 0) { newLength += notSelected.length(); newOffset = line.getOffset(); firstLine = 0; } // prefix: the part we need for formatting but won't paste IRegion refLine = document.getLineInformationOfOffset(refOffset); String prefix = document.get(refLine.getOffset(), newOffset - refLine.getOffset()); // handle the indentation computation inside a temporary document Document temp = new Document(prefix + newText); DocumentRewriteSession session = temp.startRewriteSession(DocumentRewriteSessionType.STRICTLY_SEQUENTIAL); scanner = new YangHeuristicScanner(temp); indenter = new YangIndenter(temp, scanner); installYangStuff(temp); // indent the first and second line // compute the relative indentation difference from the second line // (as the first might be partially selected) and use the value to // indent all other lines. boolean isIndentDetected = false; StringBuffer addition = new StringBuffer(); int insertLength = 0; int firstLineInsertLength = 0; int firstLineIndent = 0; int first = document.computeNumberOfLines(prefix) + firstLine; // don't format first // line int lines = temp.getNumberOfLines(); int tabLength = getVisualTabLengthPreference(); boolean changed = false; for (int l = first; l < lines; l++) { // we don't change the number of lines while // adding indents IRegion r = temp.getLineInformation(l); int lineOffset = r.getOffset(); int lineLength = r.getLength(); if (lineLength == 0) { continue; } if (!isIndentDetected) { // indent the first pasted line String current = getCurrentIndent(temp, l); StringBuffer correct = indenter.computeIndentation(lineOffset); if (correct == null) { return; // bail out } insertLength = subtractIndent(correct, current, addition, tabLength); if (l == first) { firstLineInsertLength = insertLength; firstLineIndent = current.length(); } if (l != first && temp.get(lineOffset, lineLength).trim().length() != 0) { isIndentDetected = true; if (firstLineIndent >= current.length()) { insertLength = firstLineInsertLength; } if (insertLength == 0) { // no adjustment needed, bail out if (firstLine == 0) { // but we still need to adjust the first line command.offset = newOffset; command.length = newLength; if (changed) { break; // still need to get the leading indent of the first line } } return; } } else { changed = insertLength != 0; } } // relatively indent all pasted lines if (insertLength > 0) { addIndent(temp, l, addition, tabLength); } else if (insertLength < 0) { cutIndent(temp, l, -insertLength, tabLength); } } removeYangStuff(temp); temp.stopRewriteSession(session); newText = temp.get(prefix.length(), temp.getLength() - prefix.length()); command.offset = newOffset; command.length = newLength; command.text = newText; } catch (BadLocationException e) { YangEditorPlugin.log(e); } } private void installYangStuff(Document temp) { YangDocumentSetupParticipant setupParticipant = new YangDocumentSetupParticipant(); setupParticipant.setup(temp); } private static void removeYangStuff(Document document) { document.setDocumentPartitioner(YangDocumentSetupParticipant.YANG_PARTITIONING, null); } /** * Returns the indentation of the line <code>line</code> in <code>document</code>. The returned * string may contain pairs of leading slashes that are considered part of the indentation. */ private static String getCurrentIndent(Document document, int line) throws BadLocationException { IRegion region = document.getLineInformation(line); int from = region.getOffset(); int endOffset = region.getOffset() + region.getLength(); // go behind line comments int to = from; while (to < endOffset - 2 && document.get(to, 2).equals(LINE_COMMENT)) { to += 2; } while (to < endOffset) { char ch = document.getChar(to); if (!Character.isWhitespace(ch)) { break; } to++; } // don't count the space before javadoc like, asterisk-style comment lines if (to > from && to < endOffset - 1 && document.get(to - 1, 2).equals(" *")) { //$NON-NLS-1$ String type = TextUtilities.getContentType(document, YangDocumentSetupParticipant.YANG_PARTITIONING, to, true); if (type.equals(YangPartitionScanner.YANG_COMMENT)) { to--; } } return document.get(from, to - from); } /** * Computes the difference of two indentations and returns the difference in length of current * and correct. If the return value is positive, <code>addition</code> is initialized with a * substring of that length of <code>correct</code>. */ private int subtractIndent(CharSequence correctIndentation, CharSequence currentIndentation, StringBuffer difference, int tabLength) { int c1 = computeVisualLength(correctIndentation, tabLength); int c2 = computeVisualLength(currentIndentation, tabLength); int diff = c1 - c2; if (diff <= 0) { return diff; } difference.setLength(0); int len = 0, i = 0; while (len < diff) { char c = correctIndentation.charAt(i++); difference.append(c); len += computeVisualLength(c, tabLength); } return diff; } /** * Indents line <code>line</code> in <code>document</code> with <code>indent</code>. Leaves * leading comment signs alone. */ private void addIndent(Document document, int line, CharSequence indent, int tabLength) throws BadLocationException { IRegion region = document.getLineInformation(line); int insert = region.getOffset(); int endOffset = region.getOffset() + region.getLength(); // Compute insert after all leading line comment markers int newInsert = insert; while (newInsert < endOffset - 2 && document.get(newInsert, 2).equals(LINE_COMMENT)) { newInsert += 2; } // Heuristic to check whether it is commented code or just a comment if (newInsert > insert) { int whitespaceCount = 0; int i = newInsert; while (i < endOffset - 1) { char ch = document.get(i, 1).charAt(0); if (!Character.isWhitespace(ch)) { break; } whitespaceCount = whitespaceCount + computeVisualLength(ch, tabLength); i++; } // TODO if (whitespaceCount != 0) { // CodeFormatterUtil.getIndentWidth(fProject)) insert = newInsert; } } // Insert indent document.replace(insert, 0, indent.toString()); } /** * Cuts the visual equivalent of <code>toDelete</code> characters out of the indentation of line * <code>line</code> in <code>document</code>. Leaves leading comment signs alone. */ private void cutIndent(Document document, int line, int toDelete, int tabLength) throws BadLocationException { IRegion region = document.getLineInformation(line); int from = region.getOffset(); int endOffset = region.getOffset() + region.getLength(); // go behind line comments while (from < endOffset - 2 && document.get(from, 2).equals(LINE_COMMENT)) { from += 2; } int to = from; while (toDelete > 0 && to < endOffset) { char ch = document.getChar(to); if (!Character.isWhitespace(ch)) { break; } toDelete -= computeVisualLength(ch, tabLength); if (toDelete >= 0) { to++; } else { break; } } document.replace(from, to - from, ""); //$NON-NLS-1$ } /** * Returns the visual length of a given <code>CharSequence</code> taking into account the visual * tabulator length. */ private int computeVisualLength(CharSequence seq, int tabLength) { int size = 0; for (int i = 0; i < seq.length(); i++) { char ch = seq.charAt(i); if (ch == '\t') { if (tabLength != 0) { size += tabLength - size % tabLength; // else: size stays the same } } else { size++; } } return size; } /** * Returns the visual length of a given character taking into account the visual tabulator * length. */ private int computeVisualLength(char ch, int tabLength) { if (ch == '\t') { return tabLength; } else { return 1; } } /** * The preference setting for the visual tabulator display. */ private int getVisualTabLengthPreference() { return YangEditorPlugin.getDefault().getCombinedPreferenceStore() .getInt(AbstractDecoratedTextEditorPreferenceConstants.EDITOR_TAB_WIDTH); } private boolean isLineDelimiter(IDocument document, String text) { String[] delimiters = document.getLegalLineDelimiters(); if (delimiters != null) { return TextUtilities.equals(delimiters, text) > -1; } return false; } private void smartIndentOnKeypress(IDocument document, DocumentCommand command) { switch (command.text.charAt(0)) { case '}': smartIndentAfterClosingBracket(document, command); break; case '{': smartIndentAfterOpeningBracket(document, command); break; } } /* * @see * org.eclipse.jface.text.IAutoIndentStrategy#customizeDocumentCommand(org.eclipse.jface.text * .IDocument, org.eclipse.jface.text.DocumentCommand) */ @Override public void customizeDocumentCommand(IDocument d, DocumentCommand c) { if (c.doit == false) { return; } clearCachedValues(); if (c.length == 0 && c.text != null && isLineDelimiter(d, c.text)) { if (fIsSmartIndentAfterNewline) { smartIndentAfterNewLine(d, c); } } else if (c.text.length() == 1) { smartIndentOnKeypress(d, c); } else if (c.text.length() > 1 && getPreferenceStore().getBoolean(PreferenceConstants.EDITOR_SMART_PASTE)) { if (fViewer == null || fViewer.getTextWidget() == null || !fViewer.getTextWidget().getBlockSelection()) { smartPaste(d, c); // no smart backspace for paste } } } private static IPreferenceStore getPreferenceStore() { return YangUIPlugin.getDefault().getPreferenceStore(); } private boolean closeBrace() { return fCloseBrace; } private void clearCachedValues() { IPreferenceStore preferenceStore = getPreferenceStore(); fCloseBrace = preferenceStore.getBoolean(YangPreferenceConstants.EDITOR_CLOSE_BRACES); // fIsSmartTab = preferenceStore.getBoolean(YangPreferenceConstants.EDITOR_SMART_TAB); fIsSmartIndentAfterNewline = preferenceStore .getBoolean(YangPreferenceConstants.EDITOR_SMART_INDENT_AFTER_NEWLINE); // fIsSmartMode = computeSmartMode(); } // private boolean computeSmartMode() { // IWorkbenchPage page = YangEditorPlugin.getActivePage(); // if (page != null) { // IEditorPart part = page.getActiveEditor(); // if (part instanceof ITextEditorExtension3) { // ITextEditorExtension3 extension = (ITextEditorExtension3) part; // return extension.getInsertMode() == ITextEditorExtension3.SMART_INSERT; // } // } // return false; // } /** * 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. */ private static int getBlockBalance(IDocument document, int offset, String partitioning) { if (offset < 1) { return -1; } if (offset >= document.getLength()) { return 1; } int begin = offset; int end = offset - 1; YangHeuristicScanner scanner = new YangHeuristicScanner(document); while (true) { begin = scanner.findOpeningPeer(begin - 1, '{', '}'); end = scanner.findClosingPeer(end + 1, '{', '}'); if (begin == -1 && end == -1) { return 0; } if (begin == -1) { return -1; } if (end == -1) { return 1; } } } private int getPeerPosition(IDocument document, DocumentCommand command) { if (document.getLength() == 0) { return 0; } /* * Search for scope closers in the pasted text and find their opening peers in the document. */ Document pasted = new Document(command.text); installYangStuff(pasted); int firstPeer = command.offset; YangHeuristicScanner pScanner = new YangHeuristicScanner(pasted); YangHeuristicScanner dScanner = new YangHeuristicScanner(document); // add scope relevant after context to peer search int afterToken = dScanner.nextToken(command.offset + command.length, YangHeuristicScanner.UNBOUND); try { switch (afterToken) { case Symbols.TokenRBRACE: pasted.replace(pasted.getLength(), 0, "}"); //$NON-NLS-1$ break; case Symbols.TokenRPAREN: pasted.replace(pasted.getLength(), 0, ")"); //$NON-NLS-1$ break; case Symbols.TokenRBRACKET: pasted.replace(pasted.getLength(), 0, "]"); //$NON-NLS-1$ break; } } catch (BadLocationException e) { // cannot happen Assert.isTrue(false); } int pPos = 0; // paste text position (increasing from 0) int dPos = Math.max(0, command.offset - 1); // document position (decreasing from paste // offset) while (true) { int token = pScanner.nextToken(pPos, YangHeuristicScanner.UNBOUND); pPos = pScanner.getPosition(); switch (token) { case Symbols.TokenLBRACE: case Symbols.TokenLBRACKET: case Symbols.TokenLPAREN: pPos = skipScope(pScanner, pPos, token); if (pPos == YangHeuristicScanner.NOT_FOUND) { return firstPeer; } break; // closed scope -> keep searching case Symbols.TokenRBRACE: int peer = dScanner.findOpeningPeer(dPos, '{', '}'); dPos = peer - 1; if (peer == YangHeuristicScanner.NOT_FOUND) { return firstPeer; } firstPeer = peer; break; // keep searching case Symbols.TokenRBRACKET: peer = dScanner.findOpeningPeer(dPos, '[', ']'); dPos = peer - 1; if (peer == YangHeuristicScanner.NOT_FOUND) { return firstPeer; } firstPeer = peer; break; // keep searching case Symbols.TokenRPAREN: peer = dScanner.findOpeningPeer(dPos, '(', ')'); dPos = peer - 1; if (peer == YangHeuristicScanner.NOT_FOUND) { return firstPeer; } firstPeer = peer; break; // keep searching case Symbols.TokenEOF: return firstPeer; default: // keep searching } } } /** * Skips the scope opened by <code>token</code>. */ private static int skipScope(YangHeuristicScanner scanner, int startPosition, int token) { int openToken = token; int closeToken; switch (token) { case Symbols.TokenLPAREN: closeToken = Symbols.TokenRPAREN; break; case Symbols.TokenLBRACKET: closeToken = Symbols.TokenRBRACKET; break; case Symbols.TokenLBRACE: closeToken = Symbols.TokenRBRACE; break; default: Assert.isTrue(false); return -1; // dummy } int depth = 1; int p = startPosition; while (true) { int tok = scanner.nextToken(p, YangHeuristicScanner.UNBOUND); p = scanner.getPosition(); if (tok == openToken) { depth++; } else if (tok == closeToken) { depth--; if (depth == 0) { return p + 1; } } else if (tok == Symbols.TokenEOF) { return YangHeuristicScanner.NOT_FOUND; } } } }