/** * This file Copyright (c) 2005-2008 Aptana, Inc. This program is * dual-licensed under both the Aptana Public License and the GNU General * Public license. You may elect to use one or the other of these licenses. * * This program is distributed in the hope that it will be useful, but * AS-IS and WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE, TITLE, or * NONINFRINGEMENT. Redistribution, except as permitted by whichever of * the GPL or APL you select, is prohibited. * * 1. For the GPL license (GPL), you can redistribute and/or modify this * program under the terms of the GNU General Public License, * Version 3, as published by the Free Software Foundation. You should * have received a copy of the GNU General Public License, Version 3 along * with this program; if not, write to the Free Software Foundation, Inc., 51 * Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * Aptana provides a special exception to allow redistribution of this file * with certain other free and open source software ("FOSS") code and certain additional terms * pursuant to Section 7 of the GPL. You may view the exception and these * terms on the web at http://www.aptana.com/legal/gpl/. * * 2. For the Aptana Public License (APL), this program and the * accompanying materials are made available under the terms of the APL * v1.0 which accompanies this distribution, and is available at * http://www.aptana.com/legal/apl/. * * You may view the GPL, Aptana's exception and additional terms, and the * APL in the file titled license.html at the root of the corresponding * plugin containing this source file. * * Any modifications to this file must keep this entire header intact. */ package com.aptana.ide.editors.unified; import java.util.Arrays; import org.eclipse.jface.preference.IPreferenceStore; import org.eclipse.jface.text.BadLocationException; import org.eclipse.jface.text.DocumentCommand; import org.eclipse.jface.text.IAutoEditStrategy; 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.jface.text.source.SourceViewer; import org.eclipse.jface.text.source.SourceViewerConfiguration; import com.aptana.ide.core.IdeLog; import com.aptana.ide.core.StringUtils; import com.aptana.ide.editors.UnifiedEditorsPlugin; import com.aptana.ide.editors.formatting.UnifiedBracketInserterBase; import com.aptana.ide.editors.preferences.IPreferenceConstants; import com.aptana.ide.lexer.LexemeList; /** * UnifiedAutoIndentStrategy */ public abstract class UnifiedAutoIndentStrategy implements IAutoEditStrategy, IPreferenceClient { /** * configuration */ protected SourceViewerConfiguration configuration; /** * sourceViewer */ protected ISourceViewer sourceViewer; /** * context */ protected EditorFileContext context; /** * spaces */ private String spaces = " "; //$NON-NLS-1$ /** * Creates a new instance of the JSAutoEditStrategy * * @param context * @param configuration * @param sourceViewer */ public UnifiedAutoIndentStrategy(EditorFileContext context, SourceViewerConfiguration configuration, ISourceViewer sourceViewer) { this.context = context; this.configuration = configuration; this.sourceViewer = sourceViewer; } /** * @see org.eclipse.jface.text.IAutoEditStrategy#customizeDocumentCommand(org.eclipse.jface.text.IDocument, * org.eclipse.jface.text.DocumentCommand) */ public void customizeDocumentCommand(IDocument document, DocumentCommand command) { if (command.text == null || command.length > 0) { return; } String[] lineDelimiters = document.getLegalLineDelimiters(); int index = TextUtilities.endsWith(lineDelimiters, command.text); if (index > -1) { // ends with line delimiter if (lineDelimiters[index].equals(command.text)) { indentAfterNewLine(document, command); } return; } // todo: ensure we actually need this here else if (command.text.equals("\t")) //$NON-NLS-1$ { if (configuration instanceof UnifiedConfiguration) { UnifiedConfiguration uc = (UnifiedConfiguration) configuration; if (uc.useSpacesAsTabs()) { command.text = uc.getTabAsSpaces(); } } } else if (command.text.length() == 1 && isAutoInsertCharacter(command.text.charAt(0)) && isAutoInsertEnabled() && isValidAutoInsertLocation(document, command)) { char current = command.text.charAt(0); if (overwriteBracket(current, document, command, getLexemeList())) { return; } } } /** * * @param c * @return */ private boolean isAutoInsertCharacter(char c) { int val = Arrays.binarySearch(getAutoInsertCharacters(), c); return val >= 0; } /** * getAutoInsertCharacters * * @return char[] */ protected char[] getAutoInsertCharacters() { return new char[] { '(', '<', '[', '"', '\'', '{' }; } /** * getAutoOverwriteCharacters * * @return char[] */ protected char[] getAutoOverwriteCharacters() { return new char[] { ')', '>', ']', '"', '\'', '}' }; } /** * isAutoInsertEnabled * * @return boolean */ protected boolean isAutoInsertEnabled() { IPreferenceStore store = getPreferenceStore(); String abi = com.aptana.ide.editors.preferences.IPreferenceConstants.AUTO_BRACKET_INSERTION; return (store == null || store.getString(abi).equals("NONE") == false); //$NON-NLS-1$ // if (store != null && store.getString(abi).equals("NONE")) // { // return false; // } // else // { // return true; // } } /** * isValidAutoInsertLocation * * @param d * @param c * @return boolean */ protected boolean isValidAutoInsertLocation(IDocument d, DocumentCommand c) { return true; } /** * indentAfterNewLine * * @param d * @param c */ protected void indentAfterNewLine(IDocument d, DocumentCommand c) { String indentString = getIndentString(); // nothing to add if nothing to add if (indentString.equals(StringUtils.EMPTY)) { return; } int offset = c.offset; if (offset == -1 || d.getLength() == 0) { return; } c.text += getIndentationAtOffset(d, offset); return; } /** * overwriteBracket * * @param bracket * @param document * @param command * @param ll * @return boolean */ public boolean overwriteBracket(char bracket, IDocument document, DocumentCommand command, LexemeList ll) { // if next character is "closing" char, overwrite if (canOverwriteBracket(bracket, command.offset, document, ll)) { command.text = StringUtils.EMPTY; command.shiftsCaret = false; command.caretOffset = command.offset + 1; return true; } return false; } /** * canOverwriteBracket * * @param bracket * @param offset * @param document * @param ll * @return boolean */ public boolean canOverwriteBracket(char bracket, int offset, IDocument document, LexemeList ll) { if (offset < document.getLength()) { char[] autoOverwriteChars = getAutoOverwriteCharacters(); Arrays.sort(autoOverwriteChars); if (Arrays.binarySearch(autoOverwriteChars, bracket) < 0) { return false; } // If the next char is a ">", our tag is already closed try { char sibling = document.getChar(offset); return sibling == bracket; } catch(BadLocationException ex) { return false; } } return false; } /** * closeBracket * * @param bracket * @param document * @param command * @return boolean */ public boolean closeBracket(char bracket, IDocument document, DocumentCommand command) { if (!canCloseBracket(bracket, document)) { return false; } else { command.text = Character.toString(bracket) + Character.toString(UnifiedBracketInserterBase.getPeerCharacter(bracket)); command.shiftsCaret = false; command.caretOffset = command.offset + 1; return true; } } /** * canCloseBracket * * @param bracket * @param document * @return boolean */ public boolean canCloseBracket(char bracket, IDocument document) { if (!UnifiedBracketInserterBase.hasPeerCharacter(bracket)) { return false; } char[] autoInsertChars = getAutoInsertCharacters(); Arrays.sort(autoInsertChars); if (Arrays.binarySearch(autoInsertChars, bracket) < 0) { return false; } return !UnifiedBracketInserterBase.isStringBalanced(document.get(), bracket, true); } /** * getIndentationAtOffset * * @param d * @param offset * @return String */ protected String getIndentationAtOffset(IDocument d, int offset) { String indentation = StringUtils.EMPTY; try { int p = (offset == d.getLength() ? offset - 1 : offset); IRegion line = d.getLineInformationOfOffset(p); int lineOffset = line.getOffset(); int firstNonWS = findEndOfWhiteSpace(d, lineOffset, offset); indentation = getIndentationString(d, lineOffset, firstNonWS); } catch (BadLocationException excp) { // stop work } return indentation; } /** * getIndentString * * @return String */ protected String getIndentString() { String[] indents = this.configuration.getIndentPrefixes(this.sourceViewer, this.context.getDefaultLanguage()); boolean hasIndents = !((indents == null) || (indents.length == 0)); String indentString = hasIndents ? indents[0] : "\t"; //$NON-NLS-1$ return indentString; } /** * Calculates the whitespace prefix based on user prefs and the existing line. Eg: if the line prefix is five * spaces, and user pref is tabs of width 4, then the result is "/t ". * * @param d * @param lineOffset * @param firstNonWS * @return Returns the whitespace prefix based on user prefs and the existing line. */ protected String getIndentationString(IDocument d, int lineOffset, int firstNonWS) { String lineIndent = StringUtils.EMPTY; try { lineIndent = d.get(lineOffset, firstNonWS - lineOffset); } catch (BadLocationException e1) { } if (lineIndent.equals(StringUtils.EMPTY)) { return lineIndent; } int indentSize = 0; int tabWidth = this.configuration.getTabWidth(sourceViewer); char[] indentChars = lineIndent.toCharArray(); for (int i = 0; i < indentChars.length; i++) { char e = indentChars[i]; if (e == '\t') { indentSize += tabWidth - (indentSize % tabWidth); } else { indentSize++; } } String indentString = getIndentString(); int indentStringWidth = (indentString.equals("\t")) ? tabWidth : indentString.length(); //$NON-NLS-1$ // return in case tab width is zero if (indentStringWidth == 0) { return StringUtils.EMPTY; } int indentCount = (int) Math.floor(indentSize / indentStringWidth); // assume no dived by zero from above tests String indentation = StringUtils.EMPTY; for (int i = 0; i < indentCount; i++) { indentation += indentString; } // here we might want to allow one tab when there are three spaces on the previous line when tabwdith = 4 // logic is just get the ending from the previous line int extra = indentSize % indentStringWidth; indentation += spaces.substring(0, extra);// lineIndent.substring(lineIndent.length() - extra); return indentation; } /** * Copies the indentation of the previous line. * * @param d * the document to work on * @param c * the command to deal with * @return String */ protected String getIndentForCurrentLine(IDocument d, DocumentCommand c) { if (c.offset == -1 || d.getLength() == 0) { return StringUtils.EMPTY; } try { // find start of line int p = (c.offset == d.getLength() ? c.offset - 1 : c.offset); IRegion info = d.getLineInformationOfOffset(p); int start = info.getOffset(); // find white spaces int end = findEndOfWhiteSpace(d, start, c.offset); StringBuffer buf = new StringBuffer(); if (end > start) { // append to input buf.append(d.get(start, end - start)); } return buf.toString(); } catch (BadLocationException excp) { } return StringUtils.EMPTY; } /** * Hmm, this is copied from eclipses DefaultIndentAutoIndentStategy, but only accounts for spaces or tabs (unlike * the whole insert strings allowing any string). * * @param document * @param offset * @param end * @return end of whitespace * @throws BadLocationException */ protected int findEndOfWhiteSpace(IDocument document, int offset, int end) throws BadLocationException { while (offset < end) { char c = document.getChar(offset); if (c != ' ' && c != '\t') { return offset; } offset++; } return end; } /** * Returns the current preference store * * @return The current preference store, or null if not found */ public abstract IPreferenceStore getPreferenceStore(); /** * Creates a lexeme list * * @return LexemeList */ protected abstract LexemeList getLexemeList(); /** * Handles the case where we just added a brace, and we want to return and indent to the next line * * @param d * Document * @param command * DocumentCommand * @return True if it succeeded */ protected boolean indentAfterOpenBrace(IDocument d, DocumentCommand command) { int offset = command.offset; boolean result = false; if (offset != -1 && d.getLength() != 0) { String indent = getIndentForCurrentLine(d, command); String newline = command.text; String tab = "\t"; //$NON-NLS-1$ if (configuration instanceof UnifiedConfiguration) { UnifiedConfiguration uc = (UnifiedConfiguration) configuration; tab = uc.getIndent(); } try { if (command.offset > 0) { char c = d.getChar(command.offset - 1); if (c == '{') { String startIndent = newline + indent + tab; if (command.offset < d.getLength() && d.getChar(command.offset) == '}') { command.text = startIndent + newline + indent; } else { command.text = startIndent; } command.shiftsCaret = false; command.caretOffset = command.offset + startIndent.length(); result = true; } } } catch (BadLocationException e) { IdeLog.logError(UnifiedEditorsPlugin.getDefault(), StringUtils.format(Messages.UnifiedAutoIndentStrategy_InvalidOffset, offset), e); } } return result; } /** * Forces code assist to appear */ protected void triggerContentAssistPopup() { if (sourceViewer instanceof SourceViewer) { if(!autoTriggerAssist()) { return; } ((SourceViewer) sourceViewer).doOperation(ISourceViewer.CONTENTASSIST_PROPOSALS); } } /** * Forces code assist to appear */ protected void hideContentAssistPopup() { if (sourceViewer instanceof UnifiedViewer) { ((UnifiedViewer) sourceViewer).closeContentAssist(); } } /** * Forces context assist to appear */ protected void triggerContextAssistPopup() { if (sourceViewer instanceof SourceViewer) { if(!autoTriggerAssist()) { return; } ((SourceViewer) sourceViewer).doOperation(ISourceViewer.CONTENTASSIST_CONTEXT_INFORMATION); } } /** * Do we auto-trigger content/context assist? * @return */ protected boolean autoTriggerAssist() { // only auto-pop if the preferences allow it return (getPreferenceStore() != null && getPreferenceStore().getBoolean(IPreferenceConstants.CODE_ASSIST_AUTO_ACTIVATION)); } }