/******************************************************************************* * Copyright (c) 2000, 2005 IBM Corporation and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * IBM Corporation - initial API and implementation *******************************************************************************/ package org.rubypeople.rdt.internal.corext.template.ruby; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.Map; import org.eclipse.jface.text.BadLocationException; import org.eclipse.jface.text.Document; import org.eclipse.jface.text.IDocument; import org.eclipse.jface.text.IRegion; import org.eclipse.jface.text.ITypedRegion; import org.eclipse.jface.text.templates.DocumentTemplateContext; import org.eclipse.jface.text.templates.GlobalTemplateVariables; import org.eclipse.jface.text.templates.TemplateBuffer; import org.eclipse.jface.text.templates.TemplateContext; import org.eclipse.jface.text.templates.TemplateVariable; import org.eclipse.text.edits.DeleteEdit; import org.eclipse.text.edits.InsertEdit; import org.eclipse.text.edits.MalformedTreeException; import org.eclipse.text.edits.MultiTextEdit; import org.eclipse.text.edits.RangeMarker; import org.eclipse.text.edits.ReplaceEdit; import org.eclipse.text.edits.TextEdit; import org.rubypeople.rdt.core.IRubyProject; import org.rubypeople.rdt.core.RubyCore; import org.rubypeople.rdt.core.formatter.CodeFormatter; import org.rubypeople.rdt.internal.corext.util.CodeFormatterUtil; import org.rubypeople.rdt.internal.corext.util.Strings; import org.rubypeople.rdt.internal.ui.RubyPlugin; import org.rubypeople.rdt.internal.ui.text.IRubyPartitions; import org.rubypeople.rdt.internal.ui.text.RubyHeuristicScanner; /** * A template editor using the Ruby formatter to format a template buffer. */ public class RubyFormatter { private static final String MARKER= "/*${" + GlobalTemplateVariables.Cursor.NAME + "}*/"; //$NON-NLS-1$ //$NON-NLS-2$ /** The line delimiter to use if code formatter is not used. */ private final String fLineDelimiter; /** The initial indent level */ private final int fInitialIndentLevel; /** The java partitioner */ private boolean fUseCodeFormatter; private final IRubyProject fProject; /** * Creates a RubyFormatter with the target line delimiter. * * @param lineDelimiter the line delimiter to use * @param initialIndentLevel the initial indentation level * @param useCodeFormatter <code>true</code> if the core code formatter should be used * @param project the java project from which to get the preferences, or <code>null</code> for workbench settings */ public RubyFormatter(String lineDelimiter, int initialIndentLevel, boolean useCodeFormatter, IRubyProject project) { fLineDelimiter= lineDelimiter; fUseCodeFormatter= useCodeFormatter; fInitialIndentLevel= initialIndentLevel; fProject= project; } /** * Formats the template buffer. * @param buffer * @param context * @throws BadLocationException */ public void format(TemplateBuffer buffer, TemplateContext context) throws BadLocationException { try { if (fUseCodeFormatter) // try to format and fall back to indenting try { format(buffer, (RubyContext) context); } catch (BadLocationException e) { indent(buffer); } catch (MalformedTreeException e) { indent(buffer); } else indent(buffer); // don't trim the buffer if the replacement area is empty // case: surrounding empty lines with block if (context instanceof DocumentTemplateContext) { DocumentTemplateContext dtc= (DocumentTemplateContext) context; if (dtc.getStart() == dtc.getCompletionOffset()) if (dtc.getDocument().get(dtc.getStart(), dtc.getEnd() - dtc.getStart()).trim().length() == 0) return; } trimBegin(buffer); } catch (MalformedTreeException e) { throw new BadLocationException(); } } private static int getCaretOffset(TemplateVariable[] variables) { for (int i= 0; i != variables.length; i++) { TemplateVariable variable= variables[i]; if (variable.getType().equals(GlobalTemplateVariables.Cursor.NAME)) return variable.getOffsets()[0]; } return -1; } private boolean isInsideCommentOrString(String string, int offset) { IDocument document= new Document(string); RubyPlugin.getDefault().getRubyTextTools().setupRubyDocumentPartitioner(document); try { ITypedRegion partition= document.getPartition(offset); String partitionType= partition.getType(); return partitionType != null && ( partitionType.equals(IRubyPartitions.RUBY_MULTI_LINE_COMMENT) || partitionType.equals(IRubyPartitions.RUBY_SINGLE_LINE_COMMENT)); // FIXME How do we tell if we're in a string now? } catch (BadLocationException e) { return false; } } private void format(TemplateBuffer templateBuffer, RubyContext context) throws BadLocationException { // XXX 4360, 15247 // workaround for code formatter limitations // handle a special case where cursor position is surrounded by whitespace String string= templateBuffer.getString(); TemplateVariable[] variables= templateBuffer.getVariables(); int caretOffset= getCaretOffset(variables); if ((caretOffset > 0) && Character.isWhitespace(string.charAt(caretOffset - 1)) && (caretOffset < string.length()) && Character.isWhitespace(string.charAt(caretOffset)) && ! isInsideCommentOrString(string, caretOffset)) { List positions= variablesToPositions(variables); TextEdit insert= new InsertEdit(caretOffset, MARKER); string= edit(string, positions, insert); positionsToVariables(positions, variables); templateBuffer.setContent(string, variables); try { plainFormat(templateBuffer, context); string= templateBuffer.getString(); variables= templateBuffer.getVariables(); caretOffset= getCaretOffset(variables); } finally { positions= variablesToPositions(variables); TextEdit delete= new DeleteEdit(caretOffset, MARKER.length()); string= edit(string, positions, delete); positionsToVariables(positions, variables); templateBuffer.setContent(string, variables); } } else { plainFormat(templateBuffer, context); } } private void plainFormat(TemplateBuffer templateBuffer, RubyContext context) throws BadLocationException { IDocument doc= new Document(templateBuffer.getString()); TemplateVariable[] variables= templateBuffer.getVariables(); List offsets= variablesToPositions(variables); Map options; if (context.getRubyScript() != null) options= context.getRubyScript().getRubyProject().getOptions(true); else options= RubyCore.getOptions(); String contents= doc.get(); int[] kinds= { CodeFormatter.K_EXPRESSION, CodeFormatter.K_STATEMENTS, CodeFormatter.K_UNKNOWN}; TextEdit edit= null; for (int i= 0; i < kinds.length && edit == null; i++) { edit= CodeFormatterUtil.format2(kinds[i], contents, fInitialIndentLevel, fLineDelimiter, options); } if (edit == null) throw new BadLocationException(); // fall back to indenting MultiTextEdit root; if (edit instanceof MultiTextEdit) root= (MultiTextEdit) edit; else { root= new MultiTextEdit(0, doc.getLength()); root.addChild(edit); } for (Iterator it= offsets.iterator(); it.hasNext();) { TextEdit position= (TextEdit) it.next(); try { root.addChild(position); } catch (MalformedTreeException e) { // position conflicts with formatter edit // ignore this position } } root.apply(doc, TextEdit.UPDATE_REGIONS); positionsToVariables(offsets, variables); templateBuffer.setContent(doc.get(), variables); } private void indent(TemplateBuffer templateBuffer) throws BadLocationException, MalformedTreeException { TemplateVariable[] variables= templateBuffer.getVariables(); List positions= variablesToPositions(variables); IDocument document= new Document(templateBuffer.getString()); MultiTextEdit root= new MultiTextEdit(0, document.getLength()); root.addChildren((TextEdit[]) positions.toArray(new TextEdit[positions.size()])); // first line int offset= document.getLineOffset(0); String indent = CodeFormatterUtil.createIndentString(fInitialIndentLevel, fProject); TextEdit edit= new InsertEdit(offset, indent); root.addChild(edit); root.apply(document, TextEdit.UPDATE_REGIONS); root.removeChild(edit); formatDelimiter(document, root, 0); // following lines int lineCount= document.getNumberOfLines(); RubyHeuristicScanner scanner= new RubyHeuristicScanner(document); // RubyIndenter indenter= new RubyIndenter(document, scanner, fProject); for (int line= 1; line < lineCount; line++) { IRegion region= document.getLineInformation(line); offset= region.getOffset(); // StringBuffer indent= indenter.computeIndentation(offset); if (indent == null) continue; // int nonWS= scanner.findNonWhitespaceForwardInAnyPartition(offset, offset + region.getLength()); // if (nonWS == RubyHeuristicScanner.NOT_FOUND) // nonWS= region.getLength() + offset; edit= new ReplaceEdit(offset, 0, indent.toString()); root.addChild(edit); root.apply(document, TextEdit.UPDATE_REGIONS); root.removeChild(edit); formatDelimiter(document, root, line); } positionsToVariables(positions, variables); templateBuffer.setContent(document.get(), variables); } /** * Changes the delimiter to the configured line delimiter. * * @param document the temporary document being edited * @param root the root edit containing all positions that will be updated along the way * @param line the line to format * @throws BadLocationException if applying the changes fails */ private void formatDelimiter(IDocument document, MultiTextEdit root, int line) throws BadLocationException { IRegion region= document.getLineInformation(line); String lineDelimiter= document.getLineDelimiter(line); if (lineDelimiter != null) { TextEdit edit= new ReplaceEdit(region.getOffset() + region.getLength(), lineDelimiter.length(), fLineDelimiter); root.addChild(edit); root.apply(document, TextEdit.UPDATE_REGIONS); root.removeChild(edit); } } private static void trimBegin(TemplateBuffer templateBuffer) throws BadLocationException { String string= templateBuffer.getString(); TemplateVariable[] variables= templateBuffer.getVariables(); List positions= variablesToPositions(variables); int i= 0; while ((i != string.length()) && Character.isWhitespace(string.charAt(i))) i++; string= edit(string, positions, new DeleteEdit(0, i)); positionsToVariables(positions, variables); templateBuffer.setContent(string, variables); } private static String edit(String string, List positions, TextEdit edit) throws BadLocationException { MultiTextEdit root= new MultiTextEdit(0, string.length()); root.addChildren((TextEdit[]) positions.toArray(new TextEdit[positions.size()])); root.addChild(edit); IDocument document= new Document(string); root.apply(document); return document.get(); } private static List variablesToPositions(TemplateVariable[] variables) { List positions= new ArrayList(5); for (int i= 0; i != variables.length; i++) { int[] offsets= variables[i].getOffsets(); // trim positions off whitespace String value= variables[i].getDefaultValue(); int wsStart= 0; while (wsStart < value.length() && Character.isWhitespace(value.charAt(wsStart)) && !Strings.isLineDelimiterChar(value.charAt(wsStart))) wsStart++; variables[i].getValues()[0]= value.substring(wsStart); for (int j= 0; j != offsets.length; j++) { offsets[j] += wsStart; positions.add(new RangeMarker(offsets[j], 0)); } } return positions; } private static void positionsToVariables(List positions, TemplateVariable[] variables) { Iterator iterator= positions.iterator(); for (int i= 0; i != variables.length; i++) { TemplateVariable variable= variables[i]; int[] offsets= new int[variable.getOffsets().length]; for (int j= 0; j != offsets.length; j++) offsets[j]= ((TextEdit) iterator.next()).getOffset(); variable.setOffsets(offsets); } } }