/** * Copyright (c) 2005-2013 by Appcelerator, Inc. All Rights Reserved. * Licensed under the terms of the Eclipse Public License (EPL). * Please see the license.txt included with this distribution for details. * Any modifications to this file must keep this entire header intact. */ package com.python.pydev.refactoring.tdd; import java.util.List; import org.eclipse.core.runtime.Assert; import org.eclipse.jface.dialogs.IInputValidator; import org.eclipse.jface.dialogs.InputDialog; import org.eclipse.jface.text.BadLocationException; import org.eclipse.jface.text.Region; import org.eclipse.jface.text.contentassist.CompletionProposal; import org.eclipse.jface.text.contentassist.ICompletionProposal; import org.eclipse.jface.text.contentassist.ICompletionProposalExtension2; import org.eclipse.jface.text.templates.GlobalTemplateVariables; import org.eclipse.jface.text.templates.Template; import org.eclipse.jface.text.templates.TemplateContextType; import org.eclipse.jface.text.templates.TemplateProposal; import org.python.pydev.core.docutils.PySelection; import org.python.pydev.core.docutils.PySelection.LineStartingScope; import org.python.pydev.core.docutils.PyStringUtils; import org.python.pydev.core.log.Log; import org.python.pydev.editor.codecompletion.templates.PyDocumentTemplateContext; import org.python.pydev.editor.codecompletion.templates.PyTemplateCompletionProcessor; import org.python.pydev.editor.correctionassist.heuristics.AssistAssign; import org.python.pydev.parser.jython.ast.ClassDef; import org.python.pydev.parser.jython.ast.Pass; import org.python.pydev.parser.visitors.NodeUtils; import org.python.pydev.refactoring.ast.adapters.IClassDefAdapter; import org.python.pydev.refactoring.ast.adapters.ModuleAdapter; import org.python.pydev.refactoring.ast.adapters.offsetstrategy.BeginOffset; import org.python.pydev.refactoring.ast.adapters.offsetstrategy.EndOffset; import org.python.pydev.refactoring.ast.adapters.offsetstrategy.IOffsetStrategy; import org.python.pydev.refactoring.core.base.RefactoringInfo; import org.python.pydev.shared_core.string.FastStringBuffer; import org.python.pydev.shared_core.string.StringUtils; import org.python.pydev.shared_core.structure.Tuple; import org.python.pydev.shared_ui.EditorUtils; public abstract class AbstractPyCreateClassOrMethodOrField extends AbstractPyCreateAction { public abstract String getCreationStr(); @Override public void execute(RefactoringInfo refactoringInfo, int locationStrategy) { try { String creationStr = this.getCreationStr(); final String asTitle = StringUtils.getWithFirstUpper(creationStr); PySelection pySelection = refactoringInfo.getPySelection(); Tuple<String, Integer> currToken = pySelection.getCurrToken(); String actTok = currToken.o1; List<String> parametersAfterCall = null; if (actTok.length() == 0) { InputDialog dialog = new InputDialog(EditorUtils.getShell(), asTitle + " name", "Please enter the name of the " + asTitle + " to be created.", "", new IInputValidator() { @Override public String isValid(String newText) { if (newText.length() == 0) { return "The " + asTitle + " name may not be empty"; } if (StringUtils.containsWhitespace(newText)) { return "The " + asTitle + " name may not contain whitespaces."; } return null; } }); if (dialog.open() != InputDialog.OK) { return; } actTok = dialog.getValue(); } else { parametersAfterCall = pySelection.getParametersAfterCall(currToken.o2 + actTok.length()); } execute(refactoringInfo, actTok, parametersAfterCall, locationStrategy); } catch (BadLocationException e) { Log.log(e); } } /** * When executed it'll create a proposal and apply it. */ /*default*/void execute(RefactoringInfo refactoringInfo, String actTok, List<String> parametersAfterCall, int locationStrategy) { try { ICompletionProposal proposal = createProposal(refactoringInfo, actTok, locationStrategy, parametersAfterCall); if (proposal != null) { if (proposal instanceof ICompletionProposalExtension2) { ICompletionProposalExtension2 extension2 = (ICompletionProposalExtension2) proposal; extension2.apply(targetEditor.getPySourceViewer(), '\n', 0, 0); } else { proposal.apply(refactoringInfo.getDocument()); } } } catch (Exception e) { Log.log(e); } } protected ICompletionProposal createProposal(PySelection pySelection, String source, Tuple<Integer, String> offsetAndIndent) { return createProposal(pySelection, source, offsetAndIndent, true, null); } protected ICompletionProposal createProposal(PySelection pySelection, String source, Tuple<Integer, String> offsetAndIndent, boolean requireEmptyLines, Pass replacePassStatement) { int offset; int len; String indent = offsetAndIndent.o2; if (replacePassStatement == null) { len = 0; offset = offsetAndIndent.o1; if (requireEmptyLines) { int checkLine = pySelection.getLineOfOffset(offset); int lineOffset = pySelection.getLineOffset(checkLine); //Make sure we have 2 spaces from the last thing written. if (lineOffset == offset) { //it'll be added to the start of the line, so, we have to analyze the previous line to know if we'll need //to new lines at the start. checkLine--; } if (checkLine >= 0) { //It'll be added to the current line, so, check the current line and the previous line to know about spaces. String line = pySelection.getLine(checkLine); if (line.trim().length() >= 1) { source = "\n\n" + source; } else if (checkLine > 1) { line = pySelection.getLine(checkLine - 1); if (line.trim().length() > 0) { source = "\n" + source; } } } //If we have a '\n', all is OK (all contents after a \n will be indented) if (!source.startsWith("\n")) { try { //Ok, it doesn't start with a \n, that means we have to check the line indentation where it'll //be added and make sure things are correct (eventually adding a new line or just fixing the indent). String lineContentsToCursor = pySelection.getLineContentsToCursor(offset); if (lineContentsToCursor.length() > 0) { source = "\n" + source; } else { source = indent + source; } } catch (BadLocationException e) { source = "\n" + source; } } } } else { offset = pySelection.getAbsoluteCursorOffset(replacePassStatement.beginLine - 1, replacePassStatement.beginColumn - 1); len = 4; //pass.len if (requireEmptyLines) { source = "\n\n" + source; } } if (targetEditor != null) { String creationStr = getCreationStr(); Region region = new Region(offset, len); //Note: was using new PyContextType(), but when we had something as ${user} it //would end up replacing it with the actual name of the user, which is not what //we want! TemplateContextType contextType = new TemplateContextType(); contextType.addResolver(new GlobalTemplateVariables.Cursor()); //We do want the cursor thought. PyDocumentTemplateContext context = PyTemplateCompletionProcessor.createContext(contextType, targetEditor.getPySourceViewer(), region, indent); Template template = new Template("Create " + creationStr, "Create " + creationStr, "", source, true); TemplateProposal templateProposal = new TemplateProposal(template, context, region, null); return templateProposal; } else { //This should only happen in tests. source = StringUtils.indentTo(source, indent, false); return new CompletionProposal(source, offset, len, 0); } } /** * @return the offset and the indent to be used. */ protected Tuple<Integer, String> getLocationOffset(int locationStrategy, PySelection pySelection, ModuleAdapter moduleAdapter, IClassDefAdapter targetClass) { Assert.isNotNull(targetClass); int offset; IOffsetStrategy strategy; try { switch (locationStrategy) { case LOCATION_STRATEGY_BEFORE_CURRENT: String currentLine = pySelection.getLine(); int firstCharPosition = PySelection.getFirstCharPosition(currentLine); LineStartingScope scopeStart = pySelection.getPreviousLineThatStartsScope( PySelection.CLASS_AND_FUNC_TOKENS, false, firstCharPosition); if (scopeStart == null) { Log.log("Could not get proper scope to create code inside class."); ClassDef astNode = targetClass.getASTNode(); if (astNode.body.length > 0) { offset = NodeUtils.getLineEnd(astNode.body[astNode.body.length - 1]); } else { offset = NodeUtils.getLineEnd(astNode); } } else { int iLineStartingScope = scopeStart.iLineStartingScope; String line = pySelection.getLine(iLineStartingScope); if (PySelection.matchesFunctionLine(line) || PySelection.matchesClassLine(line)) { //check for decorators... if (iLineStartingScope > 0) { int i = iLineStartingScope - 1; while (pySelection.getLine(i).trim().startsWith("@")) { iLineStartingScope = i; i--; } } } offset = pySelection.getLineOffset(iLineStartingScope); } break; case LOCATION_STRATEGY_END: strategy = new EndOffset(targetClass, pySelection.getDoc(), moduleAdapter.getAdapterPrefs()); offset = strategy.getOffset(); break; case LOCATION_STRATEGY_FIRST_METHOD: strategy = new BeginOffset(targetClass, pySelection.getDoc(), moduleAdapter.getAdapterPrefs()); offset = strategy.getOffset(); break; default: throw new AssertionError("Unknown location strategy: " + locationStrategy); } String nodeBodyIndent = targetClass.getNodeBodyIndent(); return new Tuple<Integer, String>(offset, nodeBodyIndent); } catch (BadLocationException e) { throw new RuntimeException(e); } } protected Tuple<Integer, String> getLocationOffset(int locationStrategy, PySelection pySelection, ModuleAdapter moduleAdapter) throws AssertionError { int offset; switch (locationStrategy) { case LOCATION_STRATEGY_BEFORE_CURRENT: int lastNodeFirstLineBefore = moduleAdapter.getLastNodeFirstLineBefore(pySelection.getCursorLine() + 1); lastNodeFirstLineBefore = lastNodeFirstLineBefore - 1; // From AST line to doc line int line = lastNodeFirstLineBefore; if (line > 0) { try { String trimmed = pySelection.getLine(line).trim(); if (trimmed.startsWith("class") || trimmed.startsWith("def")) { //if we'll add to a class or def line, let's see if there are comments just above it //(in this case, we'll go backwards in the file until the block comment ends) //i.e.: //#====================== //# Existing //#====================== //class Existing(object): <-- currently at this line // passMyClass() int curr = line; while (curr >= 0) { line = curr; if (curr - 1 < 0) { break; } if (!pySelection.getLine(curr - 1).trim().startsWith("#")) { break; } curr--; } } } catch (Exception e) { Log.log(e); } } offset = pySelection.getLineOffset(line); break; case LOCATION_STRATEGY_END: offset = pySelection.getEndOfDocummentOffset(); break; default: throw new AssertionError("Unknown location strategy: " + locationStrategy); } return new Tuple<Integer, String>(offset, ""); } public static FastStringBuffer createParametersList(List<String> parametersAfterCall) { FastStringBuffer params = new FastStringBuffer(parametersAfterCall.size() * 10); AssistAssign assistAssign = new AssistAssign(); for (int i = 0; i < parametersAfterCall.size(); i++) { String param = parametersAfterCall.get(i).trim(); if (params.length() > 0) { params.append(", "); } String tok = null; if (param.indexOf('=') != -1) { List<String> split = StringUtils.split(param, '='); if (split.size() > 0) { String part0 = split.get(0).trim(); if (PyStringUtils.isPythonIdentifier(part0)) { tok = part0; } } } if (tok == null) { if (PyStringUtils.isPythonIdentifier(param)) { tok = param; } else { tok = assistAssign.getTokToAssign(param); if (tok == null || tok.length() == 0) { tok = "param" + i; } } } boolean addTag = !(i == 0 && (tok.equals("cls") || tok.equals("self"))); if (addTag) { params.append("${"); } params.append(tok); if (addTag) { params.append("}"); } } return params; } }