/******************************************************************************* * Copyright (c) 2008, 2010 Symbian Software Systems 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: * Andrew Ferguson (Symbian) - Initial implementation * Anton Leherbauer (Wind River Systems) *******************************************************************************/ package org.eclipse.cdt.ui.text.doctools; import java.io.BufferedReader; import java.io.IOException; import java.io.StringReader; import org.eclipse.core.runtime.CoreException; import org.eclipse.jface.text.BadLocationException; import org.eclipse.jface.text.DocumentCommand; import org.eclipse.jface.text.DocumentRewriteSession; import org.eclipse.jface.text.DocumentRewriteSessionType; import org.eclipse.jface.text.IAutoEditStrategy; import org.eclipse.jface.text.IDocument; import org.eclipse.jface.text.IDocumentExtension4; 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.ui.IEditorPart; import org.eclipse.ui.IWorkbenchPage; import org.eclipse.ui.IWorkbenchWindow; import org.eclipse.ui.PlatformUI; import org.eclipse.cdt.core.dom.ast.ASTVisitor; import org.eclipse.cdt.core.dom.ast.IASTDeclaration; import org.eclipse.cdt.core.dom.ast.IASTNode; import org.eclipse.cdt.core.dom.ast.IASTNodeLocation; import org.eclipse.cdt.core.dom.ast.IASTNodeSelector; import org.eclipse.cdt.core.dom.ast.IASTTranslationUnit; import org.eclipse.cdt.core.model.CModelException; import org.eclipse.cdt.core.model.ITranslationUnit; import org.eclipse.cdt.ui.CUIPlugin; import org.eclipse.cdt.ui.IWorkingCopyManager; import org.eclipse.cdt.ui.text.ICPartitions; /** * This class provides default behaviors for multi-line comment auto-editing. * * This class is intended to be sub-classed. * * @since 5.0 */ public class DefaultMultilineCommentAutoEditStrategy implements IAutoEditStrategy { protected static final String MULTILINE_START = "/*"; //$NON-NLS-1$# protected static final String MULTILINE_MID = " * "; //$NON-NLS-1$ protected static final String MULTILINE_END = "*/"; //$NON-NLS-1$ private static String fgDefaultLineDelim = "\n"; //$NON-NLS-1$ public DefaultMultilineCommentAutoEditStrategy() { } /** * @see org.eclipse.jface.text.IAutoEditStrategy#customizeDocumentCommand(org.eclipse.jface.text.IDocument, org.eclipse.jface.text.DocumentCommand) */ public void customizeDocumentCommand(IDocument doc, DocumentCommand cmd) { fgDefaultLineDelim = TextUtilities.getDefaultLineDelimiter(doc); if (doc instanceof IDocumentExtension4) { boolean forNewLine= cmd.length == 0 && cmd.text != null && endsWithDelimiter(doc, cmd.text); boolean forCommentEnd= "/".equals(cmd.text); //$NON-NLS-1$ if (forNewLine || forCommentEnd) { IDocumentExtension4 ext4= (IDocumentExtension4) doc; DocumentRewriteSession drs= ext4.startRewriteSession(DocumentRewriteSessionType.UNRESTRICTED_SMALL); try { if (forNewLine) { customizeDocumentAfterNewLine(doc, cmd); } else if (forCommentEnd) { customizeDocumentForMultilineCommentEnd(doc, cmd); } } finally { ext4.stopRewriteSession(drs); } } } } /** * This implements a rule that when in a multi-line comment context typing a forward slash with * one white space after the "*" will move eliminate the whitespace. * @param doc * @param command */ protected void customizeDocumentForMultilineCommentEnd(IDocument doc, DocumentCommand command) { if (command.offset < 2 || doc.getLength() == 0) { return; } try { if ("* ".equals(doc.get(command.offset - 2, 2))) { //$NON-NLS-1$ // modify document command command.length++; command.offset--; } } catch (BadLocationException excp) { // stop work } } /** * Copies the indentation of the previous line and adds a star. * If the comment just started on this line adds also a blank. * * @param doc the document to work on * @param c the command to deal with */ public void customizeDocumentAfterNewLine(IDocument doc, final DocumentCommand c) { int offset= c.offset; if (offset == -1 || doc.getLength() == 0) return; String lineDelim = TextUtilities.getDefaultLineDelimiter(doc); final StringBuilder buf= new StringBuilder(c.text); try { // find start of line IRegion line= doc.getLineInformationOfOffset(c.offset); int lineStart= line.getOffset(); int firstNonWS= findEndOfWhiteSpaceAt(doc, lineStart, c.offset); IRegion prefix= findPrefixRange(doc, line); String indentation= doc.get(prefix.getOffset(), prefix.getLength()); int lengthToAdd= Math.min(offset - prefix.getOffset(), prefix.getLength()); buf.append(indentation.substring(0, lengthToAdd)); boolean commentAtStart= firstNonWS < c.offset && doc.getChar(firstNonWS) == '/'; if (commentAtStart) { // comment started on this line buf.append(MULTILINE_MID); } c.shiftsCaret= false; c.caretOffset= c.offset + buf.length(); if (commentAtStart && shouldCloseMultiline(doc, c.offset)) { try { doc.replace(c.offset, 0, indentation+" "+MULTILINE_END); // close the comment in order to parse //$NON-NLS-1$ buf.append(lineDelim); // as we are auto-closing, the comment becomes eligible for auto-doc'ing IASTDeclaration dec= null; IASTTranslationUnit ast= getAST(); if (ast != null) { dec= findFollowingDeclaration(ast, offset); if (dec == null) { IASTNodeSelector ans= ast.getNodeSelector(ast.getFilePath()); IASTNode node= ans.findEnclosingNode(offset, 0); if (node instanceof IASTDeclaration) { dec= (IASTDeclaration) node; } } } if (dec != null) { ITypedRegion partition= TextUtilities.getPartition(doc, ICPartitions.C_PARTITIONING /* this! */, offset, false); StringBuilder content= customizeAfterNewLineForDeclaration(doc, dec, partition); buf.append(indent(content, indentation + MULTILINE_MID, lineDelim)); } } catch(BadLocationException ble) { ble.printStackTrace(); } } c.text= buf.toString(); } catch (BadLocationException excp) { // stop work } } protected StringBuilder customizeAfterNewLineForDeclaration(IDocument doc, IASTDeclaration dec, ITypedRegion region) { return new StringBuilder(); } /* * Utilities */ /** * Locates the {@link IASTDeclaration} most immediately following the specified offset * @param unit the translation unit, or null (in which case the result will also be null) * @param offset the offset to begin the search from * @return the {@link IASTDeclaration} most immediately following the specified offset, or null if there * is no {@link IASTDeclaration} */ public static IASTDeclaration findFollowingDeclaration(IASTTranslationUnit unit, final int offset) { final IASTDeclaration[] dec= new IASTDeclaration[1]; final ASTVisitor av= new ASTVisitor() { { shouldVisitTranslationUnit= true; shouldVisitDeclarations= true; } /** * Holds the */ IASTDeclaration stopWhenLeaving; @Override public int visit(IASTDeclaration declaration) { IASTNodeLocation loc= declaration.getFileLocation(); if (loc != null) { int candidateOffset= loc.getNodeOffset(); int candidateEndOffset= candidateOffset+loc.getNodeLength(); if (offset <= candidateOffset) { dec[0]= declaration; return PROCESS_ABORT; } boolean candidateEnclosesOffset= (offset >= candidateOffset) && (offset < candidateEndOffset); if (candidateEnclosesOffset) { stopWhenLeaving= declaration; } } return PROCESS_CONTINUE; } @Override public int leave(IASTDeclaration declaration) { if (declaration == stopWhenLeaving) return PROCESS_ABORT; return PROCESS_CONTINUE; } }; if (unit != null) { unit.accept(av); } return dec[0]; } /** * @return the AST unit for the active editor, or <code>null</code> if there is no active editor, or * the AST could not be obtained. */ public IASTTranslationUnit getAST() { final ITranslationUnit unit= getTranslationUnit(); try { if (unit != null) { IASTTranslationUnit ast= unit.getAST(null, ITranslationUnit.AST_SKIP_ALL_HEADERS); return ast; } } catch (CModelException e) { CUIPlugin.log(e); } catch (CoreException e) { CUIPlugin.log(e); } return null; } /** * Assuming the offset is within a multi-line comment, returns a guess as to * whether the enclosing multi-line comment is a new comment. The result is undefined if * the offset does not occur within a multi-line comment. * * @param document the document * @param offset the offset * @return <code>true</code> if the comment should be closed, <code>false</code> if not */ /* * Adapted from JDT */ public boolean shouldCloseMultiline(IDocument document, int offset) { try { IRegion line= document.getLineInformationOfOffset(offset); ITypedRegion partition= TextUtilities.getPartition(document, ICPartitions.C_PARTITIONING, offset, false); int partitionEnd= partition.getOffset() + partition.getLength(); if (line.getOffset() >= partitionEnd) return false; String comment= document.get(partition.getOffset(), partition.getLength()); if (comment.indexOf(MULTILINE_START, offset - partition.getOffset()) != -1) return true; // enclosed another comment -> probably a new comment if (document.getLength() == partitionEnd) { return !comment.endsWith(MULTILINE_END); } return false; } catch (BadLocationException e) { return false; } } /** * @return the ITranslationUnit for the active editor, or null if no active * editor could be found. */ /* * Cloned from JDT */ protected static ITranslationUnit getTranslationUnit() { IWorkbenchWindow window= PlatformUI.getWorkbench().getActiveWorkbenchWindow(); if (window == null) return null; IWorkbenchPage page= window.getActivePage(); if (page == null) return null; IEditorPart editor= page.getActiveEditor(); if (editor == null) return null; IWorkingCopyManager manager= CUIPlugin.getDefault().getWorkingCopyManager(); ITranslationUnit unit= manager.getWorkingCopy(editor.getEditorInput()); if (unit == null) return null; return unit; } /** * Returns a new buffer with the specified indent string inserted at the beginning * of each line in the specified input buffer * @param buffer * @param indent * @param lineDelim * @since 5.3 */ protected static final StringBuilder indent(StringBuilder buffer, String indent, String lineDelim) { StringBuilder result= new StringBuilder(); BufferedReader br= new BufferedReader(new StringReader(buffer.toString())); try { for (String line= br.readLine(); line != null; line= br.readLine()) { result.append(indent).append(line).append(lineDelim); } } catch(IOException ioe) { throw new AssertionError(); // we can't get IO errors from a string backed reader } return result; } /** * Returns a new buffer with the specified indent string inserted at the beginning * of each line in the specified input buffer * @param buffer * @param indent * * @deprecated Use {{@link #indent(StringBuilder, String, String)} instead. */ @Deprecated protected static final StringBuilder indent(StringBuilder buffer, String indent) { return indent(buffer, indent, fgDefaultLineDelim); } /** * Returns the offset of the first non-whitespace character in the specified document, searching * right/downward from the specified start offset up to the specified end offset. If there is * no non-whitespace then the end offset is returned. * @param document * @param offset * @param end * @throws BadLocationException */ protected static int findEndOfWhiteSpaceAt(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 range of the comment prefix on the given line in * <code>document</code>. The prefix greedily matches the following regex * pattern: <code>\s*\*\S*\s*</code>, that is, any number of whitespace * characters, followed by an asterisk ('*'), followed by any number of * non-whitespace characters, 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 static IRegion findPrefixRange(IDocument document, IRegion line) throws BadLocationException { int lineOffset= line.getOffset(); int lineEnd= lineOffset + line.getLength(); int indentEnd= findEndOfWhiteSpaceAt(document, lineOffset, lineEnd); if (indentEnd < lineEnd && document.getChar(indentEnd) == '*') { indentEnd++; while (indentEnd < lineEnd && !isWhitespace(document.getChar(indentEnd))) indentEnd++; while (indentEnd < lineEnd && isWhitespace(document.getChar(indentEnd))) indentEnd++; } return new Region(lineOffset, indentEnd - lineOffset); } private static boolean isWhitespace(char ch) { return ch == ' ' || ch == '\t'; } /** * Returns whether the text ends with one of the specified IDocument object's * legal line delimiters. */ protected static boolean endsWithDelimiter(IDocument d, String txt) { String[] delimiters= d.getLegalLineDelimiters(); for (int i= 0; i < delimiters.length; i++) { if (txt.endsWith(delimiters[i])) return true; } return false; } }