/** * 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 org.python.pydev.debug.ui.actions; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import org.eclipse.core.resources.IProject; import org.eclipse.core.runtime.CoreException; import org.eclipse.debug.core.ILaunchConfiguration; import org.eclipse.debug.core.ILaunchConfigurationWorkingCopy; import org.eclipse.jface.action.IAction; import org.eclipse.jface.viewers.IStructuredSelection; import org.eclipse.jface.viewers.ITreeContentProvider; import org.eclipse.jface.viewers.LabelProvider; import org.eclipse.jface.viewers.Viewer; import org.eclipse.jface.window.Window; import org.eclipse.swt.SWT; import org.eclipse.swt.events.SelectionAdapter; import org.eclipse.swt.events.SelectionEvent; import org.eclipse.swt.events.TraverseEvent; import org.eclipse.swt.events.TraverseListener; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.graphics.Point; import org.eclipse.swt.layout.GridData; import org.eclipse.swt.layout.GridLayout; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Event; import org.eclipse.swt.widgets.Label; import org.eclipse.swt.widgets.Link; import org.eclipse.swt.widgets.Listener; import org.eclipse.swt.widgets.Shell; import org.eclipse.swt.widgets.Tree; import org.eclipse.swt.widgets.TreeItem; import org.python.pydev.core.IInterpreterManager; import org.python.pydev.core.IToken; import org.python.pydev.core.docutils.PySelection; import org.python.pydev.core.docutils.PyStringUtils; import org.python.pydev.debug.core.Constants; import org.python.pydev.debug.ui.launching.AbstractLaunchShortcut; import org.python.pydev.debug.ui.launching.FileOrResource; import org.python.pydev.editor.PyEdit; import org.python.pydev.editor.codecompletion.PyCodeCompletionImages; import org.python.pydev.parser.jython.SimpleNode; import org.python.pydev.parser.jython.ast.ClassDef; import org.python.pydev.parser.jython.ast.FunctionDef; import org.python.pydev.parser.visitors.NodeUtils; import org.python.pydev.parser.visitors.scope.ASTEntry; import org.python.pydev.parser.visitors.scope.EasyASTIteratorVisitor; import org.python.pydev.pyunit.preferences.PyUnitPrefsPage2; import org.python.pydev.shared_core.callbacks.CallbackWithListeners; import org.python.pydev.shared_core.callbacks.ICallbackListener; import org.python.pydev.shared_core.string.FastStringBuffer; import org.python.pydev.shared_core.structure.Tuple; import org.python.pydev.shared_ui.EditorUtils; import org.python.pydev.shared_ui.dialogs.DialogMemento; import org.python.pydev.ui.dialogs.TreeSelectionDialog; class ShiftListener implements Listener { public boolean shiftPressed = false; public CallbackWithListeners<Boolean> onChanged = new CallbackWithListeners<>(); public ShiftListener() { } @Override public void handleEvent(Event event) { if (event.keyCode == SWT.SHIFT) { if (event.type == SWT.KeyDown) { shiftPressed = true; onChanged.call(shiftPressed); } else if (event.type == SWT.KeyUp) { shiftPressed = false; onChanged.call(shiftPressed); } } } } public class RunEditorAsCustomUnitTestAction extends AbstractRunEditorAction { @Override public void run(IAction action) { PyEdit pyEdit = getPyEdit(); final Tuple<String, IInterpreterManager> launchConfigurationTypeAndInterpreterManager = this .getLaunchConfigurationTypeAndInterpreterManager(pyEdit, true); Shell shell = EditorUtils.getShell(); final DialogMemento memento = new DialogMemento(shell, "org.python.pydev.debug.ui.actions.RunEditorAsCustomUnitTestAction"); SimpleNode ast = pyEdit.getAST(); final ShiftListener shiftListener = new ShiftListener(); Display d = shell.getDisplay(); d.addFilter(SWT.KeyDown, shiftListener); d.addFilter(SWT.KeyUp, shiftListener); try { TreeSelectionDialog dialog = new TreeSelectionDialog(shell, new SelectTestLabelProvider(), new SelectTestTreeContentProvider(pyEdit)) { private Label labelShiftToDebug; @Override public boolean close() { memento.writeSettings(getShell()); return super.close(); } @Override public Control createDialogArea(Composite parent) { memento.readSettings(); Control ret = super.createDialogArea(parent); ret.addTraverseListener(new TraverseListener() { @Override public void keyTraversed(TraverseEvent e) { if (e.detail == SWT.TRAVERSE_RETURN) { okPressed(); } } }); return ret; } /* (non-Javadoc) * @see org.python.pydev.ui.dialogs.TreeSelectionDialog#createButtonBar(org.eclipse.swt.widgets.Composite) */ @Override protected Control createButtonBar(Composite parent) { Composite buttonBar = new Composite(parent, 0); GridLayout layout = new GridLayout(); layout.numColumns = 2; buttonBar.setLayout(layout); GridData data = new GridData(); data.horizontalAlignment = SWT.FILL; data.grabExcessHorizontalSpace = true; buttonBar.setLayoutData(data); Link configTestRunner = new Link(buttonBar, SWT.PUSH); configTestRunner.setText(" <a>Configure test runner</a>"); configTestRunner.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { PyUnitPrefsPage2.showPage(); } }); data = new GridData(); data.horizontalAlignment = GridData.BEGINNING; data.grabExcessHorizontalSpace = true; configTestRunner.setLayoutData(data); labelShiftToDebug = new Label(buttonBar, 0); labelShiftToDebug.setText("Run: Normal (Press Shift to Debug)"); data = new GridData(); data.horizontalAlignment = GridData.END; data.grabExcessHorizontalSpace = true; labelShiftToDebug.setLayoutData(data); shiftListener.onChanged.registerListener(new ICallbackListener<Boolean>() { @Override public Object call(Boolean shiftPressed) { if (shiftPressed) { labelShiftToDebug.setText("Run: Debug (Release Shift for Normal)"); } else { labelShiftToDebug.setText("Run: Normal (Press Shift to Debug)"); } labelShiftToDebug.getParent().layout(true); return null; } }); return buttonBar; } @Override protected Point getInitialSize() { return memento.getInitialSize(super.getInitialSize(), getShell()); } @Override protected Point getInitialLocation(Point initialSize) { return memento.getInitialLocation(initialSize, super.getInitialLocation(initialSize), getShell()); } /* * @see SelectionStatusDialog#computeResult() */ @Override @SuppressWarnings("unchecked") protected void computeResult() { doFinalUpdateBeforeComputeResult(); IStructuredSelection selection = (IStructuredSelection) getTreeViewer().getSelection(); List<Object> list = selection.toList(); if (list.size() > 0) { setResult(list); } else { Tree tree = getTreeViewer().getTree(); TreeItem[] items = tree.getItems(); list = new ArrayList<Object>(); //Now, if he didn't select anything, let's create tests with all that is currently filtered //in the interface createListWithLeafs(items, list); setResult(list); } } private void createListWithLeafs(TreeItem[] items, List<Object> leafObjectsList) { for (TreeItem item : items) { TreeItem[] children = item.getItems(); if (children.length == 0) { leafObjectsList.add(item.getData()); } else { createListWithLeafs(children, leafObjectsList); } } } }; dialog.setTitle("PyDev: Select tests to run"); dialog.setMessage("Select the tests to run (press enter to run tests shown/selected)"); PySelection ps = pyEdit.createPySelection(); String selectedText = ps.getSelectedText(); if (selectedText.length() > 0 && PyStringUtils.isValidIdentifier(selectedText, false)) { dialog.setInitialFilter(selectedText + " "); //Space in the end == exact match } else { dialog.setInitialFilter("test"); } dialog.setAllowMultiple(true); dialog.setInput(ast); int open = dialog.open(); if (open != Window.OK) { return; } Object[] result = dialog.getResult(); final FastStringBuffer buf = new FastStringBuffer(); if (result != null && result.length > 0) { for (Object o : result) { ASTEntry entry = (ASTEntry) o; if (entry.node instanceof ClassDef) { if (buf.length() > 0) { buf.append(','); } buf.append(NodeUtils.getFullRepresentationString(entry.node)); } else if (entry.node instanceof FunctionDef && entry.parent == null) { if (buf.length() > 0) { buf.append(','); } buf.append(NodeUtils.getFullRepresentationString(entry.node)); } else if (entry.node instanceof FunctionDef && entry.parent != null && entry.parent.node instanceof ClassDef) { if (buf.length() > 0) { buf.append(','); } buf.append(NodeUtils.getFullRepresentationString(entry.parent.node)); buf.append('.'); buf.append(NodeUtils.getFullRepresentationString(entry.node)); } } } final String arguments; if (buf.length() > 0) { arguments = buf.toString(); } else { arguments = ""; } AbstractLaunchShortcut shortcut = new AbstractLaunchShortcut() { @Override protected String getLaunchConfigurationType() { return launchConfigurationTypeAndInterpreterManager.o1; } @Override protected IInterpreterManager getInterpreterManager(IProject project) { return launchConfigurationTypeAndInterpreterManager.o2; } @Override public ILaunchConfigurationWorkingCopy createDefaultLaunchConfigurationWithoutSaving( FileOrResource[] resource) throws CoreException { ILaunchConfigurationWorkingCopy workingCopy = super.createDefaultLaunchConfigurationWithoutSaving( resource); if (arguments.length() > 0) { workingCopy.setAttribute(Constants.ATTR_UNITTEST_TESTS, arguments); } return workingCopy; } @Override protected List<ILaunchConfiguration> findExistingLaunchConfigurations(FileOrResource[] file) { List<ILaunchConfiguration> ret = new ArrayList<ILaunchConfiguration>(); List<ILaunchConfiguration> existing = super.findExistingLaunchConfigurations(file); for (ILaunchConfiguration launch : existing) { boolean matches = false; try { matches = launch.getAttribute(Constants.ATTR_UNITTEST_TESTS, "").equals(arguments); } catch (CoreException e) { //ignore } if (matches) { ret.add(launch); } } return ret; } }; if (shiftListener.shiftPressed) { shortcut.launch(pyEdit, "debug"); } else { shortcut.launch(pyEdit, "run"); } } finally { d.removeFilter(SWT.KeyDown, shiftListener); d.removeFilter(SWT.KeyUp, shiftListener); } } } final class SelectTestLabelProvider extends LabelProvider { @Override public Image getImage(Object element) { SimpleNode n = ((ASTEntry) element).node; if (n instanceof ClassDef) { return PyCodeCompletionImages.getImageForType(IToken.TYPE_CLASS); } if (n instanceof FunctionDef) { return PyCodeCompletionImages.getImageForType(IToken.TYPE_FUNCTION); } return PyCodeCompletionImages.getImageForType(IToken.TYPE_ATTR); } @Override public String getText(Object element) { return NodeUtils.getFullRepresentationString(((ASTEntry) element).node); } } final class SelectTestTreeContentProvider implements ITreeContentProvider { private EasyASTIteratorVisitor visitor; private Map<Object, ASTEntry[]> cache = new HashMap<Object, ASTEntry[]>(); private PyEdit pyEdit; public SelectTestTreeContentProvider(PyEdit pyEdit) { this.pyEdit = pyEdit; } @Override public Object[] getChildren(Object element) { Object[] ret = cache.get(element); if (ret != null) { return ret; } ASTEntry entry = (ASTEntry) element; //Only get classes and 1st level methods for tests. if (entry.node instanceof ClassDef) { Iterator<ASTEntry> it = visitor.getMethodsIterator(); ArrayList<ASTEntry> list = new ArrayList<ASTEntry>(); while (it.hasNext()) { ASTEntry next = it.next(); if (next.parent != null && next.parent.node == entry.node) { list.add(next); } } ASTEntry[] array = list.toArray(new ASTEntry[0]); cache.put(element, array); return array; } return null; } @Override public Object getParent(Object element) { ASTEntry entry = (ASTEntry) element; return entry.parent; } @Override public boolean hasChildren(Object element) { ASTEntry entry = (ASTEntry) element; if (entry.node instanceof ClassDef) { Object[] children = getChildren(entry); return children != null && children.length > 0; } return false; } @Override public Object[] getElements(Object inputElement) { visitor = EasyASTIteratorVisitor.create((SimpleNode) inputElement); if (visitor == null) { return new Object[0]; } //Get the top-level classes Iterator<ASTEntry> it = visitor.getClassesIterator(); ArrayList<ASTEntry> list = new ArrayList<ASTEntry>(); while (it.hasNext()) { ASTEntry next = it.next(); if (next.parent == null) { list.add(next); } } if (PyUnitPrefsPage2.isPyTestRun(this.pyEdit)) { // We'll only add methods which are top-level when in the py.test run (which accepts those, as // the regular unit-test runner doesn't accept it). it = visitor.getMethodsIterator(); while (it.hasNext()) { ASTEntry next = it.next(); if (next.parent == null) { list.add(next); } } } return list.toArray(new ASTEntry[0]); } @Override public void dispose() { //do nothing } @Override public void inputChanged(Viewer viewer, Object oldInput, Object newInput) { //do nothing } }