]>
Commit | Line | Data |
---|---|---|
1b2798f6 EK |
1 | /******************************************************************************* |
2 | * Copyright (c) 2000, 2011 IBM Corporation and others. | |
3 | * All rights reserved. This program and the accompanying materials | |
4 | * are made available under the terms of the Eclipse Public License v1.0 | |
5 | * which accompanies this distribution, and is available at | |
6 | * http://www.eclipse.org/legal/epl-v10.html | |
7 | * | |
8 | * Contributors: | |
9 | * IBM Corporation - initial API and implementation | |
10 | *******************************************************************************/ | |
11 | package org.eclipse.jdt.internal.ui.text.javadoc; | |
12 | ||
13 | import org.eclipse.core.runtime.Assert; | |
14 | import org.eclipse.core.runtime.CoreException; | |
15 | ||
16 | import org.eclipse.jface.text.BadLocationException; | |
17 | import org.eclipse.jface.text.DefaultIndentLineAutoEditStrategy; | |
18 | import org.eclipse.jface.text.DocumentCommand; | |
19 | import org.eclipse.jface.text.IDocument; | |
20 | import org.eclipse.jface.text.IRegion; | |
21 | import org.eclipse.jface.text.ITypedRegion; | |
22 | import org.eclipse.jface.text.Region; | |
23 | import org.eclipse.jface.text.TextUtilities; | |
24 | ||
25 | import org.eclipse.ui.IEditorPart; | |
26 | import org.eclipse.ui.IWorkbenchPage; | |
27 | import org.eclipse.ui.IWorkbenchWindow; | |
28 | import org.eclipse.ui.PlatformUI; | |
29 | ||
30 | import org.eclipse.ui.texteditor.ITextEditorExtension3; | |
31 | ||
32 | import org.eclipse.jdt.core.ICompilationUnit; | |
33 | import org.eclipse.jdt.core.IJavaElement; | |
34 | import org.eclipse.jdt.core.IJavaProject; | |
35 | import org.eclipse.jdt.core.IMember; | |
36 | import org.eclipse.jdt.core.IMethod; | |
37 | import org.eclipse.jdt.core.ISourceRange; | |
38 | import org.eclipse.jdt.core.IType; | |
39 | import org.eclipse.jdt.core.JavaModelException; | |
40 | ||
41 | import org.eclipse.jdt.internal.corext.codemanipulation.StubUtility; | |
42 | import org.eclipse.jdt.internal.corext.util.JavaModelUtil; | |
43 | import org.eclipse.jdt.internal.corext.util.MethodOverrideTester; | |
44 | import org.eclipse.jdt.internal.corext.util.Strings; | |
45 | import org.eclipse.jdt.internal.corext.util.SuperTypeHierarchyCache; | |
46 | ||
47 | import org.eclipse.jdt.ui.CodeGeneration; | |
48 | import org.eclipse.jdt.ui.IWorkingCopyManager; | |
49 | import org.eclipse.jdt.ui.PreferenceConstants; | |
50 | ||
51 | import org.eclipse.jdt.internal.ui.JavaPlugin; | |
52 | import org.eclipse.jdt.internal.ui.javaeditor.EditorUtility; | |
53 | ||
54 | ||
55 | /** | |
56 | * Auto indent strategy for Javadoc comments. | |
57 | */ | |
58 | public class JavaDocAutoIndentStrategy extends DefaultIndentLineAutoEditStrategy { | |
59 | ||
60 | /** The partitioning that this strategy operates on. */ | |
61 | private final String fPartitioning; | |
62 | ||
63 | /** | |
64 | * Creates a new Javadoc auto indent strategy for the given document partitioning. | |
65 | * | |
66 | * @param partitioning the document partitioning | |
67 | */ | |
68 | public JavaDocAutoIndentStrategy(String partitioning) { | |
69 | fPartitioning= partitioning; | |
70 | } | |
71 | ||
72 | /** | |
73 | * Copies the indentation of the previous line and adds a star. | |
74 | * If the Javadoc just started on this line add standard method tags | |
75 | * and close the Javadoc. | |
76 | * | |
77 | * @param d the document to work on | |
78 | * @param c the command to deal with | |
79 | */ | |
80 | private void indentAfterNewLine(IDocument d, DocumentCommand c) { | |
81 | ||
82 | int offset= c.offset; | |
83 | if (offset == -1 || d.getLength() == 0) | |
84 | return; | |
85 | ||
86 | try { | |
87 | int p= (offset == d.getLength() ? offset - 1 : offset); | |
88 | IRegion line= d.getLineInformationOfOffset(p); | |
89 | ||
90 | int lineOffset= line.getOffset(); | |
91 | int firstNonWS= findEndOfWhiteSpace(d, lineOffset, offset); | |
92 | Assert.isTrue(firstNonWS >= lineOffset, "indentation must not be negative"); //$NON-NLS-1$ | |
93 | ||
94 | StringBuffer buf= new StringBuffer(c.text); | |
95 | IRegion prefix= findPrefixRange(d, line); | |
96 | String indentation= d.get(prefix.getOffset(), prefix.getLength()); | |
97 | int lengthToAdd= Math.min(offset - prefix.getOffset(), prefix.getLength()); | |
98 | ||
99 | 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) && isNewComment(d, offset)) { | |
107 | c.shiftsCaret= false; | |
108 | c.caretOffset= c.offset + buf.length(); | |
109 | String lineDelimiter= TextUtilities.getDefaultLineDelimiter(d); | |
110 | ||
111 | int eolOffset= lineOffset + line.getLength(); | |
112 | int replacementLength= eolOffset - p; | |
113 | String restOfLine= d.get(p, replacementLength); | |
114 | String endTag= lineDelimiter + indentation + " */"; //$NON-NLS-1$ | |
115 | ||
116 | if (isPreferenceTrue(PreferenceConstants.EDITOR_ADD_JAVADOC_TAGS)) { | |
117 | // we need to close the comment before computing | |
118 | // the correct tags in order to get the method | |
119 | d.replace(offset, replacementLength, endTag); | |
120 | ||
121 | // evaluate method signature | |
122 | ICompilationUnit unit= getCompilationUnit(); | |
123 | ||
124 | if (unit != null) { | |
125 | try { | |
126 | JavaModelUtil.reconcile(unit); | |
127 | String string= createJavaDocTags(d, c, indentation, lineDelimiter, unit); | |
128 | buf.append(restOfLine); | |
129 | // only add tags if they are non-empty - the empty line has already been added above. | |
130 | if (string != null && !string.trim().equals("*")) //$NON-NLS-1$ | |
131 | buf.append(string); | |
132 | } catch (CoreException e) { | |
133 | // ignore | |
134 | } | |
135 | } | |
136 | } else { | |
137 | c.length= replacementLength; | |
138 | buf.append(restOfLine); | |
139 | buf.append(endTag); | |
140 | } | |
141 | } | |
142 | ||
143 | } | |
144 | } | |
145 | ||
146 | // move the caret behind the prefix, even if we do not have to insert it. | |
147 | if (lengthToAdd < prefix.getLength()) | |
148 | c.caretOffset= offset + prefix.getLength() - lengthToAdd; | |
149 | c.text= buf.toString(); | |
150 | ||
151 | } catch (BadLocationException excp) { | |
152 | // stop work | |
153 | } | |
154 | } | |
155 | ||
156 | /** | |
157 | * Returns the value of the given boolean-typed preference. | |
158 | * | |
159 | * @param preference the preference to look up | |
160 | * @return the value of the given preference in the Java plug-in's default preference store | |
161 | */ | |
162 | private boolean isPreferenceTrue(String preference) { | |
163 | return JavaPlugin.getDefault().getPreferenceStore().getBoolean(preference); | |
164 | } | |
165 | ||
166 | /** | |
167 | * Returns the range of the Javadoc prefix on the given line in | |
168 | * <code>document</code>. The prefix greedily matches the following regex | |
169 | * pattern: <code>\w*\*\w*</code>, that is, any number of whitespace | |
170 | * characters, followed by an asterisk ('*'), followed by any number of | |
171 | * whitespace characters. | |
172 | * | |
173 | * @param document the document to which <code>line</code> refers | |
174 | * @param line the line from which to extract the prefix range | |
175 | * @return an <code>IRegion</code> describing the range of the prefix on the given line | |
176 | * @throws BadLocationException if accessing the document fails | |
177 | */ | |
178 | private IRegion findPrefixRange(IDocument document, IRegion line) throws BadLocationException { | |
179 | int lineOffset= line.getOffset(); | |
180 | int lineEnd= lineOffset + line.getLength(); | |
181 | int indentEnd= findEndOfWhiteSpace(document, lineOffset, lineEnd); | |
182 | if (indentEnd < lineEnd && document.getChar(indentEnd) == '*') { | |
183 | indentEnd++; | |
184 | while (indentEnd < lineEnd && document.getChar(indentEnd) == ' ') | |
185 | indentEnd++; | |
186 | } | |
187 | return new Region(lineOffset, indentEnd - lineOffset); | |
188 | } | |
189 | ||
190 | /** | |
191 | * Creates the Javadoc tags for newly inserted comments. | |
192 | * | |
193 | * @param document the document | |
194 | * @param command the command | |
195 | * @param indentation the base indentation to use | |
196 | * @param lineDelimiter the line delimiter to use | |
197 | * @param unit the compilation unit shown in the editor | |
198 | * @return the tags to add to the document | |
199 | * @throws CoreException if accessing the Java model fails | |
200 | * @throws BadLocationException if accessing the document fails | |
201 | */ | |
202 | private String createJavaDocTags(IDocument document, DocumentCommand command, String indentation, String lineDelimiter, ICompilationUnit unit) | |
203 | throws CoreException, BadLocationException | |
204 | { | |
205 | IJavaElement element= unit.getElementAt(command.offset); | |
206 | if (element == null) | |
207 | return null; | |
208 | ||
209 | switch (element.getElementType()) { | |
210 | case IJavaElement.TYPE: | |
211 | return createTypeTags(document, command, indentation, lineDelimiter, (IType) element); | |
212 | ||
213 | case IJavaElement.METHOD: | |
214 | return createMethodTags(document, command, indentation, lineDelimiter, (IMethod) element); | |
215 | ||
216 | default: | |
217 | return null; | |
218 | } | |
219 | } | |
220 | ||
221 | /** | |
222 | * Removes start and end of a comment and corrects indentation and line | |
223 | * delimiters. | |
224 | * | |
225 | * @param comment the computed comment | |
226 | * @param indentation the base indentation | |
227 | * @param project the Java project for the formatter settings, or <code>null</code> for global preferences | |
228 | * @param lineDelimiter the line delimiter | |
229 | * @return a trimmed version of <code>comment</code> | |
230 | */ | |
231 | private String prepareTemplateComment(String comment, String indentation, IJavaProject project, String lineDelimiter) { | |
232 | // trim comment start and end if any | |
233 | if (comment.endsWith("*/")) //$NON-NLS-1$ | |
234 | comment= comment.substring(0, comment.length() - 2); | |
235 | comment= comment.trim(); | |
236 | if (comment.startsWith("/*")) { //$NON-NLS-1$ | |
237 | if (comment.length() > 2 && comment.charAt(2) == '*') { | |
238 | comment= comment.substring(3); // remove '/**' | |
239 | } else { | |
240 | comment= comment.substring(2); // remove '/*' | |
241 | } | |
242 | } | |
243 | // trim leading spaces, but not new lines | |
244 | int nonSpace= 0; | |
245 | int len= comment.length(); | |
246 | while (nonSpace < len && Character.getType(comment.charAt(nonSpace)) == Character.SPACE_SEPARATOR) | |
247 | nonSpace++; | |
248 | comment= comment.substring(nonSpace); | |
249 | ||
250 | return Strings.changeIndent(comment, 0, project, indentation, lineDelimiter); | |
251 | } | |
252 | ||
253 | private String createTypeTags(IDocument document, DocumentCommand command, String indentation, String lineDelimiter, IType type) | |
254 | throws CoreException, BadLocationException | |
255 | { | |
256 | String[] typeParamNames= StubUtility.getTypeParameterNames(type.getTypeParameters()); | |
257 | String comment= CodeGeneration.getTypeComment(type.getCompilationUnit(), type.getTypeQualifiedName('.'), typeParamNames, lineDelimiter); | |
258 | if (comment != null) { | |
259 | boolean javadocComment= comment.startsWith("/**"); //$NON-NLS-1$ | |
260 | if (!isFirstComment(document, command, type, javadocComment)) | |
261 | return null; | |
262 | return prepareTemplateComment(comment.trim(), indentation, type.getJavaProject(), lineDelimiter); | |
263 | } | |
264 | return null; | |
265 | } | |
266 | ||
267 | private String createMethodTags(IDocument document, DocumentCommand command, String indentation, String lineDelimiter, IMethod method) | |
268 | throws CoreException, BadLocationException | |
269 | { | |
270 | IRegion partition= TextUtilities.getPartition(document, fPartitioning, command.offset, false); | |
271 | IMethod inheritedMethod= getInheritedMethod(method); | |
272 | String comment= CodeGeneration.getMethodComment(method, inheritedMethod, lineDelimiter); | |
273 | if (comment != null) { | |
274 | comment= comment.trim(); | |
275 | boolean javadocComment= comment.startsWith("/**"); //$NON-NLS-1$ | |
276 | if (!isFirstComment(document, command, method, javadocComment)) | |
277 | return null; | |
278 | boolean isJavaDoc= partition.getLength() >= 3 && document.get(partition.getOffset(), 3).equals("/**"); //$NON-NLS-1$ | |
279 | if (javadocComment == isJavaDoc) { | |
280 | return prepareTemplateComment(comment, indentation, method.getJavaProject(), lineDelimiter); | |
281 | } | |
282 | } | |
283 | return null; | |
284 | } | |
285 | ||
286 | /** | |
287 | * Returns <code>true</code> if the comment being inserted at | |
288 | * <code>command.offset</code> is the first comment (the first | |
289 | * Javadoc comment if <code>ignoreJavadoc</code> is | |
290 | * <code>true</code>) of the given member. | |
291 | * <p> | |
292 | * see also https://bugs.eclipse.org/bugs/show_bug.cgi?id=55325 (don't add parameters if the member already has a comment) | |
293 | * </p> | |
294 | * @param document the document | |
295 | * @param command the document command | |
296 | * @param member the Java member | |
297 | * @param ignoreNonJavadoc <code>true</code> if non Javadoc should be ignored | |
298 | * @return <code>true</code> if it is the first comment | |
299 | * @throws JavaModelException if accessing the Java model fails | |
300 | * @throws BadLocationException if accessing the document fails | |
301 | */ | |
302 | private boolean isFirstComment(IDocument document, DocumentCommand command, IMember member, boolean ignoreNonJavadoc) throws BadLocationException, JavaModelException { | |
303 | IRegion partition= TextUtilities.getPartition(document, fPartitioning, command.offset, false); | |
304 | ISourceRange sourceRange= member.getSourceRange(); | |
305 | if (sourceRange == null) | |
306 | return false; | |
307 | int srcOffset= sourceRange.getOffset(); | |
308 | int srcLength= sourceRange.getLength(); | |
309 | int nameRelativeOffset= member.getNameRange().getOffset() - srcOffset; | |
310 | int partitionRelativeOffset= partition.getOffset() - srcOffset; | |
311 | String token= ignoreNonJavadoc ? "/**" : "/*"; //$NON-NLS-1$ //$NON-NLS-2$ | |
312 | return document.get(srcOffset, srcLength).lastIndexOf(token, nameRelativeOffset) == partitionRelativeOffset; | |
313 | } | |
314 | ||
315 | /** | |
316 | * Unindents a typed slash ('/') if it forms the end of a comment. | |
317 | * | |
318 | * @param d the document | |
319 | * @param c the command | |
320 | */ | |
321 | private void indentAfterCommentEnd(IDocument d, DocumentCommand c) { | |
322 | if (c.offset < 2 || d.getLength() == 0) { | |
323 | return; | |
324 | } | |
325 | try { | |
326 | if ("* ".equals(d.get(c.offset - 2, 2))) { //$NON-NLS-1$ | |
327 | // modify document command | |
328 | c.length++; | |
329 | c.offset--; | |
330 | } | |
331 | } catch (BadLocationException excp) { | |
332 | // stop work | |
333 | } | |
334 | } | |
335 | ||
336 | /** | |
337 | * Guesses if the command operates within a newly created Javadoc comment or not. | |
338 | * If in doubt, it will assume that the Javadoc is new. | |
339 | * | |
340 | * @param document the document | |
341 | * @param commandOffset the command offset | |
342 | * @return <code>true</code> if the comment should be closed, <code>false</code> if not | |
343 | */ | |
344 | private boolean isNewComment(IDocument document, int commandOffset) { | |
345 | ||
346 | try { | |
347 | int lineIndex= document.getLineOfOffset(commandOffset) + 1; | |
348 | if (lineIndex >= document.getNumberOfLines()) | |
349 | return true; | |
350 | ||
351 | IRegion line= document.getLineInformation(lineIndex); | |
352 | ITypedRegion partition= TextUtilities.getPartition(document, fPartitioning, commandOffset, false); | |
353 | int partitionEnd= partition.getOffset() + partition.getLength(); | |
354 | if (line.getOffset() >= partitionEnd) | |
355 | return false; | |
356 | ||
357 | if (document.getLength() == partitionEnd) | |
358 | return true; // partition goes to end of document - probably a new comment | |
359 | ||
360 | String comment= document.get(partition.getOffset(), partition.getLength()); | |
361 | if (comment.indexOf("/*", 2) != -1) //$NON-NLS-1$ | |
362 | return true; // enclosed another comment -> probably a new comment | |
363 | ||
364 | return false; | |
365 | ||
366 | } catch (BadLocationException e) { | |
367 | return false; | |
368 | } | |
369 | } | |
370 | ||
371 | private boolean isSmartMode() { | |
372 | IWorkbenchPage page= JavaPlugin.getActivePage(); | |
373 | if (page != null) { | |
374 | IEditorPart part= page.getActiveEditor(); | |
375 | if (part instanceof ITextEditorExtension3) { | |
376 | ITextEditorExtension3 extension= (ITextEditorExtension3) part; | |
377 | return extension.getInsertMode() == ITextEditorExtension3.SMART_INSERT; | |
378 | } else if (EditorUtility.isCompareEditorInput(part.getEditorInput())) { | |
379 | ITextEditorExtension3 extension= (ITextEditorExtension3)part.getAdapter(ITextEditorExtension3.class); | |
380 | if (extension != null) | |
381 | return extension.getInsertMode() == ITextEditorExtension3.SMART_INSERT; | |
382 | } | |
383 | } | |
384 | return false; | |
385 | } | |
386 | ||
387 | /* | |
388 | * @see IAutoIndentStrategy#customizeDocumentCommand | |
389 | */ | |
390 | @Override | |
391 | public void customizeDocumentCommand(IDocument document, DocumentCommand command) { | |
392 | ||
393 | if (!isSmartMode()) | |
394 | return; | |
395 | ||
396 | if (command.text != null) { | |
397 | if (command.length == 0) { | |
398 | String[] lineDelimiters= document.getLegalLineDelimiters(); | |
399 | int index= TextUtilities.endsWith(lineDelimiters, command.text); | |
400 | if (index > -1) { | |
401 | // ends with line delimiter | |
402 | if (lineDelimiters[index].equals(command.text)) | |
403 | // just the line delimiter | |
404 | indentAfterNewLine(document, command); | |
405 | return; | |
406 | } | |
407 | } | |
408 | ||
409 | if (command.text.equals("/")) { //$NON-NLS-1$ | |
410 | indentAfterCommentEnd(document, command); | |
411 | return; | |
412 | } | |
413 | } | |
414 | } | |
415 | ||
416 | /** | |
417 | * Returns the method inherited from, <code>null</code> if method is newly defined. | |
418 | * @param method the method being written | |
419 | * @return the ancestor method, or <code>null</code> if none | |
420 | * @throws JavaModelException if accessing the Java model fails | |
421 | */ | |
422 | private static IMethod getInheritedMethod(IMethod method) throws JavaModelException { | |
423 | IType declaringType= method.getDeclaringType(); | |
424 | MethodOverrideTester tester= SuperTypeHierarchyCache.getMethodOverrideTester(declaringType); | |
425 | return tester.findOverriddenMethod(method, true); | |
426 | } | |
427 | ||
428 | /** | |
429 | * Returns the compilation unit of the compilation unit editor invoking the <code>AutoIndentStrategy</code>, | |
430 | * might return <code>null</code> on error. | |
431 | * @return the compilation unit represented by the document | |
432 | */ | |
433 | private static ICompilationUnit getCompilationUnit() { | |
434 | ||
435 | IWorkbenchWindow window= PlatformUI.getWorkbench().getActiveWorkbenchWindow(); | |
436 | if (window == null) | |
437 | return null; | |
438 | ||
439 | IWorkbenchPage page= window.getActivePage(); | |
440 | if (page == null) | |
441 | return null; | |
442 | ||
443 | IEditorPart editor= page.getActiveEditor(); | |
444 | if (editor == null) | |
445 | return null; | |
446 | ||
447 | IWorkingCopyManager manager= JavaPlugin.getDefault().getWorkingCopyManager(); | |
448 | ICompilationUnit unit= manager.getWorkingCopy(editor.getEditorInput()); | |
449 | if (unit == null) | |
450 | return null; | |
451 | ||
452 | return unit; | |
453 | } | |
454 | ||
455 | } |