Unify line separators in documentation for Python keywords
[idea/community.git] / python / src / com / jetbrains / python / documentation / PyDocumentationBuilder.java
1 /*
2  * Copyright 2000-2014 JetBrains s.r.o.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.jetbrains.python.documentation;
17
18 import com.intellij.lang.ASTNode;
19 import com.intellij.openapi.module.Module;
20 import com.intellij.openapi.project.Project;
21 import com.intellij.openapi.projectRoots.Sdk;
22 import com.intellij.openapi.util.Pair;
23 import com.intellij.openapi.util.io.FileUtil;
24 import com.intellij.openapi.util.text.LineTokenizer;
25 import com.intellij.openapi.util.text.StringUtil;
26 import com.intellij.openapi.vfs.VfsUtilCore;
27 import com.intellij.openapi.vfs.VirtualFile;
28 import com.intellij.psi.PsiElement;
29 import com.intellij.psi.codeStyle.CodeStyleSettingsManager;
30 import com.intellij.psi.util.PsiTreeUtil;
31 import com.intellij.util.ArrayUtil;
32 import com.jetbrains.python.*;
33 import com.jetbrains.python.console.PyConsoleUtil;
34 import com.jetbrains.python.documentation.docstrings.DocStringUtil;
35 import com.jetbrains.python.documentation.docstrings.PyStructuredDocstringFormatter;
36 import com.jetbrains.python.psi.*;
37 import com.jetbrains.python.psi.impl.PyBuiltinCache;
38 import com.jetbrains.python.psi.impl.PyCallExpressionHelper;
39 import com.jetbrains.python.psi.impl.PyPsiUtils;
40 import com.jetbrains.python.psi.resolve.PyResolveContext;
41 import com.jetbrains.python.psi.resolve.QualifiedResolveResult;
42 import com.jetbrains.python.psi.resolve.RootVisitor;
43 import com.jetbrains.python.psi.resolve.RootVisitorHost;
44 import com.jetbrains.python.psi.types.*;
45 import com.jetbrains.python.toolbox.ChainIterable;
46 import com.jetbrains.python.toolbox.Maybe;
47 import org.jetbrains.annotations.NotNull;
48 import org.jetbrains.annotations.Nullable;
49
50 import java.io.FileNotFoundException;
51 import java.io.FileReader;
52 import java.io.IOException;
53 import java.util.ArrayList;
54 import java.util.List;
55 import java.util.regex.Matcher;
56 import java.util.regex.Pattern;
57
58 import static com.jetbrains.python.documentation.DocumentationBuilderKit.*;
59
60 public class PyDocumentationBuilder {
61   private final PsiElement myElement;
62   private final PsiElement myOriginalElement;
63   private ChainIterable<String> myResult;
64   private final ChainIterable<String> myProlog;      // sequence for reassignment info, etc
65   private final ChainIterable<String> myBody;        // sequence for doc string
66   private final ChainIterable<String> myEpilog;      // sequence for doc "copied from" notices and such
67
68   private static final Pattern ourSpacesPattern = Pattern.compile("^\\s+");
69   private final ChainIterable<String> myReassignmentChain;
70
71   public PyDocumentationBuilder(PsiElement element, PsiElement originalElement) {
72     myElement = element;
73     myOriginalElement = originalElement;
74     myResult = new ChainIterable<String>();
75     myProlog = new ChainIterable<String>();
76     myBody = new ChainIterable<String>();
77     myEpilog = new ChainIterable<String>();
78
79     myResult.add(myProlog).addWith(TagCode, myBody).add(myEpilog); // pre-assemble; then add stuff to individual cats as needed
80     myResult = wrapInTag("html", wrapInTag("body", myResult));
81     myReassignmentChain = new ChainIterable<String>();
82   }
83
84   @Nullable
85   public String build() {
86     final TypeEvalContext context = TypeEvalContext.userInitiated(myElement.getProject(), myElement.getContainingFile());
87     final PsiElement outerElement = myOriginalElement != null ? myOriginalElement.getParent() : null;
88
89     final PsiElement elementDefinition = resolveToDocStringOwner();
90     final boolean isProperty = buildFromProperty(elementDefinition, outerElement, context);
91
92     if (myProlog.isEmpty() && !isProperty && !isAttribute()) {
93       myProlog.add(myReassignmentChain);
94     }
95
96     if (elementDefinition instanceof PyDocStringOwner) {
97       buildFromDocstring(elementDefinition, isProperty);
98     }
99     else if (isAttribute()) {
100       buildFromAttributeDoc();
101     }
102     else if (elementDefinition instanceof PyNamedParameter) {
103       buildFromParameter(context, outerElement, elementDefinition);
104     }
105     else if (elementDefinition != null && outerElement instanceof PyReferenceExpression) {
106       myBody.addItem(combUp("\nInferred type: "));
107       PythonDocumentationProvider.describeExpressionTypeWithLinks(myBody, (PyReferenceExpression)outerElement, context);
108     }
109
110     if (elementDefinition != null) {
111       final ASTNode node = elementDefinition.getNode();
112       if (node != null && PythonDialectsTokenSetProvider.INSTANCE.getKeywordTokens().contains(node.getElementType())) {
113         buildForKeyword(elementDefinition.getText());
114       }
115     }
116     final String url = PythonDocumentationProvider.getUrlFor(myElement, myOriginalElement, true);
117     if (url != null) {
118       myEpilog.addItem(BR);
119       myEpilog.addWith(TagBold, $("External documentation:"));
120       myEpilog.addItem(BR);
121       myEpilog.addItem("<a href=\"").addItem(url).addItem("\">").addItem(url).addItem("</a>");
122     }
123
124     if (myBody.isEmpty() && myEpilog.isEmpty()) {
125       return null; // got nothing substantial to say!
126     }
127     else {
128       return myResult.toString();
129     }
130   }
131
132   private void buildForKeyword(@NotNull final String name) {
133     try {
134       final FileReader reader = new FileReader(PythonHelpersLocator.getHelperPath("/tools/python_keywords/" + name));
135       try {
136         final String text = FileUtil.loadTextAndClose(reader);
137         myEpilog.addItem(StringUtil.convertLineSeparators(text, "\n"));
138       }
139       catch (IOException ignored) {
140       }
141       finally {
142         try {
143           reader.close();
144         }
145         catch (IOException ignored) {
146         }
147       }
148     }
149     catch (FileNotFoundException ignored) {
150     }
151   }
152
153   private void buildFromParameter(@NotNull final TypeEvalContext context, @Nullable final PsiElement outerElement,
154                                   @NotNull final PsiElement elementDefinition) {
155     myBody.addItem(combUp("Parameter " + PyUtil.getReadableRepr(elementDefinition, false)));
156     final boolean typeFromDocstringAdded = addTypeAndDescriptionFromDocstring((PyNamedParameter)elementDefinition);
157     if (outerElement instanceof PyExpression) {
158       final PyType type = context.getType((PyExpression)outerElement);
159       if (type != null) {
160         String typeString = null;
161         if (type instanceof PyDynamicallyEvaluatedType) {
162           if (!typeFromDocstringAdded) {
163             typeString = "\nDynamically inferred type: ";
164           }
165         }
166         else {
167           if (outerElement.getReference() != null) {
168             final PsiElement target = outerElement.getReference().resolve();
169
170             if (target instanceof PyTargetExpression) {
171               final String targetName = ((PyTargetExpression)target).getName();
172               if (targetName != null && targetName.equals(((PyNamedParameter)elementDefinition).getName())) {
173                 typeString = "\nReassigned value has type: ";
174               }
175             }
176           }
177         }
178         if (typeString == null && !typeFromDocstringAdded) {
179           typeString = "\nInferred type: ";
180         }
181         if (typeString != null) {
182           myBody.addItem(combUp(typeString));
183           PythonDocumentationProvider.describeTypeWithLinks(myBody, elementDefinition, type, context);
184         }
185       }
186     }
187   }
188
189   private boolean buildFromProperty(PsiElement elementDefinition, @Nullable final PsiElement outerElement,
190                                     @NotNull final TypeEvalContext context) {
191     if (myOriginalElement == null) {
192       return false;
193     }
194     final String elementName = myOriginalElement.getText();
195     if (!PyNames.isIdentifier(elementName)) {
196       return false;
197     }
198     if (!(outerElement instanceof PyQualifiedExpression)) {
199       return false;
200     }
201     final PyExpression qualifier = ((PyQualifiedExpression)outerElement).getQualifier();
202     if (qualifier == null) {
203       return false;
204     }
205     final PyType type = context.getType(qualifier);
206     if (!(type instanceof PyClassType)) {
207       return false;
208     }
209     final PyClass cls = ((PyClassType)type).getPyClass();
210     final Property property = cls.findProperty(elementName, true, null);
211     if (property == null) {
212       return false;
213     }
214
215     final AccessDirection direction = AccessDirection.of((PyElement)outerElement);
216     final Maybe<PyCallable> accessor = property.getByDirection(direction);
217     myProlog.addItem("property ").addWith(TagBold, $().addWith(TagCode, $(elementName)))
218       .addItem(" of ").add(PythonDocumentationProvider.describeClass(cls, TagCode, true, true));
219     if (accessor.isDefined() && property.getDoc() != null) {
220       myBody.addItem(": ").addItem(property.getDoc()).addItem(BR);
221     }
222     else {
223       final PyCallable getter = property.getGetter().valueOrNull();
224       if (getter != null && getter != myElement && getter instanceof PyFunction) {
225         // not in getter, getter's doc comment may be useful
226         final PyStringLiteralExpression docstring = ((PyFunction)getter).getDocStringExpression();
227         if (docstring != null) {
228           myProlog
229             .addItem(BR).addWith(TagItalic, $("Copied from getter:")).addItem(BR)
230             .addItem(docstring.getStringValue())
231           ;
232         }
233       }
234       myBody.addItem(BR);
235     }
236     myBody.addItem(BR);
237     if (accessor.isDefined() && accessor.value() == null) elementDefinition = null;
238     final String accessorKind = getAccessorKind(direction);
239     if (elementDefinition != null) {
240       myEpilog.addWith(TagSmall, $(BR, BR, accessorKind, " of property")).addItem(BR);
241     }
242
243     if (!(elementDefinition instanceof PyDocStringOwner)) {
244       myBody.addWith(TagItalic, elementDefinition != null ? $("Declaration: ") : $(accessorKind + " is not defined.")).addItem(BR);
245       if (elementDefinition != null) {
246         myBody.addItem(combUp(PyUtil.getReadableRepr(elementDefinition, false)));
247       }
248     }
249     return true;
250   }
251
252   @NotNull
253   private static String getAccessorKind(@NotNull final AccessDirection dir) {
254     final String accessorKind;
255     if (dir == AccessDirection.READ) {
256       accessorKind = "Getter";
257     }
258     else if (dir == AccessDirection.WRITE) {
259       accessorKind = "Setter";
260     }
261     else {
262       accessorKind = "Deleter";
263     }
264     return accessorKind;
265   }
266
267   private void buildFromDocstring(@NotNull final PsiElement elementDefinition, boolean isProperty) {
268     PyClass pyClass = null;
269     final PyStringLiteralExpression docStringExpression = ((PyDocStringOwner)elementDefinition).getDocStringExpression();
270
271     if (elementDefinition instanceof PyClass) {
272       pyClass = (PyClass)elementDefinition;
273       myBody.add(PythonDocumentationProvider.describeDecorators(pyClass, TagItalic, BR, LCombUp));
274       myBody.add(PythonDocumentationProvider.describeClass(pyClass, TagBold, true, false));
275     }
276     else if (elementDefinition instanceof PyFunction) {
277       final PyFunction pyFunction = (PyFunction)elementDefinition;
278       if (!isProperty) {
279         pyClass = pyFunction.getContainingClass();
280         if (pyClass != null) {
281           myBody.addWith(TagSmall, PythonDocumentationProvider.describeClass(pyClass, TagCode, true, true)).addItem(BR).addItem(BR);
282         }
283       }
284       myBody.add(PythonDocumentationProvider.describeDecorators(pyFunction, TagItalic, BR, LCombUp))
285             .add(PythonDocumentationProvider.describeFunction(pyFunction, TagBold, LCombUp));
286       if (docStringExpression == null) {
287         addInheritedDocString(pyFunction, pyClass);
288       }
289     }
290     else if (elementDefinition instanceof PyFile) {
291       addModulePath((PyFile)elementDefinition);
292     }
293     if (docStringExpression != null) {
294       myBody.addItem(BR);
295       addFormattedDocString(myElement, docStringExpression.getStringValue(), myBody, myEpilog);
296     }
297   }
298
299   private boolean isAttribute() {
300     return myElement instanceof PyTargetExpression && PyUtil.isAttribute((PyTargetExpression)myElement);
301   }
302
303   @Nullable
304   private PsiElement resolveToDocStringOwner() {
305     // here the ^Q target is already resolved; the resolved element may point to intermediate assignments
306     if (myElement instanceof PyTargetExpression) {
307       final String targetName = myElement.getText();
308       myReassignmentChain.addWith(TagSmall, $(PyBundle.message("QDOC.assigned.to.$0", targetName)).addItem(BR));
309       final PyExpression assignedValue = ((PyTargetExpression)myElement).findAssignedValue();
310       if (assignedValue instanceof PyReferenceExpression) {
311         final PsiElement resolved = resolveWithoutImplicits((PyReferenceExpression)assignedValue);
312         if (resolved != null) {
313           return resolved;
314         }
315       }
316       return assignedValue;
317     }
318     if (myElement instanceof PyReferenceExpression) {
319       myReassignmentChain.addWith(TagSmall, $(PyBundle.message("QDOC.assigned.to.$0", myElement.getText())).addItem(BR));
320       return resolveWithoutImplicits((PyReferenceExpression)myElement);
321     }
322     // it may be a call to a standard wrapper
323     if (myElement instanceof PyCallExpression) {
324       final PyCallExpression call = (PyCallExpression)myElement;
325       final Pair<String, PyFunction> wrapInfo = PyCallExpressionHelper.interpretAsModifierWrappingCall(call, myOriginalElement);
326       if (wrapInfo != null) {
327         final String wrapperName = wrapInfo.getFirst();
328         final PyFunction wrappedFunction = wrapInfo.getSecond();
329         myReassignmentChain.addWith(TagSmall, $(PyBundle.message("QDOC.wrapped.in.$0", wrapperName)).addItem(BR));
330         return wrappedFunction;
331       }
332     }
333     return myElement;
334   }
335
336   private static PsiElement resolveWithoutImplicits(@NotNull PyReferenceExpression element) {
337     final QualifiedResolveResult resolveResult = element.followAssignmentsChain(PyResolveContext.noImplicits());
338     return resolveResult.isImplicit() ? null : resolveResult.getElement();
339   }
340
341   private void addInheritedDocString(@NotNull final PyFunction pyFunction, @Nullable final PyClass pyClass) {
342     boolean notFound = true;
343     final String methodName = pyFunction.getName();
344     if (pyClass == null || methodName == null) {
345       return;
346     }
347     final boolean isConstructor = PyNames.INIT.equals(methodName);
348     Iterable<PyClass> classes = pyClass.getAncestorClasses(null);
349     if (isConstructor) {
350       // look at our own class again and maybe inherit class's doc
351       classes = new ChainIterable<PyClass>(pyClass).add(classes);
352     }
353     for (PyClass ancestor : classes) {
354       PyStringLiteralExpression docstringElement = null;
355       PyFunction inherited = null;
356       boolean isFromClass = false;
357       if (isConstructor) docstringElement = pyClass.getDocStringExpression();
358       if (docstringElement != null) {
359         isFromClass = true;
360       }
361       else {
362         inherited = ancestor.findMethodByName(methodName, false);
363       }
364       if (inherited != null) {
365         docstringElement = inherited.getDocStringExpression();
366       }
367       if (docstringElement != null) {
368         final String inheritedDoc = docstringElement.getStringValue();
369         if (inheritedDoc.length() > 1) {
370           myEpilog.addItem(BR).addItem(BR);
371           final String ancestorName = ancestor.getName();
372           final String marker = (pyClass == ancestor) ? PythonDocumentationProvider.LINK_TYPE_CLASS : PythonDocumentationProvider.LINK_TYPE_PARENT;
373           final String ancestorLink =
374             $().addWith(new LinkWrapper(marker + ancestorName), $(ancestorName)).toString();
375           if (isFromClass) {
376             myEpilog.addItem(PyBundle.message("QDOC.copied.from.class.$0", ancestorLink));
377           }
378           else {
379             myEpilog.addItem(PyBundle.message("QDOC.copied.from.$0.$1", ancestorLink, methodName));
380           }
381           myEpilog.addItem(BR).addItem(BR);
382           final ChainIterable<String> formatted = new ChainIterable<String>();
383           final ChainIterable<String> unformatted = new ChainIterable<String>();
384           addFormattedDocString(pyFunction, inheritedDoc, formatted, unformatted);
385           myEpilog.addWith(TagCode, formatted).add(unformatted);
386           notFound = false;
387           break;
388         }
389       }
390     }
391
392     if (notFound) {
393       // above could have not worked because inheritance is not searched down to 'object'.
394       // for well-known methods, copy built-in doc string.
395       // TODO: also handle predefined __xxx__ that are not part of 'object'.
396       if (PyNames.UnderscoredAttributes.contains(methodName)) {
397         addPredefinedMethodDoc(pyFunction, methodName);
398       }
399     }
400   }
401
402   private void addPredefinedMethodDoc(PyFunction fun, String mothodName) {
403     final PyClassType objectType = PyBuiltinCache.getInstance(fun).getObjectType(); // old- and new-style classes share the __xxx__ stuff
404     if (objectType != null) {
405       final PyClass objectClass = objectType.getPyClass();
406       final PyFunction predefinedMethod = objectClass.findMethodByName(mothodName, false);
407       if (predefinedMethod != null) {
408         final PyStringLiteralExpression predefinedDocstring = predefinedMethod.getDocStringExpression();
409         final String predefinedDoc = predefinedDocstring != null ? predefinedDocstring.getStringValue() : null;
410         if (predefinedDoc != null && predefinedDoc.length() > 1) { // only a real-looking doc string counts
411           addFormattedDocString(fun, predefinedDoc, myBody, myBody);
412           myEpilog.addItem(BR).addItem(BR).addItem(PyBundle.message("QDOC.copied.from.builtin"));
413         }
414       }
415     }
416   }
417
418   private static void addFormattedDocString(@NotNull PsiElement element,
419                                             @NotNull String docstring,
420                                             @NotNull ChainIterable<String> formattedOutput,
421                                             @NotNull ChainIterable<String> unformattedOutput) {
422     final Project project = element.getProject();
423
424     final List<String> formatted = PyStructuredDocstringFormatter.formatDocstring(element, docstring);
425     if (formatted != null) {
426       unformattedOutput.add(formatted);
427       return;
428     }
429
430     boolean isFirstLine;
431     final List<String> result = new ArrayList<String>();
432     final String[] lines = removeCommonIndentation(docstring);
433
434     // reconstruct back, dropping first empty fragment as needed
435     isFirstLine = true;
436     final int tabSize = CodeStyleSettingsManager.getSettings(project).getTabSize(PythonFileType.INSTANCE);
437     for (String line : lines) {
438       if (isFirstLine && ourSpacesPattern.matcher(line).matches()) continue; // ignore all initial whitespace
439       if (isFirstLine) {
440         isFirstLine = false;
441       }
442       else {
443         result.add(BR);
444       }
445       int leadingTabs = 0;
446       while (leadingTabs < line.length() && line.charAt(leadingTabs) == '\t') {
447         leadingTabs++;
448       }
449       if (leadingTabs > 0) {
450         line = StringUtil.repeatSymbol(' ', tabSize * leadingTabs) + line.substring(leadingTabs);
451       }
452       result.add(combUp(line));
453     }
454     formattedOutput.add(result);
455   }
456
457   /**
458    * Adds type and description representation from function docstring
459    *
460    * @param parameter parameter of a function
461    * @return true if type from docstring was added
462    */
463   private boolean addTypeAndDescriptionFromDocstring(@NotNull final PyNamedParameter parameter) {
464     final PyFunction function = PsiTreeUtil.getParentOfType(parameter, PyFunction.class);
465     if (function != null) {
466       final String docString = PyPsiUtils.strValue(function.getDocStringExpression());
467       final Pair<String, String> typeAndDescr = getTypeAndDescription(docString, parameter);
468
469       final String type = typeAndDescr.first;
470       final String description = typeAndDescr.second;
471
472       if (type != null) {
473         final PyType pyType = PyTypeParser.getTypeByName(parameter, type);
474         if (pyType instanceof PyClassType) {
475           myBody.addItem(": ").addWith(new LinkWrapper(PythonDocumentationProvider.LINK_TYPE_PARAM), $(pyType.getName()));
476         }
477         else {
478           myBody.addItem(": ").addItem(type);
479         }
480       }
481
482       if (description != null) {
483         myEpilog.addItem(BR).addItem(description);
484       }
485
486       return type != null;
487     }
488
489     return false;
490   }
491
492   private static Pair<String, String> getTypeAndDescription(@Nullable final String docString, @NotNull final PyNamedParameter followed) {
493     String type = null;
494     String desc = null;
495     if (docString != null) {
496       final StructuredDocString structuredDocString = DocStringUtil.parse(docString);
497       final String name = followed.getName();
498       type = structuredDocString.getParamType(name);
499       desc = structuredDocString.getParamDescription(name);
500     }
501     return Pair.create(type, desc);
502   }
503
504   private void buildFromAttributeDoc() {
505     final PyClass cls = PsiTreeUtil.getParentOfType(myElement, PyClass.class);
506     assert cls != null;
507     final String type = PyUtil.isInstanceAttribute((PyExpression)myElement) ? "Instance attribute " : "Class attribute ";
508     myProlog
509       .addItem(type).addWith(TagBold, $().addWith(TagCode, $(((PyTargetExpression)myElement).getName())))
510       .addItem(" of class ").addWith(PythonDocumentationProvider.LinkMyClass, $().addWith(TagCode, $(cls.getName()))).addItem(BR);
511
512     final String docString = ((PyTargetExpression)myElement).getDocStringValue();
513     if (docString != null) {
514       addFormattedDocString(myElement, docString, myBody, myEpilog);
515     }
516   }
517
518   public static String[] removeCommonIndentation(@NotNull final String docstring) {
519     // detect common indentation
520     final String[] lines = LineTokenizer.tokenize(docstring, false);
521     boolean isFirst = true;
522     int cutWidth = Integer.MAX_VALUE;
523     int firstIndentedLine = 0;
524     for (String frag : lines) {
525       if (frag.length() == 0) continue;
526       int padWidth = 0;
527       final Matcher matcher = ourSpacesPattern.matcher(frag);
528       if (matcher.find()) {
529         padWidth = matcher.end();
530       }
531       if (isFirst) {
532         isFirst = false;
533         if (padWidth == 0) {    // first line may have zero padding
534           firstIndentedLine = 1;
535           continue;
536         }
537       }
538       if (padWidth < cutWidth) cutWidth = padWidth;
539     }
540     // remove common indentation
541     if (cutWidth > 0 && cutWidth < Integer.MAX_VALUE) {
542       for (int i = firstIndentedLine; i < lines.length; i += 1) {
543         if (lines[i].length() >= cutWidth) {
544           lines[i] = lines[i].substring(cutWidth);
545         }
546       }
547     }
548     final List<String> result = new ArrayList<String>();
549     for (String line : lines) {
550       if (line.startsWith(PyConsoleUtil.ORDINARY_PROMPT)) break;
551       result.add(line);
552     }
553     return ArrayUtil.toStringArray(result);
554   }
555
556   private void addModulePath(@NotNull PyFile followed) {
557     // what to prepend to a module description?
558     final VirtualFile file = followed.getVirtualFile();
559     if (file == null) {
560       myProlog.addWith(TagSmall, $(PyBundle.message("QDOC.module.path.unknown")));
561     }
562     else {
563       final String path = file.getPath();
564       final RootFinder finder = new RootFinder(path);
565       RootVisitorHost.visitRoots(followed, finder);
566       final String rootPath = finder.getResult();
567       if (rootPath != null) {
568         final String afterPart = path.substring(rootPath.length());
569         myProlog.addWith(TagSmall, $(rootPath).addWith(TagBold, $(afterPart)));
570       }
571       else {
572         myProlog.addWith(TagSmall, $(path));
573       }
574     }
575   }
576
577   private static class RootFinder implements RootVisitor {
578     private String myResult;
579     private final String myPath;
580
581     private RootFinder(String path) {
582       myPath = path;
583     }
584
585     public boolean visitRoot(@NotNull VirtualFile root, Module module, Sdk sdk, boolean isModuleSource) {
586       final String vpath = VfsUtilCore.urlToPath(root.getUrl());
587       if (myPath.startsWith(vpath)) {
588         myResult = vpath;
589         return false;
590       }
591       else {
592         return true;
593       }
594     }
595
596     String getResult() {
597       return myResult;
598     }
599   }
600 }