Lines 39-55
Link Here
|
39 |
* |
39 |
* |
40 |
* Portions Copyrighted 2012 Sun Microsystems, Inc. |
40 |
* Portions Copyrighted 2012 Sun Microsystems, Inc. |
41 |
*/ |
41 |
*/ |
42 |
|
|
|
43 |
package org.netbeans.modules.groovy.support.actions; |
42 |
package org.netbeans.modules.groovy.support.actions; |
44 |
|
43 |
|
|
|
44 |
import java.awt.EventQueue; |
45 |
import java.io.IOException; |
45 |
import java.io.IOException; |
46 |
import java.lang.ref.Reference; |
46 |
import java.io.LineNumberReader; |
47 |
import java.util.concurrent.Future; |
47 |
import java.io.StringReader; |
48 |
import java.util.logging.Level; |
48 |
import java.lang.reflect.InvocationTargetException; |
49 |
import java.util.logging.Logger; |
49 |
import java.lang.reflect.Method; |
|
|
50 |
import java.util.Collections; |
51 |
import java.util.List; |
52 |
import java.util.concurrent.atomic.AtomicInteger; |
53 |
import java.util.concurrent.atomic.AtomicReference; |
50 |
import javax.swing.JEditorPane; |
54 |
import javax.swing.JEditorPane; |
51 |
import javax.swing.text.Document; |
55 |
import javax.swing.text.Document; |
52 |
import org.netbeans.api.java.source.JavaSource; |
56 |
import org.codehaus.groovy.ast.ClassNode; |
|
|
57 |
import org.codehaus.groovy.ast.MethodNode; |
58 |
import org.codehaus.groovy.ast.ModuleNode; |
59 |
import org.netbeans.modules.parsing.api.ParserManager; |
60 |
import org.netbeans.modules.parsing.api.ResultIterator; |
61 |
import org.netbeans.modules.parsing.api.Source; |
62 |
import org.netbeans.modules.parsing.api.UserTask; |
63 |
import org.netbeans.modules.parsing.spi.ParseException; |
64 |
import org.netbeans.modules.parsing.spi.Parser.Result; |
53 |
import org.netbeans.spi.project.SingleMethod; |
65 |
import org.netbeans.spi.project.SingleMethod; |
54 |
import org.openide.cookies.EditorCookie; |
66 |
import org.openide.cookies.EditorCookie; |
55 |
import org.openide.filesystems.FileObject; |
67 |
import org.openide.filesystems.FileObject; |
Lines 58-63
Link Here
|
58 |
import org.openide.loaders.DataObject; |
70 |
import org.openide.loaders.DataObject; |
59 |
import org.openide.nodes.Node; |
71 |
import org.openide.nodes.Node; |
60 |
import org.openide.text.NbDocument; |
72 |
import org.openide.text.NbDocument; |
|
|
73 |
import org.openide.util.Parameters; |
61 |
|
74 |
|
62 |
/** |
75 |
/** |
63 |
* |
76 |
* |
Lines 65-101
Link Here
|
65 |
*/ |
78 |
*/ |
66 |
public final class TestMethodUtil { |
79 |
public final class TestMethodUtil { |
67 |
|
80 |
|
68 |
private static final Logger LOGGER = Logger.getLogger(TestMethodUtil.class.getName()); |
81 |
private static final String GROOVY_PARSER_RESULT_CLASS_NAME = "org.netbeans.modules.groovy.editor.api.parser.GroovyParserResult"; |
69 |
private Reference<JavaSource> resolver; |
82 |
|
|
|
83 |
private static final String GPR_GET_ROOT_ELEMENT = "getRootElement"; |
84 |
|
85 |
private static final String AST_GET_MODULE_NODE = "getModuleNode"; |
70 |
|
86 |
|
71 |
static boolean isTestClass(Node activatedNode) { |
87 |
static boolean isTestClass(Node activatedNode) { |
72 |
FileObject fo = getFileObjectFromNode(activatedNode); |
88 |
FileObject fo = getFileObjectFromNode(activatedNode); |
73 |
if (fo != null) { |
89 |
if (fo != null) { |
74 |
if (!isGroovyFile(fo)) { |
|
|
75 |
return false; |
76 |
} |
77 |
//TODO add more checks here when action gets enabled? |
90 |
//TODO add more checks here when action gets enabled? |
|
|
91 |
return isGroovyFile(fo); |
78 |
} |
92 |
} |
79 |
return false; |
93 |
return false; |
80 |
} |
94 |
} |
81 |
|
95 |
|
82 |
static SingleMethod getTestMethod(Document doc, int cursor){ |
96 |
/** |
83 |
SingleMethod sm = null; |
97 |
* Given the text from a Document, will read through the lines, adding 1 to |
84 |
if (doc != null){ |
98 |
* a counter for each line, plus the length of the given line until the |
85 |
JavaSource js = JavaSource.forDocument(doc); |
99 |
* value is greater than or equal to the cursor, and this gives us the line. |
86 |
GroovyTestClassInfoTask task = new GroovyTestClassInfoTask(cursor); |
100 |
* Next, the offset in the line which would make the counter equal to the |
|
|
101 |
* cursor value is taken to represent the column. This is done to map to the |
102 |
* Groovy AST nodes line and columns to match to classes and methods. The |
103 |
* return from this method can be used to find the class and method the |
104 |
* cursor is in. Remember, the Groovy lines and columns are index 1 with |
105 |
* relation to the AST node classes. |
106 |
* |
107 |
* @param srcText the Groovy source as represented in the edited Groovy |
108 |
* document; it must be this text as the expectation is a line will always |
109 |
* end in a \n |
110 |
* @param cursor the cursor offset in the given text. |
111 |
* @return the line and column of the cursor offset in the text as an |
112 |
* integer array such that index 0 is the line, and index 1 is the column |
113 |
*/ |
114 |
public static int[] getLineAndColumn(final String srcText, final int cursor) { |
115 |
Parameters.notNull("srcText", srcText); |
116 |
Parameters.notEmpty("srcText", srcText); |
117 |
int[] ret = new int[]{-1, -1}; |
118 |
if (cursor > 0) { |
87 |
try { |
119 |
try { |
88 |
if (js != null) { |
120 |
final StringReader sr = new StringReader(srcText); |
89 |
Future<Void> f = js.runWhenScanFinished(task, true); |
121 |
final LineNumberReader lr = new LineNumberReader(sr); |
90 |
if (f.isDone() && task.getFileObject() != null && task.getMethodName() != null){ |
122 |
int counter = 0; |
91 |
sm = new SingleMethod(task.getFileObject(), task.getMethodName()); |
123 |
String line; |
|
|
124 |
while ((line = lr.readLine()) != null) { |
125 |
//remember, we went over a line, and it has a \n |
126 |
counter += line.length(); |
127 |
if (counter >= cursor) { |
128 |
//lr incs line number at every line it reads, |
129 |
//and started at 0 |
130 |
ret[0] = lr.getLineNumber(); |
131 |
//if we take away what we just added, then take the whole |
132 |
//away from the cursor, then we are left with the number |
133 |
//it takes to equal the cursor, and then since the chars |
134 |
//and positions in the line are 0 indexed, we need to add |
135 |
//1; if 0 or 0 length, then that should also work out |
136 |
//correctly as if the cursor is on the line, it is in |
137 |
//column 1 |
138 |
ret[1] = (cursor - (counter - line.length())) + 1; |
139 |
break; |
92 |
} |
140 |
} |
|
|
141 |
//account for the newline now as we don't care about it |
142 |
//until we are done with the "current" line. The extra value |
143 |
//will throw off the above calculation if added too soon, but |
144 |
//if not added at all will also throw it off. So, add it here. |
145 |
counter++; |
93 |
} |
146 |
} |
94 |
} catch (IOException ex) { |
147 |
} catch (IOException ex) { |
95 |
LOGGER.log(Level.WARNING, null, ex); |
148 |
throw new RuntimeException(ex); |
|
|
149 |
} |
150 |
} else if (cursor == 0) { |
151 |
//first line and start of first line |
152 |
ret[0] = 1; |
153 |
ret[1] = 1; |
154 |
} |
155 |
return ret; |
156 |
} |
157 |
|
158 |
/** |
159 |
* Given a start and end line and a start and end column plus a given line |
160 |
* and column, tests if the given line and column falls between the start |
161 |
* and end values in relation to Groovy code and the Groovy AST. |
162 |
* |
163 |
* @param startLine the start line |
164 |
* @param startCol the start column |
165 |
* @param endLine the end line |
166 |
* @param endCol the end column |
167 |
* @param line the line to test |
168 |
* @param col the column to test |
169 |
* @return whether the line and column fall between the given start and end |
170 |
* values |
171 |
*/ |
172 |
public static boolean isBetweenLinesAndColumns(final int startLine, |
173 |
final int startCol, |
174 |
final int endLine, |
175 |
final int endCol, |
176 |
final int line, |
177 |
final int col) { |
178 |
boolean ret = false; |
179 |
if (line >= startLine && line <= endLine) { |
180 |
//at the momment we are approximately |
181 |
//in the method bounds, so we'll assume |
182 |
//true, then adjust accordingly |
183 |
//as this is simpler than embedded ifs |
184 |
//and logic as we only care about the col |
185 |
//if we are on one of the bounding lines |
186 |
ret = true; |
187 |
|
188 |
//even though above we say true, we may take it back |
189 |
//if on the same line as the start line and not in |
190 |
//the method bounds |
191 |
if (line == startLine && !(col >= startCol)) { |
192 |
ret = false; |
193 |
} |
194 |
|
195 |
//even though above we may say true, we may take it back |
196 |
//if on the same line as the end line and not in |
197 |
//the method bounds |
198 |
if (line == endLine && !(col <= endCol)) { |
199 |
ret = false; |
96 |
} |
200 |
} |
97 |
} |
201 |
} |
98 |
return sm; |
202 |
return ret; |
|
|
203 |
} |
204 |
|
205 |
/** |
206 |
* Gets the Groovy AST ModuleNode associated with the given Source result. |
207 |
* This is currently done with reflection due to the dependency graph of |
208 |
* "Groovy Support" and "Groovy Editor". This is hopefully just temporary |
209 |
* until the modules can be refactored into a better DAG. |
210 |
* |
211 |
* @param r the Result to extract from |
212 |
* @return the ModuleNode if the Result is in fact a "GroovyParserResult", |
213 |
* the sources have been parsed, and the {@link ModuleNode ModuleNode} has |
214 |
* been set. If not set or the wrong "Result" time, then null will be |
215 |
* returned. |
216 |
*/ |
217 |
public static ModuleNode extractModuleNode(final Result r) { |
218 |
//below line long to show up properly in enumerations |
219 |
//TODO TestMethodUtil.extractModuleNode refactor "Groovy Support" and "Groovy Editor" to have a better DAG to remove need to use reflection to extract Groovy ModuleNode |
220 |
ModuleNode ret = null; |
221 |
try { |
222 |
//no need to test result type as it will have the method or it won't |
223 |
//and this makes it easier to test |
224 |
final Method getRE = r.getClass().getMethod(GPR_GET_ROOT_ELEMENT); |
225 |
final Object astRoot = getRE.invoke(r); |
226 |
if (astRoot != null) { |
227 |
final Method getMN = astRoot.getClass().getMethod(AST_GET_MODULE_NODE); |
228 |
final ModuleNode lmn = ModuleNode.class.cast(getMN.invoke(astRoot)); |
229 |
ret = lmn; |
230 |
} |
231 |
} catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { |
232 |
//push it up to the NB caller |
233 |
throw new RuntimeException("The given result doesn't appear to come from parsing a Groovy file.", e); |
234 |
} |
235 |
return ret; |
236 |
} |
237 |
|
238 |
/** |
239 |
* Given a root {@link ModuleNode ModuleNode}, finds and returns the |
240 |
* {@link ClassNode ClassNode} for the given line and column. It is possible |
241 |
* in some situations for this to be null. |
242 |
* |
243 |
* @param root the root ModuleNode |
244 |
* @param line the line |
245 |
* @param col the column |
246 |
* @return the ClassNode for the line and column (cursor) |
247 |
*/ |
248 |
public static ClassNode getClassNodeForLineAndColumn(final ModuleNode root, |
249 |
final int line, |
250 |
final int col) { |
251 |
ClassNode ret = null; |
252 |
if (root != null) { |
253 |
final List<ClassNode> classes = root.getClasses(); |
254 |
for (ClassNode cn : classes) { |
255 |
if (isBetweenLinesAndColumns(cn.getLineNumber(), cn.getColumnNumber(), |
256 |
cn.getLastLineNumber(), cn.getLastColumnNumber(), |
257 |
line, col)) { |
258 |
return cn; |
259 |
} |
260 |
} |
261 |
} |
262 |
return ret; |
263 |
} |
264 |
|
265 |
/** |
266 |
* Given a {@link ClassNode ClassNode}, finds and returns the |
267 |
* {@link MethodNode MethodNode} for the given line and column. It is |
268 |
* possible in some situations for this to be null. |
269 |
* |
270 |
* @param cn the ClassNode |
271 |
* @param line the line |
272 |
* @param col the column |
273 |
* @return the MethodNode for the line and column (cursor) |
274 |
*/ |
275 |
public static MethodNode getMethodNodeForLineAndColumn(final ClassNode cn, |
276 |
final int line, |
277 |
final int col) { |
278 |
MethodNode ret = null; |
279 |
if (cn != null) { |
280 |
final List<MethodNode> methods = cn.getMethods(); |
281 |
for (MethodNode mn : methods) { |
282 |
if (isBetweenLinesAndColumns(mn.getLineNumber(), mn.getColumnNumber(), |
283 |
mn.getLastLineNumber(), mn.getLastColumnNumber(), |
284 |
line, col)) { |
285 |
return mn; |
286 |
} |
287 |
} |
288 |
} |
289 |
return ret; |
290 |
} |
291 |
|
292 |
public static SingleMethod getTestMethod(final Document doc, final int cursor) { |
293 |
final AtomicReference<SingleMethod> sm = new AtomicReference<>(); |
294 |
if (doc != null) { |
295 |
|
296 |
Source s = Source.create(doc); |
297 |
try { |
298 |
ParserManager.parseWhenScanFinished(Collections.<Source>singleton(s), new UserTask() { |
299 |
@Override |
300 |
public void run(ResultIterator rit) throws Exception { |
301 |
Result r = rit.getParserResult(); |
302 |
//0:line, 1:column |
303 |
final int[] lc = getLineAndColumn(doc.getText(0, doc.getLength()), cursor); |
304 |
final int line = lc[0]; |
305 |
final int col = lc[1]; |
306 |
final ModuleNode root = extractModuleNode(r); |
307 |
final ClassNode cn = getClassNodeForLineAndColumn(root, line, col); |
308 |
final MethodNode mn = getMethodNodeForLineAndColumn(cn, line, col); |
309 |
if (mn != null) { |
310 |
final SingleMethod lsm = new SingleMethod(s.getFileObject(), mn.getName()); |
311 |
sm.set(lsm); |
312 |
} |
313 |
} |
314 |
|
315 |
}); |
316 |
} catch (ParseException e) { |
317 |
throw new RuntimeException(e); |
318 |
} |
319 |
} |
320 |
return sm.get(); |
99 |
} |
321 |
} |
100 |
|
322 |
|
101 |
static boolean canHandle(Node activatedNode) { |
323 |
static boolean canHandle(Node activatedNode) { |
Lines 105-116
Link Here
|
105 |
return false; |
327 |
return false; |
106 |
} |
328 |
} |
107 |
|
329 |
|
108 |
EditorCookie ec = activatedNode.getLookup().lookup(EditorCookie.class); |
330 |
final EditorCookie ec = activatedNode.getLookup().lookup(EditorCookie.class); |
109 |
if (ec != null) { |
331 |
if (ec != null) { |
110 |
JEditorPane pane = NbDocument.findRecentEditorPane(ec); |
332 |
final AtomicReference<Document> doc = new AtomicReference<>(); |
111 |
if (pane != null) { |
333 |
final AtomicInteger dot = new AtomicInteger(); |
112 |
SingleMethod sm = getTestMethod(pane.getDocument(), pane.getCaret().getDot()); |
334 |
try { |
113 |
if(sm != null) { |
335 |
EventQueue.invokeAndWait(() -> { |
|
|
336 |
final JEditorPane pane = NbDocument.findRecentEditorPane(ec); |
337 |
if (pane != null) { |
338 |
doc.set(pane.getDocument()); |
339 |
dot.set(pane.getCaret().getDot()); |
340 |
} |
341 |
}); |
342 |
} catch (InterruptedException | InvocationTargetException e) { |
343 |
throw new RuntimeException(e); |
344 |
} |
345 |
if (doc.get() != null) { |
346 |
SingleMethod sm = getTestMethod(doc.get(), dot.get()); |
347 |
if (sm != null) { |
114 |
return true; |
348 |
return true; |
115 |
} |
349 |
} |
116 |
} |
350 |
} |
Lines 136-141
Link Here
|
136 |
} |
370 |
} |
137 |
|
371 |
|
138 |
private static boolean isGroovyFile(FileObject fileObj) { |
372 |
private static boolean isGroovyFile(FileObject fileObj) { |
139 |
return "groovy".equals(fileObj.getExt()) || "text/x-groovy".equals(FileUtil.getMIMEType(fileObj)); //NOI18N |
373 |
final String ext = fileObj.getExt(); |
|
|
374 |
final String mtype = FileUtil.getMIMEType(fileObj); |
375 |
return "groovy".equals(ext) || "text/x-groovy".equals(mtype); //NOI18N |
140 |
} |
376 |
} |
141 |
} |
377 |
} |