PageRenderTime 51ms CodeModel.GetById 11ms RepoModel.GetById 0ms app.codeStats 1ms

/groovy-eclipse/org.codehaus.groovy.eclipse.ui/src/org/codehaus/groovy/eclipse/editor/GroovyAutoIndentStrategy.java

http://groovy-eclipse.googlecode.com/
Java | 1256 lines | 1229 code | 12 blank | 15 comment | 12 complexity | e89865c7592cf1bf8c33f4c630d42cc0 MD5 | raw file
Possible License(s): Apache-2.0
  1. package org.codehaus.groovy.eclipse.editor;
  2. import org.eclipse.core.runtime.Assert;
  3. import org.eclipse.jdt.core.IJavaProject;
  4. import org.eclipse.jdt.core.ToolFactory;
  5. import org.eclipse.jdt.core.compiler.IProblem;
  6. import org.eclipse.jdt.core.compiler.IScanner;
  7. import org.eclipse.jdt.core.compiler.ITerminalSymbols;
  8. import org.eclipse.jdt.core.compiler.InvalidInputException;
  9. import org.eclipse.jdt.core.dom.AST;
  10. import org.eclipse.jdt.core.dom.ASTNode;
  11. import org.eclipse.jdt.core.dom.ASTParser;
  12. import org.eclipse.jdt.core.dom.CompilationUnit;
  13. import org.eclipse.jdt.core.dom.DoStatement;
  14. import org.eclipse.jdt.core.dom.Expression;
  15. import org.eclipse.jdt.core.dom.ForStatement;
  16. import org.eclipse.jdt.core.dom.IfStatement;
  17. import org.eclipse.jdt.core.dom.Statement;
  18. import org.eclipse.jdt.core.dom.WhileStatement;
  19. import org.eclipse.jdt.internal.corext.dom.NodeFinder;
  20. import org.eclipse.jdt.internal.corext.util.CodeFormatterUtil;
  21. import org.eclipse.jdt.internal.ui.JavaPlugin;
  22. import org.eclipse.jdt.internal.ui.text.FastJavaPartitionScanner;
  23. import org.eclipse.jdt.internal.ui.text.JavaHeuristicScanner;
  24. import org.eclipse.jdt.internal.ui.text.Symbols;
  25. import org.eclipse.jdt.ui.PreferenceConstants;
  26. import org.eclipse.jdt.ui.text.IJavaPartitions;
  27. import org.eclipse.jface.preference.IPreferenceStore;
  28. import org.eclipse.jface.text.BadLocationException;
  29. import org.eclipse.jface.text.DefaultIndentLineAutoEditStrategy;
  30. import org.eclipse.jface.text.Document;
  31. import org.eclipse.jface.text.DocumentCommand;
  32. import org.eclipse.jface.text.DocumentRewriteSession;
  33. import org.eclipse.jface.text.DocumentRewriteSessionType;
  34. import org.eclipse.jface.text.IDocument;
  35. import org.eclipse.jface.text.IRegion;
  36. import org.eclipse.jface.text.ITypedRegion;
  37. import org.eclipse.jface.text.Region;
  38. import org.eclipse.jface.text.TextUtilities;
  39. import org.eclipse.jface.text.rules.FastPartitioner;
  40. import org.eclipse.ui.IEditorPart;
  41. import org.eclipse.ui.IWorkbenchPage;
  42. import org.eclipse.ui.texteditor.ITextEditorExtension3;
  43. /**
  44. * Auto indent strategy sensitive to brackets.
  45. * This is a copy of JavaAutoIndentStrategy except it holds an instance of GroovyIndenter instead
  46. * of JavaIndenter
  47. */
  48. public class GroovyAutoIndentStrategy extends DefaultIndentLineAutoEditStrategy {
  49. /** The line comment introducer. Value is "{@value}" */
  50. private static final String LINE_COMMENT= "//"; //$NON-NLS-1$
  51. private static class CompilationUnitInfo {
  52. char[] buffer;
  53. int delta;
  54. CompilationUnitInfo(char[] buffer, int delta) {
  55. this.buffer= buffer;
  56. this.delta= delta;
  57. }
  58. }
  59. private boolean fCloseBrace;
  60. private boolean fIsSmartMode;
  61. private String fPartitioning;
  62. private final IJavaProject fProject;
  63. /**
  64. * Creates a new Java auto indent strategy for the given document partitioning.
  65. *
  66. * @param partitioning the document partitioning
  67. * @param project the project to get formatting preferences from, or null to use default preferences
  68. */
  69. public GroovyAutoIndentStrategy(String partitioning, IJavaProject project) {
  70. fPartitioning= partitioning;
  71. fProject= project;
  72. }
  73. private int getBracketCount(IDocument d, int startOffset, int endOffset, boolean ignoreCloseBrackets) throws BadLocationException {
  74. int bracketCount= 0;
  75. while (startOffset < endOffset) {
  76. char curr= d.getChar(startOffset);
  77. startOffset++;
  78. switch (curr) {
  79. case '/' :
  80. if (startOffset < endOffset) {
  81. char next= d.getChar(startOffset);
  82. if (next == '*') {
  83. // a comment starts, advance to the comment end
  84. startOffset= getCommentEnd(d, startOffset + 1, endOffset);
  85. } else if (next == '/') {
  86. // '//'-comment: nothing to do anymore on this line
  87. startOffset= endOffset;
  88. }
  89. }
  90. break;
  91. case '*' :
  92. if (startOffset < endOffset) {
  93. char next= d.getChar(startOffset);
  94. if (next == '/') {
  95. // we have been in a comment: forget what we read before
  96. bracketCount= 0;
  97. startOffset++;
  98. }
  99. }
  100. break;
  101. case '{' :
  102. bracketCount++;
  103. ignoreCloseBrackets= false;
  104. break;
  105. case '}' :
  106. if (!ignoreCloseBrackets) {
  107. bracketCount--;
  108. }
  109. break;
  110. case '"' :
  111. case '\'' :
  112. startOffset= getStringEnd(d, startOffset, endOffset, curr);
  113. break;
  114. default :
  115. }
  116. }
  117. return bracketCount;
  118. }
  119. // ----------- bracket counting ------------------------------------------------------
  120. private int getCommentEnd(IDocument d, int offset, int endOffset) throws BadLocationException {
  121. while (offset < endOffset) {
  122. char curr= d.getChar(offset);
  123. offset++;
  124. if (curr == '*') {
  125. if (offset < endOffset && d.getChar(offset) == '/') {
  126. return offset + 1;
  127. }
  128. }
  129. }
  130. return endOffset;
  131. }
  132. private String getIndentOfLine(IDocument d, int line) throws BadLocationException {
  133. if (line > -1) {
  134. int start= d.getLineOffset(line);
  135. int end= start + d.getLineLength(line) - 1;
  136. int whiteEnd= findEndOfWhiteSpace(d, start, end);
  137. return d.get(start, whiteEnd - start);
  138. }
  139. return ""; //$NON-NLS-1$
  140. }
  141. private int getStringEnd(IDocument d, int offset, int endOffset, char ch) throws BadLocationException {
  142. while (offset < endOffset) {
  143. char curr= d.getChar(offset);
  144. offset++;
  145. if (curr == '\\') {
  146. // ignore escaped characters
  147. offset++;
  148. } else if (curr == ch) {
  149. return offset;
  150. }
  151. }
  152. return endOffset;
  153. }
  154. private void smartIndentAfterClosingBracket(IDocument d, DocumentCommand c) {
  155. if (c.offset == -1 || d.getLength() == 0)
  156. return;
  157. try {
  158. int p= (c.offset == d.getLength() ? c.offset - 1 : c.offset);
  159. int line= d.getLineOfOffset(p);
  160. int start= d.getLineOffset(line);
  161. int whiteend= findEndOfWhiteSpace(d, start, c.offset);
  162. JavaHeuristicScanner scanner= new JavaHeuristicScanner(d);
  163. GroovyIndenter indenter= new GroovyIndenter(d, scanner, fProject);
  164. // shift only when line does not contain any text up to the closing bracket
  165. if (whiteend == c.offset) {
  166. // evaluate the line with the opening bracket that matches out closing bracket
  167. int reference= indenter.findReferencePosition(c.offset, false, true, false, false);
  168. int indLine= d.getLineOfOffset(reference);
  169. if (indLine != -1 && indLine != line) {
  170. // take the indent of the found line
  171. StringBuffer replaceText= new StringBuffer(getIndentOfLine(d, indLine));
  172. // add the rest of the current line including the just added close bracket
  173. replaceText.append(d.get(whiteend, c.offset - whiteend));
  174. replaceText.append(c.text);
  175. // modify document command
  176. c.length += c.offset - start;
  177. c.offset= start;
  178. c.text= replaceText.toString();
  179. }
  180. }
  181. } catch (BadLocationException e) {
  182. JavaPlugin.log(e);
  183. }
  184. }
  185. private void smartIndentAfterOpeningBracket(IDocument d, DocumentCommand c) {
  186. if (c.offset < 1 || d.getLength() == 0)
  187. return;
  188. JavaHeuristicScanner scanner= new JavaHeuristicScanner(d);
  189. int p= (c.offset == d.getLength() ? c.offset - 1 : c.offset);
  190. try {
  191. // current line
  192. int line= d.getLineOfOffset(p);
  193. int lineOffset= d.getLineOffset(line);
  194. // make sure we don't have any leading comments etc.
  195. if (d.get(lineOffset, p - lineOffset).trim().length() != 0)
  196. return;
  197. // line of last javacode
  198. int pos= scanner.findNonWhitespaceBackward(p, JavaHeuristicScanner.UNBOUND);
  199. if (pos == -1)
  200. return;
  201. int lastLine= d.getLineOfOffset(pos);
  202. // only shift if the last java line is further up and is a braceless block candidate
  203. if (lastLine < line) {
  204. GroovyIndenter indenter= new GroovyIndenter(d, scanner, fProject);
  205. StringBuffer indent= indenter.computeIndentation(p, true);
  206. String toDelete= d.get(lineOffset, c.offset - lineOffset);
  207. if (indent != null && !indent.toString().equals(toDelete)) {
  208. c.text= indent.append(c.text).toString();
  209. c.length += c.offset - lineOffset;
  210. c.offset= lineOffset;
  211. }
  212. }
  213. } catch (BadLocationException e) {
  214. JavaPlugin.log(e);
  215. }
  216. }
  217. private void smartIndentAfterNewLine(IDocument d, DocumentCommand c) {
  218. JavaHeuristicScanner scanner= new JavaHeuristicScanner(d);
  219. GroovyIndenter indenter= new GroovyIndenter(d, scanner, fProject);
  220. StringBuffer indent= indenter.computeIndentation(c.offset);
  221. if (indent == null)
  222. indent= new StringBuffer();
  223. int docLength= d.getLength();
  224. if (c.offset == -1 || docLength == 0)
  225. return;
  226. try {
  227. int p= (c.offset == docLength ? c.offset - 1 : c.offset);
  228. int line= d.getLineOfOffset(p);
  229. StringBuffer buf= new StringBuffer(c.text + indent);
  230. IRegion reg= d.getLineInformation(line);
  231. int lineEnd= reg.getOffset() + reg.getLength();
  232. int contentStart= findEndOfWhiteSpace(d, c.offset, lineEnd);
  233. c.length= Math.max(contentStart - c.offset, 0);
  234. int start= reg.getOffset();
  235. ITypedRegion region= TextUtilities.getPartition(d, fPartitioning, start, true);
  236. if (IJavaPartitions.JAVA_DOC.equals(region.getType()))
  237. start= d.getLineInformationOfOffset(region.getOffset()).getOffset();
  238. // insert closing brace on new line after an unclosed opening brace
  239. if (getBracketCount(d, start, c.offset, true) > 0 && closeBrace() && !isClosed(d, c.offset, c.length)) {
  240. c.caretOffset= c.offset + buf.length();
  241. c.shiftsCaret= false;
  242. // copy old content of line behind insertion point to new line
  243. // unless we think we are inserting an anonymous type definition
  244. if (c.offset == 0 || !(computeAnonymousPosition(d, c.offset - 1, fPartitioning, lineEnd) != -1)) {
  245. if (lineEnd - contentStart > 0) {
  246. c.length= lineEnd - c.offset;
  247. buf.append(d.get(contentStart, lineEnd - contentStart).toCharArray());
  248. }
  249. }
  250. buf.append(TextUtilities.getDefaultLineDelimiter(d));
  251. StringBuffer reference= null;
  252. int nonWS= findEndOfWhiteSpace(d, start, lineEnd);
  253. if (nonWS < c.offset && d.getChar(nonWS) == '{')
  254. reference= new StringBuffer(d.get(start, nonWS - start));
  255. else
  256. reference= indenter.getReferenceIndentation(c.offset);
  257. if (reference != null)
  258. buf.append(reference);
  259. buf.append('}');
  260. }
  261. // insert extra line upon new line between two braces
  262. else if (c.offset > start && contentStart < lineEnd && d.getChar(contentStart) == '}') {
  263. int firstCharPos= scanner.findNonWhitespaceBackward(c.offset - 1, start);
  264. if (firstCharPos != JavaHeuristicScanner.NOT_FOUND && d.getChar(firstCharPos) == '{') {
  265. c.caretOffset= c.offset + buf.length();
  266. c.shiftsCaret= false;
  267. StringBuffer reference= null;
  268. int nonWS= findEndOfWhiteSpace(d, start, lineEnd);
  269. if (nonWS < c.offset && d.getChar(nonWS) == '{')
  270. reference= new StringBuffer(d.get(start, nonWS - start));
  271. else
  272. reference= indenter.getReferenceIndentation(c.offset);
  273. buf.append(TextUtilities.getDefaultLineDelimiter(d));
  274. if (reference != null)
  275. buf.append(reference);
  276. }
  277. }
  278. c.text= buf.toString();
  279. } catch (BadLocationException e) {
  280. JavaPlugin.log(e);
  281. }
  282. }
  283. /**
  284. * Computes an insert position for an opening brace if <code>offset</code> maps to a position in
  285. * <code>document</code> with a expression in parenthesis that will take a block after the closing parenthesis.
  286. *
  287. * @param document the document being modified
  288. * @param offset the offset of the caret position, relative to the line start.
  289. * @param partitioning the document partitioning
  290. * @param max the max position
  291. * @return an insert position relative to the line start if <code>line</code> contains a parenthesized expression that can be followed by a block, -1 otherwise
  292. */
  293. private static int computeAnonymousPosition(IDocument document, int offset, String partitioning, int max) {
  294. // find the opening parenthesis for every closing parenthesis on the current line after offset
  295. // return the position behind the closing parenthesis if it looks like a method declaration
  296. // or an expression for an if, while, for, catch statement
  297. JavaHeuristicScanner scanner= new JavaHeuristicScanner(document);
  298. int pos= offset;
  299. int length= max;
  300. int scanTo= scanner.scanForward(pos, length, '}');
  301. if (scanTo == -1)
  302. scanTo= length;
  303. int closingParen= findClosingParenToLeft(scanner, pos) - 1;
  304. while (true) {
  305. int startScan= closingParen + 1;
  306. closingParen= scanner.scanForward(startScan, scanTo, ')');
  307. if (closingParen == -1)
  308. break;
  309. int openingParen= scanner.findOpeningPeer(closingParen - 1, '(', ')');
  310. // no way an expression at the beginning of the document can mean anything
  311. if (openingParen < 1)
  312. break;
  313. // only select insert positions for parenthesis currently embracing the caret
  314. if (openingParen > pos)
  315. continue;
  316. if (looksLikeAnonymousClassDef(document, partitioning, scanner, openingParen - 1))
  317. return closingParen + 1;
  318. }
  319. return -1;
  320. }
  321. /**
  322. * Finds a closing parenthesis to the left of <code>position</code> in document, where that parenthesis is only
  323. * separated by whitespace from <code>position</code>. If no such parenthesis can be found, <code>position</code> is returned.
  324. *
  325. * @param scanner the java heuristic scanner set up on the document
  326. * @param position the first character position in <code>document</code> to be considered
  327. * @return the position of a closing parenthesis left to <code>position</code> separated only by whitespace, or <code>position</code> if no parenthesis can be found
  328. */
  329. private static int findClosingParenToLeft(JavaHeuristicScanner scanner, int position) {
  330. if (position < 1)
  331. return position;
  332. if (scanner.previousToken(position - 1, JavaHeuristicScanner.UNBOUND) == Symbols.TokenRPAREN)
  333. return scanner.getPosition() + 1;
  334. return position;
  335. }
  336. /**
  337. * Checks whether the content of <code>document</code> in the range (<code>offset</code>, <code>length</code>)
  338. * contains the <code>new</code> keyword.
  339. *
  340. * @param document the document being modified
  341. * @param offset the first character position in <code>document</code> to be considered
  342. * @param length the length of the character range to be considered
  343. * @param partitioning the document partitioning
  344. * @return <code>true</code> if the specified character range contains a <code>new</code> keyword, <code>false</code> otherwise.
  345. */
  346. private static boolean isNewMatch(IDocument document, int offset, int length, String partitioning) {
  347. Assert.isTrue(length >= 0);
  348. Assert.isTrue(offset >= 0);
  349. Assert.isTrue(offset + length < document.getLength() + 1);
  350. try {
  351. String text= document.get(offset, length);
  352. int pos= text.indexOf("new"); //$NON-NLS-1$
  353. while (pos != -1 && !isDefaultPartition(document, pos + offset, partitioning))
  354. pos= text.indexOf("new", pos + 2); //$NON-NLS-1$
  355. if (pos < 0)
  356. return false;
  357. if (pos != 0 && Character.isJavaIdentifierPart(text.charAt(pos - 1)))
  358. return false;
  359. if (pos + 3 < length && Character.isJavaIdentifierPart(text.charAt(pos + 3)))
  360. return false;
  361. return true;
  362. } catch (BadLocationException e) {
  363. }
  364. return false;
  365. }
  366. /**
  367. * Checks whether the content of <code>document</code> at <code>position</code> looks like an
  368. * anonymous class definition. <code>position</code> must be to the left of the opening
  369. * parenthesis of the definition's parameter list.
  370. *
  371. * @param document the document being modified
  372. * @param position the first character position in <code>document</code> to be considered
  373. * @param partitioning the document partitioning
  374. * @return <code>true</code> if the content of <code>document</code> looks like an anonymous class definition, <code>false</code> otherwise
  375. */
  376. private static boolean looksLikeAnonymousClassDef(IDocument document, String partitioning, JavaHeuristicScanner scanner, int position) {
  377. int previousCommaParenEqual= scanner.scanBackward(position - 1, JavaHeuristicScanner.UNBOUND, new char[] {',', '(', '='});
  378. if (previousCommaParenEqual == -1 || position < previousCommaParenEqual + 5) // 2 for borders, 3 for "new"
  379. return false;
  380. if (isNewMatch(document, previousCommaParenEqual + 1, position - previousCommaParenEqual - 2, partitioning))
  381. return true;
  382. return false;
  383. }
  384. /**
  385. * Checks whether <code>position</code> resides in a default (Java) partition of <code>document</code>.
  386. *
  387. * @param document the document being modified
  388. * @param position the position to be checked
  389. * @param partitioning the document partitioning
  390. * @return <code>true</code> if <code>position</code> is in the default partition of <code>document</code>, <code>false</code> otherwise
  391. */
  392. private static boolean isDefaultPartition(IDocument document, int position, String partitioning) {
  393. Assert.isTrue(position >= 0);
  394. Assert.isTrue(position <= document.getLength());
  395. try {
  396. ITypedRegion region= TextUtilities.getPartition(document, partitioning, position, false);
  397. return region.getType().equals(IDocument.DEFAULT_CONTENT_TYPE);
  398. } catch (BadLocationException e) {
  399. }
  400. return false;
  401. }
  402. private boolean isClosed(IDocument document, int offset, int length) {
  403. CompilationUnitInfo info= getCompilationUnitForMethod(document, offset, fPartitioning);
  404. if (info == null)
  405. return false;
  406. CompilationUnit compilationUnit= null;
  407. try {
  408. ASTParser parser= ASTParser.newParser(AST.JLS3);
  409. parser.setSource(info.buffer);
  410. compilationUnit= (CompilationUnit) parser.createAST(null);
  411. } catch (ArrayIndexOutOfBoundsException x) {
  412. // work around for parser problem
  413. return false;
  414. }
  415. IProblem[] problems= compilationUnit.getProblems();
  416. for (int i= 0; i != problems.length; ++i) {
  417. if (problems[i].getID() == IProblem.UnmatchedBracket)
  418. return true;
  419. }
  420. final int relativeOffset= offset - info.delta;
  421. ASTNode node= NodeFinder.perform(compilationUnit, relativeOffset, length);
  422. if (length == 0) {
  423. while (node != null && (relativeOffset == node.getStartPosition() || relativeOffset == node.getStartPosition() + node.getLength()))
  424. node= node.getParent();
  425. }
  426. if (node == null)
  427. return false;
  428. switch (node.getNodeType()) {
  429. case ASTNode.BLOCK:
  430. return getBlockBalance(document, offset, fPartitioning) <= 0;
  431. case ASTNode.IF_STATEMENT:
  432. {
  433. IfStatement ifStatement= (IfStatement) node;
  434. Expression expression= ifStatement.getExpression();
  435. IRegion expressionRegion= createRegion(expression, info.delta);
  436. Statement thenStatement= ifStatement.getThenStatement();
  437. IRegion thenRegion= createRegion(thenStatement, info.delta);
  438. // between expression and then statement
  439. if (expressionRegion.getOffset() + expressionRegion.getLength() <= offset && offset + length <= thenRegion.getOffset())
  440. return thenStatement != null;
  441. Statement elseStatement= ifStatement.getElseStatement();
  442. IRegion elseRegion= createRegion(elseStatement, info.delta);
  443. if (elseStatement != null) {
  444. int sourceOffset= thenRegion.getOffset() + thenRegion.getLength();
  445. int sourceLength= elseRegion.getOffset() - sourceOffset;
  446. IRegion elseToken= getToken(document, new Region(sourceOffset, sourceLength), ITerminalSymbols.TokenNameelse);
  447. return elseToken != null && elseToken.getOffset() + elseToken.getLength() <= offset && offset + length < elseRegion.getOffset();
  448. }
  449. }
  450. break;
  451. case ASTNode.WHILE_STATEMENT:
  452. case ASTNode.FOR_STATEMENT:
  453. {
  454. Expression expression= node.getNodeType() == ASTNode.WHILE_STATEMENT ? ((WhileStatement) node).getExpression() : ((ForStatement) node).getExpression();
  455. IRegion expressionRegion= createRegion(expression, info.delta);
  456. Statement body= node.getNodeType() == ASTNode.WHILE_STATEMENT ? ((WhileStatement) node).getBody() : ((ForStatement) node).getBody();
  457. IRegion bodyRegion= createRegion(body, info.delta);
  458. // between expression and body statement
  459. if (expressionRegion.getOffset() + expressionRegion.getLength() <= offset && offset + length <= bodyRegion.getOffset())
  460. return body != null;
  461. }
  462. break;
  463. case ASTNode.DO_STATEMENT:
  464. {
  465. DoStatement doStatement= (DoStatement) node;
  466. IRegion doRegion= createRegion(doStatement, info.delta);
  467. Statement body= doStatement.getBody();
  468. IRegion bodyRegion= createRegion(body, info.delta);
  469. if (doRegion.getOffset() + doRegion.getLength() <= offset && offset + length <= bodyRegion.getOffset())
  470. return body != null;
  471. }
  472. break;
  473. }
  474. return true;
  475. }
  476. /**
  477. * Installs a java partitioner with <code>document</code>.
  478. *
  479. * @param document the document
  480. */
  481. private static void installJavaStuff(Document document) {
  482. String[] types= new String[] {
  483. IJavaPartitions.JAVA_DOC,
  484. IJavaPartitions.JAVA_MULTI_LINE_COMMENT,
  485. IJavaPartitions.JAVA_SINGLE_LINE_COMMENT,
  486. IJavaPartitions.JAVA_STRING,
  487. IJavaPartitions.JAVA_CHARACTER,
  488. IDocument.DEFAULT_CONTENT_TYPE
  489. };
  490. FastPartitioner partitioner= new FastPartitioner(new FastJavaPartitionScanner(), types);
  491. partitioner.connect(document);
  492. document.setDocumentPartitioner(IJavaPartitions.JAVA_PARTITIONING, partitioner);
  493. }
  494. /**
  495. * Installs a java partitioner with <code>document</code>.
  496. *
  497. * @param document the document
  498. */
  499. private static void removeJavaStuff(Document document) {
  500. document.setDocumentPartitioner(IJavaPartitions.JAVA_PARTITIONING, null);
  501. }
  502. private void smartPaste(IDocument document, DocumentCommand command) {
  503. int newOffset= command.offset;
  504. int newLength= command.length;
  505. String newText= command.text;
  506. try {
  507. JavaHeuristicScanner scanner= new JavaHeuristicScanner(document);
  508. GroovyIndenter indenter= new GroovyIndenter(document, scanner, fProject);
  509. int offset= newOffset;
  510. // reference position to get the indent from
  511. int refOffset= indenter.findReferencePosition(offset);
  512. if (refOffset == JavaHeuristicScanner.NOT_FOUND)
  513. return;
  514. int peerOffset= getPeerPosition(document, command);
  515. peerOffset= indenter.findReferencePosition(peerOffset);
  516. refOffset= Math.min(refOffset, peerOffset);
  517. // eat any WS before the insertion to the beginning of the line
  518. int firstLine= 1; // don't format the first line per default, as it has other content before it
  519. IRegion line= document.getLineInformationOfOffset(offset);
  520. String notSelected= document.get(line.getOffset(), offset - line.getOffset());
  521. if (notSelected.trim().length() == 0) {
  522. newLength += notSelected.length();
  523. newOffset= line.getOffset();
  524. firstLine= 0;
  525. }
  526. // prefix: the part we need for formatting but won't paste
  527. IRegion refLine= document.getLineInformationOfOffset(refOffset);
  528. String prefix= document.get(refLine.getOffset(), newOffset - refLine.getOffset());
  529. // handle the indentation computation inside a temporary document
  530. Document temp= new Document(prefix + newText);
  531. DocumentRewriteSession session= temp.startRewriteSession(DocumentRewriteSessionType.STRICTLY_SEQUENTIAL);
  532. scanner= new JavaHeuristicScanner(temp);
  533. indenter= new GroovyIndenter(temp, scanner, fProject);
  534. installJavaStuff(temp);
  535. // indent the first and second line
  536. // compute the relative indentation difference from the second line
  537. // (as the first might be partially selected) and use the value to
  538. // indent all other lines.
  539. boolean isIndentDetected= false;
  540. StringBuffer addition= new StringBuffer();
  541. int insertLength= 0;
  542. int first= document.computeNumberOfLines(prefix) + firstLine; // don't format first line
  543. int lines= temp.getNumberOfLines();
  544. boolean changed= false;
  545. for (int l= first; l < lines; l++) { // we don't change the number of lines while adding indents
  546. IRegion r= temp.getLineInformation(l);
  547. int lineOffset= r.getOffset();
  548. int lineLength= r.getLength();
  549. if (lineLength == 0) // don't modify empty lines
  550. continue;
  551. if (!isIndentDetected) {
  552. // indent the first pasted line
  553. String current= getCurrentIndent(temp, l);
  554. StringBuffer correct= indenter.computeIndentation(lineOffset);
  555. if (correct == null)
  556. return; // bail out
  557. insertLength= subtractIndent(correct, current, addition);
  558. if (l != first && temp.get(lineOffset, lineLength).trim().length() != 0) {
  559. isIndentDetected= true;
  560. if (insertLength == 0) {
  561. // no adjustment needed, bail out
  562. if (firstLine == 0) {
  563. // but we still need to adjust the first line
  564. command.offset= newOffset;
  565. command.length= newLength;
  566. if (changed)
  567. break; // still need to get the leading indent of the first line
  568. }
  569. return;
  570. }
  571. removeJavaStuff(temp);
  572. } else {
  573. changed= insertLength != 0;
  574. }
  575. }
  576. // relatively indent all pasted lines
  577. if (insertLength > 0)
  578. addIndent(temp, l, addition);
  579. else if (insertLength < 0)
  580. cutIndent(temp, l, -insertLength);
  581. }
  582. temp.stopRewriteSession(session);
  583. newText= temp.get(prefix.length(), temp.getLength() - prefix.length());
  584. command.offset= newOffset;
  585. command.length= newLength;
  586. command.text= newText;
  587. } catch (BadLocationException e) {
  588. JavaPlugin.log(e);
  589. }
  590. }
  591. /**
  592. * Returns the indentation of the line <code>line</code> in <code>document</code>.
  593. * The returned string may contain pairs of leading slashes that are considered
  594. * part of the indentation. The space before the asterix in a javadoc-like
  595. * comment is not considered part of the indentation.
  596. *
  597. * @param document the document
  598. * @param line the line
  599. * @return the indentation of <code>line</code> in <code>document</code>
  600. * @throws BadLocationException if the document is changed concurrently
  601. */
  602. private static String getCurrentIndent(Document document, int line) throws BadLocationException {
  603. IRegion region= document.getLineInformation(line);
  604. int from= region.getOffset();
  605. int endOffset= region.getOffset() + region.getLength();
  606. // go behind line comments
  607. int to= from;
  608. while (to < endOffset - 2 && document.get(to, 2).equals(LINE_COMMENT))
  609. to += 2;
  610. while (to < endOffset) {
  611. char ch= document.getChar(to);
  612. if (!Character.isWhitespace(ch))
  613. break;
  614. to++;
  615. }
  616. // don't count the space before javadoc like, asterix-style comment lines
  617. if (to > from && to < endOffset - 1 && document.get(to - 1, 2).equals(" *")) { //$NON-NLS-1$
  618. String type= TextUtilities.getContentType(document, IJavaPartitions.JAVA_PARTITIONING, to, true);
  619. if (type.equals(IJavaPartitions.JAVA_DOC) || type.equals(IJavaPartitions.JAVA_MULTI_LINE_COMMENT))
  620. to--;
  621. }
  622. return document.get(from, to - from);
  623. }
  624. /**
  625. * Computes the difference of two indentations and returns the difference in
  626. * length of current and correct. If the return value is positive, <code>addition</code>
  627. * is initialized with a substring of that length of <code>correct</code>.
  628. *
  629. * @param correct the correct indentation
  630. * @param current the current indentation (migth contain non-whitespace)
  631. * @param difference a string buffer - if the return value is positive, it will be cleared and set to the substring of <code>current</code> of that length
  632. * @return the difference in lenght of <code>correct</code> and <code>current</code>
  633. */
  634. private int subtractIndent(CharSequence correct, CharSequence current, StringBuffer difference) {
  635. int c1= computeVisualLength(correct);
  636. int c2= computeVisualLength(current);
  637. int diff= c1 - c2;
  638. if (diff <= 0)
  639. return diff;
  640. difference.setLength(0);
  641. int len= 0, i= 0;
  642. while (len < diff) {
  643. char c= correct.charAt(i++);
  644. difference.append(c);
  645. len += computeVisualLength(c);
  646. }
  647. return diff;
  648. }
  649. /**
  650. * Indents line <code>line</code> in <code>document</code> with <code>indent</code>.
  651. * Leaves leading comment signs alone.
  652. *
  653. * @param document the document
  654. * @param line the line
  655. * @param indent the indentation to insert
  656. * @throws BadLocationException on concurrent document modification
  657. */
  658. private static void addIndent(Document document, int line, CharSequence indent) throws BadLocationException {
  659. IRegion region= document.getLineInformation(line);
  660. int insert= region.getOffset();
  661. int endOffset= region.getOffset() + region.getLength();
  662. // go behind line comments
  663. while (insert < endOffset - 2 && document.get(insert, 2).equals(LINE_COMMENT))
  664. insert += 2;
  665. // insert indent
  666. document.replace(insert, 0, indent.toString());
  667. }
  668. /**
  669. * Cuts the visual equivalent of <code>toDelete</code> characters out of the
  670. * indentation of line <code>line</code> in <code>document</code>. Leaves
  671. * leading comment signs alone.
  672. *
  673. * @param document the document
  674. * @param line the line
  675. * @param toDelete the number of space equivalents to delete.
  676. * @throws BadLocationException on concurrent document modification
  677. */
  678. private void cutIndent(Document document, int line, int toDelete) throws BadLocationException {
  679. IRegion region= document.getLineInformation(line);
  680. int from= region.getOffset();
  681. int endOffset= region.getOffset() + region.getLength();
  682. // go behind line comments
  683. while (from < endOffset - 2 && document.get(from, 2).equals(LINE_COMMENT))
  684. from += 2;
  685. int to= from;
  686. while (toDelete > 0 && to < endOffset) {
  687. char ch= document.getChar(to);
  688. if (!Character.isWhitespace(ch))
  689. break;
  690. toDelete -= computeVisualLength(ch);
  691. if (toDelete >= 0)
  692. to++;
  693. else
  694. break;
  695. }
  696. document.replace(from, to - from, null);
  697. }
  698. /**
  699. * Returns the visual length of a given <code>CharSequence</code> taking into
  700. * account the visual tabulator length.
  701. *
  702. * @param seq the string to measure
  703. * @return the visual length of <code>seq</code>
  704. */
  705. private int computeVisualLength(CharSequence seq) {
  706. int size= 0;
  707. int tablen= getVisualTabLengthPreference();
  708. for (int i= 0; i < seq.length(); i++) {
  709. char ch= seq.charAt(i);
  710. if (ch == '\t') {
  711. if (tablen != 0)
  712. size += tablen - size % tablen;
  713. // else: size stays the same
  714. } else {
  715. size++;
  716. }
  717. }
  718. return size;
  719. }
  720. /**
  721. * Returns the visual length of a given character taking into
  722. * account the visual tabulator length.
  723. *
  724. * @param ch the character to measure
  725. * @return the visual length of <code>ch</code>
  726. */
  727. private int computeVisualLength(char ch) {
  728. if (ch == '\t'){
  729. return getVisualTabLengthPreference();
  730. }
  731. return 1;
  732. }
  733. /**
  734. * The preference setting for the visual tabulator display.
  735. *
  736. * @return the number of spaces displayed for a tabulator in the editor
  737. */
  738. private int getVisualTabLengthPreference() {
  739. return CodeFormatterUtil.getTabWidth(fProject);
  740. }
  741. private int getPeerPosition(IDocument document, DocumentCommand command) {
  742. if (document.getLength() == 0)
  743. return 0;
  744. /*
  745. * Search for scope closers in the pasted text and find their opening peers
  746. * in the document.
  747. */
  748. Document pasted= new Document(command.text);
  749. installJavaStuff(pasted);
  750. int firstPeer= command.offset;
  751. JavaHeuristicScanner pScanner= new JavaHeuristicScanner(pasted);
  752. JavaHeuristicScanner dScanner= new JavaHeuristicScanner(document);
  753. // add scope relevant after context to peer search
  754. int afterToken= dScanner.nextToken(command.offset + command.length, JavaHeuristicScanner.UNBOUND);
  755. try {
  756. switch (afterToken) {
  757. case Symbols.TokenRBRACE:
  758. pasted.replace(pasted.getLength(), 0, "}"); //$NON-NLS-1$
  759. break;
  760. case Symbols.TokenRPAREN:
  761. pasted.replace(pasted.getLength(), 0, ")"); //$NON-NLS-1$
  762. break;
  763. case Symbols.TokenRBRACKET:
  764. pasted.replace(pasted.getLength(), 0, "]"); //$NON-NLS-1$
  765. break;
  766. }
  767. } catch (BadLocationException e) {
  768. // cannot happen
  769. Assert.isTrue(false);
  770. }
  771. int pPos= 0; // paste text position (increasing from 0)
  772. int dPos= Math.max(0, command.offset - 1); // document position (decreasing from paste offset)
  773. while (true) {
  774. int token= pScanner.nextToken(pPos, JavaHeuristicScanner.UNBOUND);
  775. pPos= pScanner.getPosition();
  776. switch (token) {
  777. case Symbols.TokenLBRACE:
  778. case Symbols.TokenLBRACKET:
  779. case Symbols.TokenLPAREN:
  780. pPos= skipScope(pScanner, pPos, token);
  781. if (pPos == JavaHeuristicScanner.NOT_FOUND)
  782. return firstPeer;
  783. break; // closed scope -> keep searching
  784. case Symbols.TokenRBRACE:
  785. int peer= dScanner.findOpeningPeer(dPos, '{', '}');
  786. dPos= peer - 1;
  787. if (peer == JavaHeuristicScanner.NOT_FOUND)
  788. return firstPeer;
  789. firstPeer= peer;
  790. break; // keep searching
  791. case Symbols.TokenRBRACKET:
  792. peer= dScanner.findOpeningPeer(dPos, '[', ']');
  793. dPos= peer - 1;
  794. if (peer == JavaHeuristicScanner.NOT_FOUND)
  795. return firstPeer;
  796. firstPeer= peer;
  797. break; // keep searching
  798. case Symbols.TokenRPAREN:
  799. peer= dScanner.findOpeningPeer(dPos, '(', ')');
  800. dPos= peer - 1;
  801. if (peer == JavaHeuristicScanner.NOT_FOUND)
  802. return firstPeer;
  803. firstPeer= peer;
  804. break; // keep searching
  805. case Symbols.TokenCASE:
  806. case Symbols.TokenDEFAULT:
  807. GroovyIndenter indenter= new GroovyIndenter(document, dScanner, fProject);
  808. peer= indenter.findReferencePosition(dPos, false, false, false, true);
  809. if (peer == JavaHeuristicScanner.NOT_FOUND)
  810. return firstPeer;
  811. firstPeer= peer;
  812. break; // keep searching
  813. case Symbols.TokenEOF:
  814. return firstPeer;
  815. default:
  816. // keep searching
  817. }
  818. }
  819. }
  820. /**
  821. * Skips the scope opened by <code>token</code> in <code>document</code>,
  822. * returns either the position of the
  823. * @param pos
  824. * @param token
  825. * @return the position after the scope
  826. */
  827. private static int skipScope(JavaHeuristicScanner scanner, int pos, int token) {
  828. int openToken= token;
  829. int closeToken;
  830. switch (token) {
  831. case Symbols.TokenLPAREN:
  832. closeToken= Symbols.TokenRPAREN;
  833. break;
  834. case Symbols.TokenLBRACKET:
  835. closeToken= Symbols.TokenRBRACKET;
  836. break;
  837. case Symbols.TokenLBRACE:
  838. closeToken= Symbols.TokenRBRACE;
  839. break;
  840. default:
  841. Assert.isTrue(false);
  842. return -1; // dummy
  843. }
  844. int depth= 1;
  845. int p= pos;
  846. while (true) {
  847. int tok= scanner.nextToken(p, JavaHeuristicScanner.UNBOUND);
  848. p= scanner.getPosition();
  849. if (tok == openToken) {
  850. depth++;
  851. } else if (tok == closeToken) {
  852. depth--;
  853. if (depth == 0)
  854. return p + 1;
  855. } else if (tok == Symbols.TokenEOF) {
  856. return JavaHeuristicScanner.NOT_FOUND;
  857. }
  858. }
  859. }
  860. private boolean isLineDelimiter(IDocument document, String text) {
  861. String[] delimiters= document.getLegalLineDelimiters();
  862. if (delimiters != null)
  863. return TextUtilities.equals(delimiters, text) > -1;
  864. return false;
  865. }
  866. private void smartIndentOnKeypress(IDocument document, DocumentCommand command) {
  867. switch (command.text.charAt(0)) {
  868. case '}':
  869. smartIndentAfterClosingBracket(document, command);
  870. break;
  871. case '{':
  872. smartIndentAfterOpeningBracket(document, command);
  873. break;
  874. case 'e':
  875. smartIndentUponE(document, command);
  876. break;
  877. }
  878. }
  879. private void smartIndentUponE(IDocument d, DocumentCommand c) {
  880. if (c.offset < 4 || d.getLength() == 0)
  881. return;
  882. try {
  883. String content= d.get(c.offset - 3, 3);
  884. if (content.equals("els")) { //$NON-NLS-1$
  885. JavaHeuristicScanner scanner= new JavaHeuristicScanner(d);
  886. int p= c.offset - 3;
  887. // current line
  888. int line= d.getLineOfOffset(p);
  889. int lineOffset= d.getLineOffset(line);
  890. // make sure we don't have any leading comments etc.
  891. if (d.get(lineOffset, p - lineOffset).trim().length() != 0)
  892. return;
  893. // line of last javacode
  894. int pos= scanner.findNonWhitespaceBackward(p - 1, JavaHeuristicScanner.UNBOUND);
  895. if (pos == -1)
  896. return;
  897. int lastLine= d.getLineOfOffset(pos);
  898. // only shift if the last java line is further up and is a braceless block candidate
  899. if (lastLine < line) {
  900. GroovyIndenter indenter= new GroovyIndenter(d, scanner, fProject);
  901. int ref= indenter.findReferencePosition(p, true, false, false, false);
  902. if (ref == JavaHeuristicScanner.NOT_FOUND)
  903. return;
  904. int refLine= d.getLineOfOffset(ref);
  905. String indent= getIndentOfLine(d, refLine);
  906. if (indent != null) {
  907. c.text= indent.toString() + "else"; //$NON-NLS-1$
  908. c.length += c.offset - lineOffset;
  909. c.offset= lineOffset;
  910. }
  911. }
  912. return;
  913. }
  914. if (content.equals("cas")) { //$NON-NLS-1$
  915. JavaHeuristicScanner scanner= new JavaHeuristicScanner(d);
  916. int p= c.offset - 3;
  917. // current line
  918. int line= d.getLineOfOffset(p);
  919. int lineOffset= d.getLineOffset(line);
  920. // make sure we don't have any leading comments etc.
  921. if (d.get(lineOffset, p - lineOffset).trim().length() != 0)
  922. return;
  923. // line of last javacode
  924. int pos= scanner.findNonWhitespaceBackward(p - 1, JavaHeuristicScanner.UNBOUND);
  925. if (pos == -1)
  926. return;
  927. int lastLine= d.getLineOfOffset(pos);
  928. // only shift if the last java line is further up and is a braceless block candidate
  929. if (lastLine < line) {
  930. GroovyIndenter indenter= new GroovyIndenter(d, scanner, fProject);
  931. int ref= indenter.findReferencePosition(p, false, false, false, true);
  932. if (ref == JavaHeuristicScanner.NOT_FOUND)
  933. return;
  934. int refLine= d.getLineOfOffset(ref);
  935. int nextToken= scanner.nextToken(ref, JavaHeuristicScanner.UNBOUND);
  936. String indent;
  937. if (nextToken == Symbols.TokenCASE || nextToken == Symbols.TokenDEFAULT)
  938. indent= getIndentOfLine(d, refLine);
  939. else // at the brace of the switch
  940. indent= indenter.computeIndentation(p).toString();
  941. if (indent != null) {
  942. c.text= indent.toString() + "case"; //$NON-NLS-1$
  943. c.length += c.offset - lineOffset;
  944. c.offset= lineOffset;
  945. }
  946. }
  947. return;
  948. }
  949. } catch (BadLocationException e) {
  950. JavaPlugin.log(e);
  951. }
  952. }
  953. /*
  954. * @see org.eclipse.jface.text.IAutoIndentStrategy#customizeDocumentCommand(org.eclipse.jface.text.IDocument, org.eclipse.jface.text.DocumentCommand)
  955. */
  956. public void customizeDocumentCommand(IDocument d, DocumentCommand c) {
  957. if (c.doit == false)
  958. return;
  959. clearCachedValues();
  960. if (!isSmartMode()) {
  961. super.customizeDocumentCommand(d, c);
  962. return;
  963. }
  964. if (c.length == 0 && c.text != null && isLineDelimiter(d, c.text))
  965. smartIndentAfterNewLine(d, c);
  966. else if (c.text.length() == 1)
  967. smartIndentOnKeypress(d, c);
  968. else if (c.text.length() > 1 && getPreferenceStore().getBoolean(PreferenceConstants.EDITOR_SMART_PASTE))
  969. smartPaste(d, c); // no smart backspace for paste
  970. }
  971. private static IPreferenceStore getPreferenceStore() {
  972. return JavaPlugin.getDefault().getCombinedPreferenceStore();
  973. }
  974. private boolean closeBrace() {
  975. return fCloseBrace;
  976. }
  977. private boolean isSmartMode() {
  978. return fIsSmartMode;
  979. }
  980. private void clearCachedValues() {
  981. IPreferenceStore preferenceStore= getPreferenceStore();
  982. fCloseBrace= preferenceStore.getBoolean(PreferenceConstants.EDITOR_CLOSE_BRACES);
  983. fIsSmartMode= computeSmartMode();
  984. }
  985. private boolean computeSmartMode() {
  986. IWorkbenchPage page= JavaPlugin.getActivePage();
  987. if (page != null) {
  988. IEditorPart part= page.getActiveEditor();
  989. if (part instanceof ITextEditorExtension3) {
  990. ITextEditorExtension3 extension= (ITextEditorExtension3) part;
  991. return extension.getInsertMode() == ITextEditorExtension3.SMART_INSERT;
  992. }
  993. }
  994. return false;
  995. }
  996. private static CompilationUnitInfo getCompilationUnitForMethod(IDocument document, int offset, String partitioning) {
  997. try {
  998. JavaHeuristicScanner scanner= new JavaHeuristicScanner(document);
  999. IRegion sourceRange= scanner.findSurroundingBlock(offset);
  1000. if (sourceRange == null)
  1001. return null;
  1002. String source= document.get(sourceRange.getOffset(), sourceRange.getLength());
  1003. StringBuffer contents= new StringBuffer();
  1004. contents.append("class ____C{void ____m()"); //$NON-NLS-1$
  1005. final int methodOffset= contents.length();
  1006. contents.append(source);
  1007. contents.append('}');
  1008. char[] buffer= contents.toString().toCharArray();
  1009. return new CompilationUnitInfo(buffer, sourceRange.getOffset() - methodOffset);
  1010. } catch (BadLocationException e) {
  1011. JavaPlugin.log(e);
  1012. }
  1013. return null;
  1014. }
  1015. /**
  1016. * Returns the block balance, i.e. zero if the blocks are balanced at
  1017. * <code>offset</code>, a negative number if there are more closing than opening
  1018. * braces, and a positive number if there are more opening than closing braces.
  1019. *
  1020. * @param document
  1021. * @param offset
  1022. * @param partitioning
  1023. * @return the block balance
  1024. */
  1025. private static int getBlockBalance(IDocument document, int offset, String partitioning) {
  1026. if (offset < 1)
  1027. return -1;
  1028. if (offset >= document.getLength())
  1029. return 1;
  1030. int begin= offset;
  1031. int end= offset - 1;
  1032. JavaHeuristicScanner scanner= new JavaHeuristicScanner(document);
  1033. while (true) {
  1034. begin= scanner.findOpeningPeer(begin - 1, '{', '}');
  1035. end= scanner.findClosingPeer(end + 1, '{', '}');
  1036. if (begin == -1 && end == -1)
  1037. return 0;
  1038. if (begin == -1)
  1039. return -1;
  1040. if (end == -1)
  1041. return 1;
  1042. }
  1043. }
  1044. private static IRegion createRegion(ASTNode node, int delta) {
  1045. return node == null ? null : new Region(node.getStartPosition() + delta, node.getLength());
  1046. }
  1047. private static IRegion getToken(IDocument document, IRegion scanRegion, int tokenId) {
  1048. try {
  1049. final String source= document.get(scanRegion.getOffset(), scanRegion.getLength());
  1050. IScanner scanner= ToolFactory.createScanner(false, false, false, false);
  1051. scanner.setSource(source.toCharArray());
  1052. int id= scanner.getNextToken();
  1053. while (id != ITerminalSymbols.TokenNameEOF && id != tokenId)
  1054. id= scanner.getNextToken();
  1055. if (id == ITerminalSymbols.TokenNameEOF)
  1056. return null;
  1057. int tokenOffset= scanner.getCurrentTokenStartPosition();
  1058. int tokenLength= scanner.getCurrentTokenEndPosition() + 1 - tokenOffset; // inclusive end
  1059. return new Region(tokenOffset + scanRegion.getOffset(), tokenLength);
  1060. } catch (InvalidInputException x) {
  1061. return null;
  1062. } catch (BadLocationException x) {
  1063. return null;
  1064. }
  1065. }
  1066. }