/** * 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.editor.jscomment.formatting; import org.eclipse.jface.preference.IPreferenceStore; import org.eclipse.jface.text.BadLocationException; import org.eclipse.jface.text.DocumentCommand; import org.eclipse.jface.text.IDocument; import org.eclipse.jface.text.IRegion; import org.eclipse.jface.text.ITypedRegion; import org.eclipse.jface.text.Region; import org.eclipse.jface.text.TextUtilities; import org.eclipse.jface.text.source.ISourceViewer; import org.eclipse.jface.text.source.SourceViewerConfiguration; import com.aptana.ide.editor.js.JSPlugin; import com.aptana.ide.editor.js.preferences.IPreferenceConstants; import com.aptana.ide.editor.jscomment.JSCommentFileLanguageService; import com.aptana.ide.editor.jscomment.parsing.JSCommentMimeType; import com.aptana.ide.editors.unified.EditorFileContext; import com.aptana.ide.editors.unified.UnifiedAutoIndentStrategy; import com.aptana.ide.editors.unified.UnifiedConfiguration; import com.aptana.ide.lexer.LexemeList; /** * */ public class JSCommentAutoIndentStrategy extends UnifiedAutoIndentStrategy { /** * linePrefix */ protected String linePrefix = "* "; // " "; // //$NON-NLS-1$ /** * @param context * @param configuration * @param sourceViewer */ public JSCommentAutoIndentStrategy(EditorFileContext context, SourceViewerConfiguration configuration, ISourceViewer sourceViewer) { super(context, configuration, 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(); } } } } /** * indentCloseToken * * @param doc * @param c * @param offset * @param lineOffset * @param firstNonWS * @return boolean */ protected boolean indentCloseToken(IDocument doc, DocumentCommand c, int offset, int lineOffset, int firstNonWS) { boolean isClose = false; if (doc.getLength() < 2 || offset < 2) { isClose = true; } else { try { if (doc.getChar(offset - 1) == '/' && doc.getChar(offset - 2) == '*') { isClose = true; } } catch (BadLocationException e) { } } if (isClose) { String append = getIndentationString(doc, lineOffset, firstNonWS); // multiline comments indent with "space *" after the first, so trim that if it is there if (append.endsWith(" ")) //$NON-NLS-1$ { append = append.substring(0, append.length() - 1); } c.text += append; return true; } return false; } /** * Copies the indentation of the previous line and adds a star. If the javadoc just started on * this line add standard method tags and close the javadoc. * * @param d * the document to work on * @param c * the command to deal with */ protected void indentAfterNewLine(IDocument d, DocumentCommand c) { int offset = c.offset; if (offset == -1 || d.getLength() == 0) { return; } try { int p = (offset == d.getLength() ? offset - 1 : offset); IRegion line = d.getLineInformationOfOffset(p); int lineOffset = line.getOffset(); int firstNonWS = findEndOfWhiteSpace(d, lineOffset, offset); // find out if this is a return after a */ (in which case only add an indent, not a *) if (indentCloseToken(d, c, offset, lineOffset, firstNonWS)) { return; } // find out if this is a // style single line comment if ((d.getLength() > firstNonWS + 1) && (d.getChar(firstNonWS + 1) == '/')) { super.indentAfterNewLine(d, c); return; } // get line prefix IPreferenceStore store = JSPlugin.getDefault().getPreferenceStore(); boolean useStar = true; if (store != null) { useStar = store.getBoolean(IPreferenceConstants.PREFERENCE_COMMENT_INDENT_USE_STAR); } linePrefix = useStar ? "* " : " "; //$NON-NLS-1$ //$NON-NLS-2$ StringBuffer buf = new StringBuffer(c.text); IRegion prefix = findPrefixRange(d, line); String indentation = d.get(prefix.getOffset(), prefix.getLength()); // String indentation = getIndentationString(d, lineOffset, firstNonWS); // if(indentation == "") // return; int lengthToAdd = Math.min(offset - prefix.getOffset(), prefix.getLength()); buf.append(indentation.substring(0, lengthToAdd)); if (firstNonWS < offset) { if (d.getChar(firstNonWS) == '/') { // javadoc started on this line buf.append(" " + linePrefix); //$NON-NLS-1$ if (isNewComment(d, offset)) { c.shiftsCaret = false; c.caretOffset = c.offset + buf.length(); String lineDelimiter = TextUtilities.getDefaultLineDelimiter(d); String endTag = lineDelimiter + indentation + " */"; //$NON-NLS-1$ // guard for end of doc (multiline comment at very end of doc if (d.getLength() > firstNonWS + 2 && d.getChar(firstNonWS + 1) == '*') { // we need to close the comment d.replace(offset, 0, endTag); } else { buf.append(endTag); } } } } // move the caret behind the prefix, even if we do not have to insert it. if (lengthToAdd < prefix.getLength()) { c.caretOffset = offset + prefix.getLength() - lengthToAdd; } c.text = buf.toString(); } catch (BadLocationException excp) { // stop work } } /** * Guesses if the command operates within a newly created javadoc comment or not. If in doubt, * it will assume that the javadoc is new. * * @param document * the document * @param commandOffset * the command offset * @return <code>true</code> if the comment should be closed, <code>false</code> if not */ protected boolean isNewComment(IDocument document, int commandOffset) { try { // Lexeme lx = lexemeList.get( lexemeList.getLexemeCeilingIndex(commandOffset) ); // if( lx.getLanguage().equals(ScriptDocMimeType.MimeType) ) // return false; int lineIndex = document.getLineOfOffset(commandOffset) + 1; if (lineIndex >= document.getNumberOfLines()) { return true; } IRegion line = document.getLineInformation(lineIndex); ITypedRegion partition = TextUtilities.getPartition(document, UnifiedConfiguration.UNIFIED_PARTITIONING, commandOffset, false); int partitionEnd = partition.getOffset() + partition.getLength() - 1; // partitions // have overlaps // in eclipse if (line.getOffset() >= partitionEnd) { return true; } String comment = document.get(partition.getOffset(), partition.getLength()); // comments that don't end with */ are certainly not closed if (!comment.endsWith("*/")) //$NON-NLS-1$ { return true; } int firstNewline = comment.indexOf('\n'); // assume short comment always unclosed and guard for next test if (comment.length() < 4) { return true; } if (comment.startsWith("/**/")) //$NON-NLS-1$ { return false; } // comments that have * as the first non ws char on next line are probably closed String subComment = comment.substring(firstNewline).trim(); if (subComment.startsWith("*")) //$NON-NLS-1$ { return false; } // no extra lines means probably not closed (can be a */ line due to previous test) if (subComment.indexOf("\n") == -1) //$NON-NLS-1$ { return true; } // look on the first non ws line for trigger words - // this can't be done by counting lexemes instead of text, nor making sure function is a // keyword // allowing var x.y.z = function syntax String firstLine = subComment.substring(0, subComment.indexOf("\n")); //$NON-NLS-1$ if (firstLine.indexOf("=") > -1) //$NON-NLS-1$ { return true; } if (firstLine.indexOf("function") > -1) //$NON-NLS-1$ { return true; } if (firstLine.indexOf("var") > -1) //$NON-NLS-1$ { return true; } if (comment.indexOf("/*", 2) != -1) //$NON-NLS-1$ { return true; // enclosed another comment -> probably a new comment } return false; } catch (BadLocationException e) { return false; } } /** * Unindents a typed slash ('/') if it forms the end of a comment. * * @param d * the document * @param c * the command */ protected void indentAfterCommentEnd(IDocument d, DocumentCommand c) { if (c.offset < 2 || d.getLength() == 0) { return; } try { if ("* ".equals(d.get(c.offset - 2, 2))) { //$NON-NLS-1$ // modify document command c.length++; c.offset--; } } catch (BadLocationException excp) { // stop work } } /** * Returns the range of the multiline comment prefix on the given line in <code>document</code>. * The prefix greedily matches the following regex pattern: <code>\w*\*\w*</code>, that is, * any number of whitespace characters, followed by an asterix ('*'), followed by any number of * whitespace characters. * * @param document * the document to which <code>line</code> refers * @param line * the line from which to extract the prefix range * @return an <code>IRegion</code> describing the range of the prefix on the given line * @throws BadLocationException * if accessing the document fails */ protected IRegion findPrefixRange(IDocument document, IRegion line) throws BadLocationException { int lineOffset = line.getOffset(); int lineEnd = lineOffset + line.getLength(); int indentEnd = findEndOfWhiteSpace(document, lineOffset, lineEnd); if (indentEnd < lineEnd && document.get(indentEnd, linePrefix.length()).equals(linePrefix)) { indentEnd++; while (indentEnd < lineEnd && document.getChar(indentEnd) == ' ') { indentEnd++; } } return new Region(lineOffset, indentEnd - lineOffset); } /** * @see UnifiedAutoIndentStrategy#getPreferenceStore() */ public IPreferenceStore getPreferenceStore() { return JSPlugin.getDefault().getPreferenceStore(); } /** * @see com.aptana.ide.editors.unified.UnifiedAutoIndentStrategy#getLexemeList() */ protected LexemeList getLexemeList() { JSCommentFileLanguageService ls = (JSCommentFileLanguageService) context .getLanguageService(JSCommentMimeType.MimeType); return ls.getFileContext().getLexemeList(); } }