package org.rubypeople.rdt.internal.ui.text.comment; import org.eclipse.jface.text.Assert; import org.eclipse.jface.text.BadLocationException; import org.eclipse.jface.text.DefaultIndentLineAutoEditStrategy; import org.eclipse.jface.text.DocumentCommand; import org.eclipse.jface.text.IDocument; import org.eclipse.jface.text.IRegion; import org.eclipse.jface.text.Region; import org.eclipse.jface.text.TextUtilities; import org.eclipse.ui.texteditor.ITextEditor; import org.jruby.lexer.yacc.SyntaxException; import org.rubypeople.rdt.core.IMethod; import org.rubypeople.rdt.core.IRubyElement; import org.rubypeople.rdt.core.IRubyProject; import org.rubypeople.rdt.core.IRubyScript; import org.rubypeople.rdt.core.RubyModelException; import org.rubypeople.rdt.internal.core.parser.RubyParser; import org.rubypeople.rdt.internal.corext.util.CodeFormatterUtil; import org.rubypeople.rdt.internal.ui.RubyPlugin; import org.rubypeople.rdt.internal.ui.rubyeditor.WorkingCopyManager; import org.rubypeople.rdt.internal.ui.text.IRubyPartitions; public class RubyCommentAutoIndentStrategy extends DefaultIndentLineAutoEditStrategy { private String fPartitioning; private ITextEditor fEditor; private WorkingCopyManager fManager; private IRubyProject fProject; public RubyCommentAutoIndentStrategy(ITextEditor textEditor, String partitioning, IRubyProject project) { fPartitioning = partitioning; fEditor = textEditor; fManager = RubyPlugin.getDefault().getWorkingCopyManager(); fProject = project; } public void customizeDocumentCommand(IDocument document, DocumentCommand command) { if (command.text != null) { if (command.length == 0) { String[] lineDelimiters = document.getLegalLineDelimiters(); int index = TextUtilities .endsWith(lineDelimiters, command.text); if (index > -1) { // ends with line delimiter if (lineDelimiters[index].equals(command.text)) // just the line delimiter indentAfterNewLine(document, command); return; } } } } /** * Copies the indentation of the previous line and adds a #. * * @param d * the document to work on * @param c * the command to deal with */ private void indentAfterNewLine(IDocument d, DocumentCommand c) { if (fPartitioning.equals(IRubyPartitions.RUBY_SINGLE_LINE_COMMENT)) { doSingleLineComment(d, c); } else { doMultiLineComment(d, c); } } private void doMultiLineComment(IDocument d, DocumentCommand c) { int offset = c.offset; if (offset == -1 || d.getLength() == 0) return; try { int p = (offset == d.getLength() ? offset - 1 : offset); try { new RubyParser().parse(d.get()); return; } catch(SyntaxException se) { if (!se.getMessage().equals("embedded document meets end of file")) { return; } } int lineNumber = d.getLineOfOffset(p); IRegion line = d.getLineInformation(lineNumber); String aLine = getLine(d, lineNumber - 1); StringBuffer buf = new StringBuffer(c.text); if (aLine.trim().equals("=begin")) { // add =end buf.append(CodeFormatterUtil.createIndentString(1, fProject)); c.caretOffset= c.offset + buf.length(); c.shiftsCaret= false; buf.append(TextUtilities.getDefaultLineDelimiter(d)); buf.append("=end"); } c.text = buf.toString(); } catch (BadLocationException excp) { // stop work } } private void doSingleLineComment(IDocument d, DocumentCommand c) { int offset = c.offset; if (offset == -1 || d.getLength() == 0) return; try { int p = (offset == d.getLength() ? offset - 1 : offset); int lineNumber = d.getLineOfOffset(p); IRegion line = d.getLineInformation(lineNumber); if (lineNumber != 0) { // If first line, extend the comment String nextLine = getLine(d, lineNumber + 1); // otherwise check next line if (!(isComment(nextLine) || isClassDefinition(nextLine) || isMethodDeclaration(nextLine) || isAttributeCall(nextLine) || isAliasCall(nextLine) || isModuleDeclaration(nextLine) || isConstantAssignment(nextLine))) { // if next line is commonly documented element, continue comment String previousLine = getLine(d, lineNumber - 1); if (!isComment(previousLine)) // last, if the previous line was a comment (so two lines in a row now), extend comments return; } } int lineOffset = line.getOffset(); int firstNonWS = findEndOfWhiteSpace(d, lineOffset, offset); Assert.isTrue(firstNonWS >= lineOffset, "indentation must not be negative"); //$NON-NLS-1$ StringBuffer buf = new StringBuffer(c.text); IRegion prefix = findPrefixRange(d, line); String indentation = d.get(prefix.getOffset(), prefix.getLength()); int lengthToAdd = Math.min(offset - prefix.getOffset(), prefix .getLength()); buf.append(indentation.substring(0, lengthToAdd)); String src = getRDoc(d, c.text, lineNumber + 1); if (src != null) buf.append(src); // move the caret behind the prefix, even if we do not have to // insert it. if (lengthToAdd < prefix.getLength()) c.caretOffset = offset + prefix.getLength() - lengthToAdd; c.text = buf.toString(); } catch (BadLocationException excp) { // stop work } } private String getRDoc(IDocument d, String newLine, int line) { IRubyScript script = fManager.getWorkingCopy(fEditor.getEditorInput()); int pos; try { IRegion region = d.getLineInformation(line); pos = findEndOfWhiteSpace(d, region.getOffset(), region.getOffset() + region.getLength()); } catch (BadLocationException e) { // TODO Auto-generated catch block e.printStackTrace(); return null; } IRubyElement element = null; try { element = script.getElementAt(pos); if (element == null) return null; StringBuffer buffer = new StringBuffer(); if (element instanceof IMethod) { IMethod method = (IMethod) element; String[] names = method.getParameterNames(); for (int i = 0; i < names.length; i++) { String name = names[i]; int end = name.indexOf(' '); if (end != -1) { name = name.substring(0, end); } buffer.append("+"); buffer.append(name); buffer.append("+"); buffer.append(newLine); } } return buffer.toString(); } catch (RubyModelException e) { // TODO Auto-generated catch block e.printStackTrace(); return null; } } private boolean isComment(String nextLineText) { return nextLineText.matches("^\\s*#.*"); } private boolean isClassDefinition(String nextLineText) { return nextLineText.matches("^\\s*class\\s+.+\\s*"); } private boolean isAliasCall(String nextLineText) { return nextLineText.matches("^\\s*alias\\s+.+\\s*"); } private boolean isModuleDeclaration(String nextLineText) { return nextLineText.matches("^\\s*module\\s+.+\\s*"); } private boolean isMethodDeclaration(String nextLineText) { return nextLineText.matches("^\\s*def\\s+.+\\s*"); } private boolean isAttributeCall(String nextLineText) { return nextLineText.matches("^\\s*attr.+\\s*"); } private boolean isConstantAssignment(String nextLineText) { return nextLineText.matches("^\\s*[A-Z_]+\\s?=\\s+.+\\s*"); } private String getLine(IDocument d, int lineNum) throws BadLocationException { IRegion nextLineRegion = d.getLineInformation(lineNum + 1); return d.get(nextLineRegion.getOffset(), nextLineRegion.getLength()); } /** * Returns the range of the comment prefix on the given line in * <code>document</code>. The prefix greedily matches the following regex * pattern: <code>\w*#\w*</code>, that is, any number of whitespace * characters, followed by an pound symbol ('#'), 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 */ private IRegion findPrefixRange(IDocument document, IRegion line) throws BadLocationException { int lineOffset = line.getOffset(); int lineEnd = lineOffset + line.getLength(); int indentEnd = findEndOfWhiteSpace(document, lineOffset, lineEnd); if (indentEnd < lineEnd && document.getChar(indentEnd) == '#') { indentEnd++; while (indentEnd < lineEnd && document.getChar(indentEnd) == ' ') indentEnd++; } return new Region(lineOffset, indentEnd - lineOffset); } }