|
From: Christopher W. <caw...@us...> - 2006-02-18 16:53:56
|
Update of /cvsroot/rubyeclipse/org.rubypeople.rdt.ui/src/org/rubypeople/rdt/internal/corext/template/ruby In directory sc8-pr-cvs1.sourceforge.net:/tmp/cvs-serv28822/src/org/rubypeople/rdt/internal/corext/template/ruby Added Files: RubyScriptContextType.java RubyScriptContext.java RubyFormatter.java RubyContextType.java RubyContext.java RubyTemplateMessages.java Log Message: overhaul our completion proposal/template code. Now we will apply formatting to templates when inserted! Plus we're closer to the way JDT does things. We still can't use the actual code formatter on the templates (because our old one does a massive replace edit which messes up variables like the cursor placement). The end result is that users should create templates with somewhat proper formatting initially. --- NEW FILE: RubyScriptContext.java --- package org.rubypeople.rdt.internal.corext.template.ruby; import org.eclipse.jface.text.IDocument; import org.eclipse.jface.text.templates.DocumentTemplateContext; import org.eclipse.jface.text.templates.TemplateContextType; import org.rubypeople.rdt.core.IRubyScript; import org.rubypeople.rdt.internal.ui.text.template.contentassist.MultiVariableGuess; public class RubyScriptContext extends DocumentTemplateContext { private IRubyScript fRubyScript; /** A flag to force evaluation in head-less mode. */ protected boolean fForceEvaluation; /** A global state for proposals that change if a master proposal changes. */ protected MultiVariableGuess fMultiVariableGuess; /** * Creates a ruby script context. * * @param type * the context type * @param document * the document * @param completionOffset * the completion position within the document * @param completionLength * the completion length within the document * @param rubyScript * the ruby script (may be <code>null</code>) */ protected RubyScriptContext(TemplateContextType type, IDocument document, int completionOffset, int completionLength, IRubyScript rubyScript) { super(type, document, completionOffset, completionLength); fRubyScript = rubyScript; } /** * Returns the ruby script if one is associated with this context, * <code>null</code> otherwise. * * @return the ruby script of this context or <code>null</code> */ public final IRubyScript getRubyScript() { return fRubyScript; } /** * Sets whether evaluation is forced or not. * * @param evaluate <code>true</code> in order to force evaluation, * <code>false</code> otherwise */ public void setForceEvaluation(boolean evaluate) { fForceEvaluation= evaluate; } /** * Returns the multi-variable guess. * * @return the multi-variable guess */ public MultiVariableGuess getMultiVariableGuess() { return fMultiVariableGuess; } /** * @param multiVariableGuess The multiVariableGuess to set. */ public void setMultiVariableGuess(MultiVariableGuess multiVariableGuess) { fMultiVariableGuess= multiVariableGuess; } } --- NEW FILE: RubyFormatter.java --- /******************************************************************************* * 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) || partitionType.equals(IRubyPartitions.RUBY_STRING) || partitionType.equals(IRubyPartitions.RUBY_CHARACTER) || partitionType.equals(IRubyPartitions.RUBY_DOC)); } 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); } } } --- NEW FILE: RubyContext.java --- /******************************************************************************* * Copyright (c) 2004 IBM Corporation and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Common Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/cpl-v10.html * * Contributors: * IBM Corporation - initial API and implementation *******************************************************************************/ package org.rubypeople.rdt.internal.corext.template.ruby; import org.eclipse.jface.preference.IPreferenceStore; import org.eclipse.jface.text.BadLocationException; import org.eclipse.jface.text.IDocument; import org.eclipse.jface.text.IRegion; import org.eclipse.jface.text.TextUtilities; import org.eclipse.jface.text.templates.Template; import org.eclipse.jface.text.templates.TemplateBuffer; import org.eclipse.jface.text.templates.TemplateContextType; import org.eclipse.jface.text.templates.TemplateException; import org.eclipse.jface.text.templates.TemplateTranslator; import org.eclipse.jface.text.templates.TemplateVariable; import org.rubypeople.rdt.core.IRubyProject; import org.rubypeople.rdt.core.IRubyScript; import org.rubypeople.rdt.internal.corext.util.Strings; import org.rubypeople.rdt.internal.ui.RubyPlugin; import org.rubypeople.rdt.internal.ui.text.template.contentassist.MultiVariable; import org.rubypeople.rdt.ui.PreferenceConstants; public class RubyContext extends RubyScriptContext { /** * Creates a ruby template context. * * @param type * the context type. * @param document * the document. * @param completionOffset * the completion offset within the document. * @param completionLength * the completion length. * @param compilationUnit * the compilation unit (may be <code>null</code>). */ public RubyContext(TemplateContextType type, IDocument document, int completionOffset, int completionLength, IRubyScript compilationUnit) { super(type, document, completionOffset, completionLength, compilationUnit); } /* * (non-Javadoc) * * @see org.eclipse.jface.text.templates.TemplateContext#evaluate(org.eclipse.jface.text.templates.Template) */ public TemplateBuffer evaluate(Template template) throws BadLocationException, TemplateException { if (!canEvaluate(template)) throw new TemplateException( RubyTemplateMessages.Context_error_cannot_evaluate); TemplateTranslator translator = new TemplateTranslator() { /* * @see org.eclipse.jface.text.templates.TemplateTranslator#createVariable(java.lang.String, * java.lang.String, int[]) */ protected TemplateVariable createVariable(String type, String name, int[] offsets) { return new MultiVariable(type, name, offsets); } }; TemplateBuffer buffer = translator.translate(template); getContextType().resolve(buffer, this); IPreferenceStore prefs = RubyPlugin.getDefault().getPreferenceStore(); boolean useCodeFormatter = prefs .getBoolean(PreferenceConstants.TEMPLATES_USE_CODEFORMATTER); IRubyProject project = getRubyScript() != null ? getRubyScript() .getRubyProject() : null; RubyFormatter formatter = new RubyFormatter(TextUtilities .getDefaultLineDelimiter(getDocument()), getIndentation(), useCodeFormatter, project); formatter.format(buffer, this); return buffer; } /** * Returns the indentation level at the position of code completion. * * @return the indentation level at the position of the code completion */ private int getIndentation() { int start = getStart(); IDocument document = getDocument(); try { IRegion region = document.getLineInformationOfOffset(start); String lineContent = document.get(region.getOffset(), region .getLength()); IRubyScript compilationUnit = getRubyScript(); IRubyProject project = compilationUnit == null ? null : compilationUnit.getRubyProject(); return Strings.computeIndentUnits(lineContent, project); } catch (BadLocationException e) { return 0; } } /* * @see TemplateContext#canEvaluate(Template templates) */ public boolean canEvaluate(Template template) { if (fForceEvaluation) return true; String key = getKey(); return template.matches(key, getContextType().getId()) && key.length() != 0 && template.getName().toLowerCase().startsWith( key.toLowerCase()); } /* * @see org.eclipse.jdt.internal.corext.template.DocumentTemplateContext#getKey() */ public String getKey() { if (getCompletionLength() == 0) return super.getKey(); try { IDocument document = getDocument(); int start = getStart(); int end = getCompletionOffset(); return start <= end ? document.get(start, end - start) : ""; //$NON-NLS-1$ } catch (BadLocationException e) { return super.getKey(); } } /* * (non-Javadoc) * * @see org.eclipse.jface.text.templates.DocumentTemplateContext#getEnd() */ public int getEnd() { if (getCompletionLength() == 0) return super.getEnd(); try { IDocument document = getDocument(); int start = getCompletionOffset(); int end = getCompletionOffset() + getCompletionLength(); while (start != end && Character.isWhitespace(document.getChar(end - 1))) end--; return end; } catch (BadLocationException e) { return super.getEnd(); } } /* * (non-Javadoc) * * @see org.eclipse.jface.text.templates.DocumentTemplateContext#getStart() */ public int getStart() { try { IDocument document = getDocument(); int start = getCompletionOffset(); int end = getCompletionOffset() + getCompletionLength(); while (start != 0 && Character.isUnicodeIdentifierPart(document .getChar(start - 1))) start--; while (start != end && Character.isWhitespace(document.getChar(start))) start++; if (start == end) start = getCompletionOffset(); return start; } catch (BadLocationException e) { return super.getStart(); } } } --- NEW FILE: RubyContextType.java --- package org.rubypeople.rdt.internal.corext.template.ruby; import org.eclipse.jface.text.IDocument; import org.eclipse.jface.text.templates.GlobalTemplateVariables; import org.rubypeople.rdt.core.IRubyScript; public class RubyContextType extends RubyScriptContextType { public static final String NAME = "ruby"; //$NON-NLS-1$ /** * Creates a ruby context type. */ public RubyContextType() { super(NAME); // global addResolver(new GlobalTemplateVariables.Cursor()); addResolver(new GlobalTemplateVariables.WordSelection()); addResolver(new GlobalTemplateVariables.LineSelection()); addResolver(new GlobalTemplateVariables.Dollar()); addResolver(new GlobalTemplateVariables.Date()); addResolver(new GlobalTemplateVariables.Year()); addResolver(new GlobalTemplateVariables.Time()); addResolver(new GlobalTemplateVariables.User()); } /* * (non-Javadoc) * * @see org.rubypeople.rdt.internal.corext.template.ruby.RubyFileContextType#createContext(org.eclipse.jface.text.IDocument, * int, int, org.rubypeople.rdt.core.IRubyScript) */ public RubyScriptContext createContext(IDocument document, int offset, int length, IRubyScript script) { return new RubyContext(this, document, offset, length, script); } } --- NEW FILE: RubyTemplateMessages.java --- package org.rubypeople.rdt.internal.corext.template.ruby; import org.eclipse.osgi.util.NLS; public class RubyTemplateMessages extends NLS { private static final String BUNDLE_NAME = RubyTemplateMessages.class .getName(); private RubyTemplateMessages() { // Do not instantiate } public static String ContextType_error_multiple_cursor_variables; public static String Context_error_cannot_evaluate; static { NLS.initializeMessages(BUNDLE_NAME, RubyTemplateMessages.class); } } --- NEW FILE: RubyScriptContextType.java --- /******************************************************************************* * Copyright (c) 2000, 2004 IBM Corporation and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Common Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/cpl-v10.html * * Contributors: * IBM Corporation - initial API and implementation *******************************************************************************/ package org.rubypeople.rdt.internal.corext.template.ruby; import org.eclipse.jface.text.IDocument; import org.eclipse.jface.text.templates.GlobalTemplateVariables; import org.eclipse.jface.text.templates.TemplateContextType; import org.eclipse.jface.text.templates.TemplateException; import org.eclipse.jface.text.templates.TemplateVariable; import org.rubypeople.rdt.core.IRubyScript; /** * A very simple context type. */ public abstract class RubyScriptContextType extends TemplateContextType { /** * Creates a new Ruby context type. */ public RubyScriptContextType(String name) { super(name); } public abstract RubyScriptContext createContext(IDocument document, int completionPosition, int length, IRubyScript script); /* * (non-Javadoc) * * @see org.eclipse.jdt.internal.corext.template.ContextType#validateVariables(org.eclipse.jdt.internal.corext.template.TemplateVariable[]) */ protected void validateVariables(TemplateVariable[] variables) throws TemplateException { // check for multiple cursor variables for (int i = 0; i < variables.length; i++) { TemplateVariable var = variables[i]; if (var.getType().equals(GlobalTemplateVariables.Cursor.NAME)) { if (var.getOffsets().length > 1) { throw new TemplateException( RubyTemplateMessages.ContextType_error_multiple_cursor_variables); } } } } } |