/******************************************************************************* * Copyright (c) 2010, 2011 IBM Corporation 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 * * Contributors: * Bruno Medeiros - initial API and implementation *******************************************************************************/ package melnorme.lang.ide.core.text.format; import static melnorme.utilbox.core.Assert.AssertNamespace.assertTrue; import static melnorme.utilbox.core.Assert.AssertNamespace.assertUnreachable; import static melnorme.utilbox.core.CoreUtil.array; import org.eclipse.jface.text.BadLocationException; import org.eclipse.jface.text.IDocument; import org.eclipse.jface.text.IDocumentExtension3; import org.eclipse.jface.text.IRegion; import org.eclipse.jface.text.ITypedRegion; import org.eclipse.jface.text.TextUtilities; import melnorme.lang.ide.core.text.BlockHeuristicsScannner; import melnorme.lang.ide.core.text.BlockHeuristicsScannner.BlockBalanceResult; import melnorme.lang.ide.core.text.BlockHeuristicsScannner.BlockTokenRule; import melnorme.lang.ide.core.text.DocumentCommand2; import melnorme.lang.ide.core.text.TextSourceUtils; /** * LangAutoEditStrategy provides a common auto-edit strategy of smart indenting and de-indenting, * for block based languages (like C-style languages) * * TODO smart paste * * @author BrunoM */ public class LangAutoEditStrategy extends AbstractAutoEditStrategy { protected static final BlockTokenRule[] BLOCK_RULES_Braces = array(new BlockTokenRule('{', '}')); protected static final BlockTokenRule[] BLOCK_RULES_BracesAndParenthesis = array(new BlockTokenRule('{', '}'), new BlockTokenRule('(', ')')); protected final ILangAutoEditsPreferencesAccess preferences; protected final String partitioning; protected final String contentType; protected LangAutoEditStrategy(ILastKeyInfoProvider lastKeyInfoProvider, ILangAutoEditsPreferencesAccess preferences) { this(lastKeyInfoProvider, IDocumentExtension3.DEFAULT_PARTITIONING, IDocument.DEFAULT_CONTENT_TYPE, preferences); } public LangAutoEditStrategy(ILastKeyInfoProvider lastKeyInfoProvider, String partitioning, String contentType, ILangAutoEditsPreferencesAccess preferences ) { super(lastKeyInfoProvider); this.preferences = preferences; this.partitioning = partitioning; this.contentType = contentType; } protected boolean parenthesesAsBlocks; protected String indentUnit; @Override protected void doCustomizeDocumentCommand(IDocument doc, DocumentCommand2 cmd) throws BadLocationException { parenthesesAsBlocks = preferences.parenthesesAsBlocks(); indentUnit = preferences.getIndentUnit(); boolean isSmartIndent = preferences.isSmartIndent(); if(isSmartIndent && isSimpleNewLineKeyPress(cmd)) { smartIndentAfterNewLine(doc, cmd); } else if(smartDeIndentAfterDeletion(doc, cmd)) { return; } else if(isSmartIndent && isSimpleKeyPressCommand(cmd)) { smartIndentOnKeypress(doc, cmd); } else { } } protected BlockHeuristicsScannner createBlockHeuristicsScanner(IDocument doc) { return new BlockHeuristicsScannner(doc, partitioning, contentType, getBlockRules()); } protected BlockTokenRule[] getBlockRules() { if(parenthesesAsBlocks) { return BLOCK_RULES_BracesAndParenthesis; } else { return BLOCK_RULES_Braces; } } /* ------------------------------------- */ protected void smartIndentAfterNewLine(IDocument doc, DocumentCommand2 cmd) throws BadLocationException { if(cmd.length > 0 || cmd.text == null) return; IRegion lineRegion = doc.getLineInformationOfOffset(cmd.offset); int lineEnd = getRegionEnd(lineRegion); int postWsEndPos = TextSourceUtils.findEndOfIndent(docContents, cmd.offset); boolean hasPendingTextAfterEdit = postWsEndPos != lineEnd; BlockHeuristicsScannner bhscanner = createBlockHeuristicsScanner(doc); int offsetForBalanceCalculation = findOffsetForBalanceCalculation(doc, bhscanner, cmd.offset); int lineForBalanceCalculation = doc.getLineOfOffset(cmd.offset); // Find block balances of preceding text (line start to edit cursor) LineIndentResult nli = determineIndent(doc, bhscanner, lineForBalanceCalculation, offsetForBalanceCalculation); cmd.text += nli.nextLineIndent; BlockBalanceResult blockInfo = nli.blockInfo; if(blockInfo.unbalancedOpens > 0) { if(preferences.closeBraces() && !hasPendingTextAfterEdit){ if(bhscanner.shouldCloseBlock(blockInfo.rightmostUnbalancedBlockOpenOffset)) { //close block cmd.caretOffset = cmd.offset + cmd.text.length(); cmd.shiftsCaret = false; String delimiter = TextUtilities.getDefaultLineDelimiter(doc); char openChar = doc.getChar(blockInfo.rightmostUnbalancedBlockOpenOffset); char closeChar = bhscanner.getClosingPeer(openChar); cmd.text += delimiter + addIndent(nli.editLineIndent, blockInfo.unbalancedOpens - 1) + closeChar; } } return; } } protected int findOffsetForBalanceCalculation(IDocument doc, BlockHeuristicsScannner bhscanner, int offset) { while(offset < doc.getLength()) { char ch; try { ch = doc.getChar(offset); } catch(BadLocationException e) { break; } if(!bhscanner.isClosingBrace(ch)) { break; } offset++; } return offset; } public static class LineIndentResult { String editLineIndent; String nextLineIndent; BlockBalanceResult blockInfo; public LineIndentResult(String editLineIndent, String nextLineIndent, BlockBalanceResult blockInfo) { this.editLineIndent = editLineIndent; this.nextLineIndent = nextLineIndent; this.blockInfo = blockInfo; } } protected final LineIndentResult determineIndent(IDocument doc, BlockHeuristicsScannner bhscanner, int line) throws BadLocationException { IRegion lineRegion = doc.getLineInformation(line); return determineIndent(doc, bhscanner, line, getRegionEnd(lineRegion)); } protected LineIndentResult determineIndent(IDocument doc, BlockHeuristicsScannner bhscanner, final int line, final int editOffset) throws BadLocationException { IRegion lineRegion = doc.getLineInformation(line); final int lineStart = lineRegion.getOffset(); assertTrue(lineStart <= editOffset && editOffset <= getRegionEnd(lineRegion)); ITypedRegion partition = bhscanner.getPartition(lineStart); if(partitionIsIgnoredForLineIndentString(editOffset, partition)) { if(line == 0) { // empty/zero block balance return new LineIndentResult("", "", new BlockBalanceResult()); } else { return determineIndent(doc, bhscanner, line-1); } } BlockBalanceResult blockInfo = bhscanner.calculateBlockBalances(lineStart, editOffset); if(blockInfo.unbalancedOpens == 0 && blockInfo.unbalancedCloses > 0) { int blockStartOffset = bhscanner.findBlockStart(blockInfo.rightmostUnbalancedBlockCloseOffset); assertTrue(doc.getLineOfOffset(blockStartOffset) <= doc.getLineOfOffset(lineStart)); String startLineIndent = getLineIndentForOffset(blockStartOffset); // Now calculate the balance for the block start line, before the block start int lineOffset = TextSourceUtils.findLineStartForOffset(docContents, blockStartOffset); BlockBalanceResult blockStartInfo = bhscanner.calculateBlockBalances(lineOffset, blockStartOffset); // Add the indent of the start line, plus the unbalanced opens there String newLineIndent = addIndent(startLineIndent, blockStartInfo.unbalancedOpens); return new LineIndentResult(null, newLineIndent, blockInfo); } // The indent string to be added to the new line String lineIndent = getLineIndentForLineStart(lineStart, editOffset); if(blockInfo.unbalancedOpens == 0 && blockInfo.unbalancedCloses == 0) { return new LineIndentResult(null, lineIndent, blockInfo); // finished } if(blockInfo.unbalancedOpens > 0) { String newLineIndent = addIndent(lineIndent, blockInfo.unbalancedOpens); // cache lineIndent so as to not recalculate return new LineIndentResult(lineIndent, newLineIndent, blockInfo); // finished } throw assertUnreachable(); } /** Subclasses may override. */ protected boolean partitionIsIgnoredForLineIndentString(final int editOffset, ITypedRegion partition) { return partitionTypeIsIgnoredForLineIndentString(partition) && editOffset <= getRegionEnd(partition); } /** Subclasses may override. */ protected boolean partitionTypeIsIgnoredForLineIndentString(ITypedRegion partition) { return !partition.getType().equals(IDocument.DEFAULT_CONTENT_TYPE); } protected String addIndent(String indentStr, int indentDelta) { return indentStr + TextSourceUtils.stringNTimes(indentUnit, indentDelta); } /* ------------------------------------- */ protected boolean smartDeIndentAfterDeletion(IDocument doc, DocumentCommand2 cmd) throws BadLocationException { if(!preferences.isSmartDeIndent()) return false; if(!cmd.text.isEmpty()) return false; IRegion lineRegion = doc.getLineInformationOfOffset(cmd.offset); int lineEnd = getRegionEnd(lineRegion); int line = doc.getLineOfOffset(cmd.offset); // Delete at beginning of NL if(cmd.offset == lineEnd && lengthMatchesLineDelimiter(cmd.length, doc.getLineDelimiter(line))) { if(keyWasBackspace()) return false; // Only Delete key should trigger this edit int indentLine = line+1; if(indentLine < doc.getNumberOfLines()) { assertTrue(doc.getLineInformation(indentLine).getOffset() == cmd.offset + cmd.length); IRegion indentLineRegion = doc.getLineInformation(indentLine); int indentEnd = findEndOfIndent(indentLineRegion.getOffset()); String deletableIndentStr = calculateDeletableIndent(doc, indentLine, indentEnd); if(equalsDocumentString(deletableIndentStr, doc, indentLineRegion)) { cmd.length += deletableIndentStr.length(); return true; } } return false; } // Backspace at end of indent case if(cmd.length == 1 && isIndentWhiteSpace(doc.getChar(cmd.offset)) && line > 0) { if(keyWasDelete()) return false; // Only Backspace key should trigger this IRegion indentLineRegion = lineRegion; int indentLine = line; int indentEnd = findEndOfIndent(indentLineRegion.getOffset()); if(cmd.offset < indentEnd) { // potentially true String deletableIndentStr = calculateDeletableIndent(doc, indentLine, indentEnd); if(equalsDocumentString(deletableIndentStr, doc, indentLineRegion)) { int acceptedIndentEnd = indentLineRegion.getOffset() + deletableIndentStr.length(); if(cmd.offset == acceptedIndentEnd-1) { int lineDelimLen = doc.getLineDelimiter(line-1).length(); cmd.offset = indentLineRegion.getOffset() - lineDelimLen; cmd.length = lineDelimLen + deletableIndentStr.length(); return true; } } } return false; } return false; } protected static boolean lengthMatchesLineDelimiter(int length, String lineDelimiter) { return lineDelimiter != null && length == lineDelimiter.length(); } protected boolean isIndentWhiteSpace(char ch) throws BadLocationException { return ch == ' ' || ch == '\t'; } protected String calculateDeletableIndent(IDocument doc, int indentedLine, int indentEnd) throws BadLocationException { IRegion indentedLineRegion = doc.getLineInformation(indentedLine); String expectedIndentStr = determineExpectedIndent(doc, indentedLine-1); int indentLength = indentEnd - indentedLineRegion.getOffset(); if(indentLength < expectedIndentStr.length()) { // cap expected length expectedIndentStr = expectedIndentStr.substring(0, indentLength); } return expectedIndentStr; } protected String determineExpectedIndent(IDocument doc, int line) throws BadLocationException { BlockHeuristicsScannner bhscanner = createBlockHeuristicsScanner(doc); LineIndentResult nli = determineIndent(doc, bhscanner, line); String expectedIndentStr = nli.nextLineIndent; return expectedIndentStr; } /* ------------------------------------- */ protected void smartIndentOnKeypress(IDocument doc, DocumentCommand2 cmd) { assertTrue(cmd.text.length() == 1); int offset = cmd.offset; int lineStart = TextSourceUtils.findLineStartForOffset(docContents, offset); String beforeCursor = docContents.substring(lineStart, offset); if(!beforeCursor.trim().isEmpty()) { return; } if(!isCloseSymbol(cmd.text)) { return; } char closeBrace = cmd.text.charAt(0); BlockHeuristicsScannner bhScanner = createBlockHeuristicsScanner(doc); int blockStartOffset = bhScanner.findBlockStart(offset, closeBrace); // Replace current indent cmd.offset = lineStart; cmd.length = beforeCursor.length(); // With indent of block-start line String startLineIndent = getLineIndentForOffset(blockStartOffset); cmd.text = startLineIndent + closeBrace; } protected boolean isCloseSymbol(String string) { for (BlockTokenRule blockTokenRule : getBlockRules()) { if(string.charAt(0) == blockTokenRule.close) { return true; } } return false; } }