001: /*******************************************************************************
002: * Copyright (c) 2000, 2007 IBM Corporation and others.
003: * All rights reserved. This program and the accompanying materials
004: * are made available under the terms of the Eclipse Public License v1.0
005: * which accompanies this distribution, and is available at
006: * http://www.eclipse.org/legal/epl-v10.html
007: *
008: * Contributors:
009: * IBM Corporation - initial API and implementation
010: *******************************************************************************/package org.eclipse.jdt.internal.ui.text.javadoc;
011:
012: import org.eclipse.core.runtime.Assert;
013: import org.eclipse.core.runtime.CoreException;
014:
015: import org.eclipse.jface.text.BadLocationException;
016: import org.eclipse.jface.text.DefaultIndentLineAutoEditStrategy;
017: import org.eclipse.jface.text.DocumentCommand;
018: import org.eclipse.jface.text.IDocument;
019: import org.eclipse.jface.text.IRegion;
020: import org.eclipse.jface.text.ITypedRegion;
021: import org.eclipse.jface.text.Region;
022: import org.eclipse.jface.text.TextUtilities;
023:
024: import org.eclipse.ui.IEditorPart;
025: import org.eclipse.ui.IWorkbenchPage;
026: import org.eclipse.ui.IWorkbenchWindow;
027: import org.eclipse.ui.PlatformUI;
028: import org.eclipse.ui.texteditor.ITextEditorExtension3;
029:
030: import org.eclipse.jdt.core.ICompilationUnit;
031: import org.eclipse.jdt.core.IJavaElement;
032: import org.eclipse.jdt.core.IJavaProject;
033: import org.eclipse.jdt.core.IMember;
034: import org.eclipse.jdt.core.IMethod;
035: import org.eclipse.jdt.core.ISourceRange;
036: import org.eclipse.jdt.core.IType;
037: import org.eclipse.jdt.core.JavaModelException;
038:
039: import org.eclipse.jdt.internal.corext.codemanipulation.StubUtility;
040: import org.eclipse.jdt.internal.corext.util.JavaModelUtil;
041: import org.eclipse.jdt.internal.corext.util.MethodOverrideTester;
042: import org.eclipse.jdt.internal.corext.util.Strings;
043: import org.eclipse.jdt.internal.corext.util.SuperTypeHierarchyCache;
044:
045: import org.eclipse.jdt.ui.CodeGeneration;
046: import org.eclipse.jdt.ui.IWorkingCopyManager;
047: import org.eclipse.jdt.ui.PreferenceConstants;
048:
049: import org.eclipse.jdt.internal.ui.JavaPlugin;
050:
051: /**
052: * Auto indent strategy for Javadoc comments.
053: */
054: public class JavaDocAutoIndentStrategy extends
055: DefaultIndentLineAutoEditStrategy {
056:
057: /** The partitioning that this strategy operates on. */
058: private final String fPartitioning;
059:
060: /**
061: * Creates a new Javadoc auto indent strategy for the given document partitioning.
062: *
063: * @param partitioning the document partitioning
064: */
065: public JavaDocAutoIndentStrategy(String partitioning) {
066: fPartitioning = partitioning;
067: }
068:
069: /**
070: * Copies the indentation of the previous line and adds a star.
071: * If the Javadoc just started on this line add standard method tags
072: * and close the Javadoc.
073: *
074: * @param d the document to work on
075: * @param c the command to deal with
076: */
077: private void indentAfterNewLine(IDocument d, DocumentCommand c) {
078:
079: int offset = c.offset;
080: if (offset == -1 || d.getLength() == 0)
081: return;
082:
083: try {
084: int p = (offset == d.getLength() ? offset - 1 : offset);
085: IRegion line = d.getLineInformationOfOffset(p);
086:
087: int lineOffset = line.getOffset();
088: int firstNonWS = findEndOfWhiteSpace(d, lineOffset, offset);
089: Assert.isTrue(firstNonWS >= lineOffset,
090: "indentation must not be negative"); //$NON-NLS-1$
091:
092: StringBuffer buf = new StringBuffer(c.text);
093: IRegion prefix = findPrefixRange(d, line);
094: String indentation = d.get(prefix.getOffset(), prefix
095: .getLength());
096: int lengthToAdd = Math.min(offset - prefix.getOffset(),
097: prefix.getLength());
098:
099: buf.append(indentation.substring(0, lengthToAdd));
100:
101: if (firstNonWS < offset) {
102: if (d.getChar(firstNonWS) == '/') {
103: // Javadoc started on this line
104: buf.append(" * "); //$NON-NLS-1$
105:
106: if (isPreferenceTrue(PreferenceConstants.EDITOR_CLOSE_JAVADOCS)
107: && isNewComment(d, offset)) {
108: c.shiftsCaret = false;
109: c.caretOffset = c.offset + buf.length();
110: String lineDelimiter = TextUtilities
111: .getDefaultLineDelimiter(d);
112:
113: int eolOffset = lineOffset + line.getLength();
114: int replacementLength = eolOffset - p;
115: String restOfLine = d.get(p, replacementLength);
116: String endTag = lineDelimiter + indentation
117: + " */"; //$NON-NLS-1$
118:
119: if (isPreferenceTrue(PreferenceConstants.EDITOR_ADD_JAVADOC_TAGS)) {
120: // we need to close the comment before computing
121: // the correct tags in order to get the method
122: d
123: .replace(offset, replacementLength,
124: endTag);
125:
126: // evaluate method signature
127: ICompilationUnit unit = getCompilationUnit();
128:
129: if (unit != null) {
130: try {
131: JavaModelUtil.reconcile(unit);
132: String string = createJavaDocTags(
133: d, c, indentation,
134: lineDelimiter, unit);
135: buf.append(restOfLine);
136: // only add tags if they are non-empty - the empty line has already been added above.
137: if (string != null
138: && !string.trim().equals(
139: "*")) //$NON-NLS-1$
140: buf.append(string);
141: } catch (CoreException e) {
142: // ignore
143: }
144: }
145: } else {
146: c.length = replacementLength;
147: buf.append(restOfLine);
148: buf.append(endTag);
149: }
150: }
151:
152: }
153: }
154:
155: // move the caret behind the prefix, even if we do not have to insert it.
156: if (lengthToAdd < prefix.getLength())
157: c.caretOffset = offset + prefix.getLength()
158: - lengthToAdd;
159: c.text = buf.toString();
160:
161: } catch (BadLocationException excp) {
162: // stop work
163: }
164: }
165:
166: /**
167: * Returns the value of the given boolean-typed preference.
168: *
169: * @param preference the preference to look up
170: * @return the value of the given preference in the Java plug-in's default preference store
171: */
172: private boolean isPreferenceTrue(String preference) {
173: return JavaPlugin.getDefault().getPreferenceStore().getBoolean(
174: preference);
175: }
176:
177: /**
178: * Returns the range of the Javadoc prefix on the given line in
179: * <code>document</code>. The prefix greedily matches the following regex
180: * pattern: <code>\w*\*\w*</code>, that is, any number of whitespace
181: * characters, followed by an asterisk ('*'), followed by any number of
182: * whitespace characters.
183: *
184: * @param document the document to which <code>line</code> refers
185: * @param line the line from which to extract the prefix range
186: * @return an <code>IRegion</code> describing the range of the prefix on the given line
187: * @throws BadLocationException if accessing the document fails
188: */
189: private IRegion findPrefixRange(IDocument document, IRegion line)
190: throws BadLocationException {
191: int lineOffset = line.getOffset();
192: int lineEnd = lineOffset + line.getLength();
193: int indentEnd = findEndOfWhiteSpace(document, lineOffset,
194: lineEnd);
195: if (indentEnd < lineEnd && document.getChar(indentEnd) == '*') {
196: indentEnd++;
197: while (indentEnd < lineEnd
198: && document.getChar(indentEnd) == ' ')
199: indentEnd++;
200: }
201: return new Region(lineOffset, indentEnd - lineOffset);
202: }
203:
204: /**
205: * Creates the Javadoc tags for newly inserted comments.
206: *
207: * @param document the document
208: * @param command the command
209: * @param indentation the base indentation to use
210: * @param lineDelimiter the line delimiter to use
211: * @param unit the compilation unit shown in the editor
212: * @return the tags to add to the document
213: * @throws CoreException if accessing the Java model fails
214: * @throws BadLocationException if accessing the document fails
215: */
216: private String createJavaDocTags(IDocument document,
217: DocumentCommand command, String indentation,
218: String lineDelimiter, ICompilationUnit unit)
219: throws CoreException, BadLocationException {
220: IJavaElement element = unit.getElementAt(command.offset);
221: if (element == null)
222: return null;
223:
224: switch (element.getElementType()) {
225: case IJavaElement.TYPE:
226: return createTypeTags(document, command, indentation,
227: lineDelimiter, (IType) element);
228:
229: case IJavaElement.METHOD:
230: return createMethodTags(document, command, indentation,
231: lineDelimiter, (IMethod) element);
232:
233: default:
234: return null;
235: }
236: }
237:
238: /**
239: * Removes start and end of a comment and corrects indentation and line
240: * delimiters.
241: *
242: * @param comment the computed comment
243: * @param indentation the base indentation
244: * @param project the Java project for the formatter settings, or <code>null</code> for global preferences
245: * @param lineDelimiter the line delimiter
246: * @return a trimmed version of <code>comment</code>
247: */
248: private String prepareTemplateComment(String comment,
249: String indentation, IJavaProject project,
250: String lineDelimiter) {
251: // trim comment start and end if any
252: if (comment.endsWith("*/")) //$NON-NLS-1$
253: comment = comment.substring(0, comment.length() - 2);
254: comment = comment.trim();
255: if (comment.startsWith("/*")) { //$NON-NLS-1$
256: if (comment.length() > 2 && comment.charAt(2) == '*') {
257: comment = comment.substring(3); // remove '/**'
258: } else {
259: comment = comment.substring(2); // remove '/*'
260: }
261: }
262: // trim leading spaces, but not new lines
263: int nonSpace = 0;
264: int len = comment.length();
265: while (nonSpace < len
266: && Character.getType(comment.charAt(nonSpace)) == Character.SPACE_SEPARATOR)
267: nonSpace++;
268: comment = comment.substring(nonSpace);
269:
270: return Strings.changeIndent(comment, 0, project, indentation,
271: lineDelimiter);
272: }
273:
274: private String createTypeTags(IDocument document,
275: DocumentCommand command, String indentation,
276: String lineDelimiter, IType type) throws CoreException,
277: BadLocationException {
278: String[] typeParamNames = StubUtility
279: .getTypeParameterNames(type.getTypeParameters());
280: String comment = CodeGeneration.getTypeComment(type
281: .getCompilationUnit(), type.getTypeQualifiedName('.'),
282: typeParamNames, lineDelimiter);
283: if (comment != null) {
284: boolean javadocComment = comment.startsWith("/**"); //$NON-NLS-1$
285: if (!isFirstComment(document, command, type, javadocComment))
286: return null;
287: return prepareTemplateComment(comment.trim(), indentation,
288: type.getJavaProject(), lineDelimiter);
289: }
290: return null;
291: }
292:
293: private String createMethodTags(IDocument document,
294: DocumentCommand command, String indentation,
295: String lineDelimiter, IMethod method) throws CoreException,
296: BadLocationException {
297: IRegion partition = TextUtilities.getPartition(document,
298: fPartitioning, command.offset, false);
299: IMethod inheritedMethod = getInheritedMethod(method);
300: String comment = CodeGeneration.getMethodComment(method,
301: inheritedMethod, lineDelimiter);
302: if (comment != null) {
303: comment = comment.trim();
304: boolean javadocComment = comment.startsWith("/**"); //$NON-NLS-1$
305: if (!isFirstComment(document, command, method,
306: javadocComment))
307: return null;
308: boolean isJavaDoc = partition.getLength() >= 3
309: && document.get(partition.getOffset(), 3).equals(
310: "/**"); //$NON-NLS-1$
311: if (javadocComment == isJavaDoc) {
312: return prepareTemplateComment(comment, indentation,
313: method.getJavaProject(), lineDelimiter);
314: }
315: }
316: return null;
317: }
318:
319: /**
320: * Returns <code>true</code> if the comment being inserted at
321: * <code>command.offset</code> is the first comment (the first
322: * Javadoc comment if <code>ignoreJavadoc</code> is
323: * <code>true</code>) of the given member.
324: * <p>
325: * see also https://bugs.eclipse.org/bugs/show_bug.cgi?id=55325 (don't add parameters if the member already has a comment)
326: * </p>
327: * @param document the document
328: * @param command the document command
329: * @param member the Java member
330: * @param ignoreNonJavadoc <code>true</code> if non Javadoc should be ignored
331: * @return <code>true</code> if it is the first comment
332: * @throws JavaModelException if accessing the Java model fails
333: * @throws BadLocationException if accessing the document fails
334: */
335: private boolean isFirstComment(IDocument document,
336: DocumentCommand command, IMember member,
337: boolean ignoreNonJavadoc) throws BadLocationException,
338: JavaModelException {
339: IRegion partition = TextUtilities.getPartition(document,
340: fPartitioning, command.offset, false);
341: ISourceRange sourceRange = member.getSourceRange();
342: if (sourceRange == null
343: || sourceRange.getOffset() != partition.getOffset())
344: return false;
345: int srcOffset = sourceRange.getOffset();
346: int srcLength = sourceRange.getLength();
347: int nameRelativeOffset = member.getNameRange().getOffset()
348: - srcOffset;
349: int partitionRelativeOffset = partition.getOffset() - srcOffset;
350: String token = ignoreNonJavadoc ? "/**" : "/*"; //$NON-NLS-1$ //$NON-NLS-2$
351: return document.get(srcOffset, srcLength).lastIndexOf(token,
352: nameRelativeOffset) == partitionRelativeOffset;
353: }
354:
355: /**
356: * Unindents a typed slash ('/') if it forms the end of a comment.
357: *
358: * @param d the document
359: * @param c the command
360: */
361: private void indentAfterCommentEnd(IDocument d, DocumentCommand c) {
362: if (c.offset < 2 || d.getLength() == 0) {
363: return;
364: }
365: try {
366: if ("* ".equals(d.get(c.offset - 2, 2))) { //$NON-NLS-1$
367: // modify document command
368: c.length++;
369: c.offset--;
370: }
371: } catch (BadLocationException excp) {
372: // stop work
373: }
374: }
375:
376: /**
377: * Guesses if the command operates within a newly created Javadoc comment or not.
378: * If in doubt, it will assume that the Javadoc is new.
379: *
380: * @param document the document
381: * @param commandOffset the command offset
382: * @return <code>true</code> if the comment should be closed, <code>false</code> if not
383: */
384: private boolean isNewComment(IDocument document, int commandOffset) {
385:
386: try {
387: int lineIndex = document.getLineOfOffset(commandOffset) + 1;
388: if (lineIndex >= document.getNumberOfLines())
389: return true;
390:
391: IRegion line = document.getLineInformation(lineIndex);
392: ITypedRegion partition = TextUtilities.getPartition(
393: document, fPartitioning, commandOffset, false);
394: int partitionEnd = partition.getOffset()
395: + partition.getLength();
396: if (line.getOffset() >= partitionEnd)
397: return false;
398:
399: if (document.getLength() == partitionEnd)
400: return true; // partition goes to end of document - probably a new comment
401:
402: String comment = document.get(partition.getOffset(),
403: partition.getLength());
404: if (comment.indexOf("/*", 2) != -1) //$NON-NLS-1$
405: return true; // enclosed another comment -> probably a new comment
406:
407: return false;
408:
409: } catch (BadLocationException e) {
410: return false;
411: }
412: }
413:
414: private boolean isSmartMode() {
415: IWorkbenchPage page = JavaPlugin.getActivePage();
416: if (page != null) {
417: IEditorPart part = page.getActiveEditor();
418: if (part instanceof ITextEditorExtension3) {
419: ITextEditorExtension3 extension = (ITextEditorExtension3) part;
420: return extension.getInsertMode() == ITextEditorExtension3.SMART_INSERT;
421: }
422: }
423: return false;
424: }
425:
426: /*
427: * @see IAutoIndentStrategy#customizeDocumentCommand
428: */
429: public void customizeDocumentCommand(IDocument document,
430: DocumentCommand command) {
431:
432: if (!isSmartMode())
433: return;
434:
435: if (command.text != null) {
436: if (command.length == 0) {
437: String[] lineDelimiters = document
438: .getLegalLineDelimiters();
439: int index = TextUtilities.endsWith(lineDelimiters,
440: command.text);
441: if (index > -1) {
442: // ends with line delimiter
443: if (lineDelimiters[index].equals(command.text))
444: // just the line delimiter
445: indentAfterNewLine(document, command);
446: return;
447: }
448: }
449:
450: if (command.text.equals("/")) { //$NON-NLS-1$
451: indentAfterCommentEnd(document, command);
452: return;
453: }
454: }
455: }
456:
457: /**
458: * Returns the method inherited from, <code>null</code> if method is newly defined.
459: * @param method the method being written
460: * @return the ancestor method, or <code>null</code> if none
461: * @throws JavaModelException if accessing the Java model fails
462: */
463: private static IMethod getInheritedMethod(IMethod method)
464: throws JavaModelException {
465: IType declaringType = method.getDeclaringType();
466: MethodOverrideTester tester = SuperTypeHierarchyCache
467: .getMethodOverrideTester(declaringType);
468: return tester.findOverriddenMethod(method, true);
469: }
470:
471: /**
472: * Returns the compilation unit of the compilation unit editor invoking the <code>AutoIndentStrategy</code>,
473: * might return <code>null</code> on error.
474: * @return the compilation unit represented by the document
475: */
476: private static ICompilationUnit getCompilationUnit() {
477:
478: IWorkbenchWindow window = PlatformUI.getWorkbench()
479: .getActiveWorkbenchWindow();
480: if (window == null)
481: return null;
482:
483: IWorkbenchPage page = window.getActivePage();
484: if (page == null)
485: return null;
486:
487: IEditorPart editor = page.getActiveEditor();
488: if (editor == null)
489: return null;
490:
491: IWorkingCopyManager manager = JavaPlugin.getDefault()
492: .getWorkingCopyManager();
493: ICompilationUnit unit = manager.getWorkingCopy(editor
494: .getEditorInput());
495: if (unit == null)
496: return null;
497:
498: return unit;
499: }
500:
501: }
|