Skip to content

Commit

Permalink
Interactive scaladoc.
Browse files Browse the repository at this point in the history
Add interactive scaladoc used when showing the hover and
when presenting scala symbol in the completion popup.

Fix #1000453, Fix #1000210.
  • Loading branch information
vigdorchik authored and huitseeker committed Jun 24, 2013
1 parent fdcf2fa commit 5a6447c
Show file tree
Hide file tree
Showing 18 changed files with 473 additions and 68 deletions.
Expand Up @@ -18,6 +18,7 @@
import scala.tools.eclipse.lexical.ScalaDocumentPartitionerTest;
import scala.tools.eclipse.occurrences.OccurrencesFinderTest;
import scala.tools.eclipse.pc.PresentationCompilerRefreshTest;
import scala.tools.eclipse.pc.PresentationCompilerDocTest;
import scala.tools.eclipse.pc.PresentationCompilerTest;
import scala.tools.eclipse.sbtbuilder.DeprecationWarningsTests;
import scala.tools.eclipse.sbtbuilder.MultipleErrorsTest;
Expand Down Expand Up @@ -62,6 +63,7 @@
LexicalTestsSuite.class,
PresentationCompilerRefreshTest.class,
PresentationCompilerTest.class,
PresentationCompilerDocTest.class,
MultipleErrorsTest.class,
NestedProjectsTest.class,
OccurrencesFinderTest.class,
Expand Down
Expand Up @@ -229,13 +229,9 @@ class CompletionTests {
withCompletions("ticket_1001207/T1207.scala") {
(index, position, completions) =>
assertEquals("There is only one completion location", 0, index)
assertTrue("The completion should return java.util", completions.exists(
_ match {
case CompletionProposal(MemberKind.Package, _, "util", _, _, _, _, _, _, _, _) =>
true
case _ =>
false
}))
assertTrue("The completion should return java.util", completions.exists {
case prop: CompletionProposal => prop.kind == MemberKind.Package && prop.completion == "util"
})
}
}

Expand Down
@@ -0,0 +1,62 @@
package scala.tools.eclipse
package pc

import javaelements.ScalaCompilationUnit
import scala.tools.nsc.doc.base.comment.Comment
import scala.tools.nsc.interactive.Response
import scala.reflect.internal.util.{ Position, SourceFile }
import org.junit._

object PresentationCompilerDocTest extends testsetup.TestProjectSetup("pc_doc")

class PresentationCompilerDocTest {
import PresentationCompilerDocTest._

@Test
def variableExpansion() {
val expect: Comment => Boolean = { cmt =>
existsText(cmt.body, "correctly got derived comment")
}
doTest(open("varz.scala"), expect)
}

@Test
def inheritedDoc() {
val expect: Comment => Boolean = { cmt =>
existsText(cmt.todo, "implement me")
}
doTest(open("inherited.scala"), expect)
}

private def doTest(unit: ScalaCompilationUnit, expectation: Comment => Boolean) {
project.withSourceFile(unit) { (src, compiler) =>
val pos = docPosition(src, compiler)
val response = new Response[compiler.Tree]
compiler.askTypeAt(pos, response)
response.get.left.toOption match {
case None => Assert.fail("Couldn't get typed tree")
case Some(t) =>
compiler.parsedDocComment(t.symbol, t.symbol.enclClass) match {
case None => Assert.fail("Couldn't get documentation")
case Some(comment) => Assert.assertTrue(s"Expectation failed for $comment", expectation(comment))
}
}
}()
}

val rangeStartMarker = "/*s*/"
val rangeEndMarker = "/*e*/"

private def docPosition(src: SourceFile, compiler: ScalaPresentationCompiler): Position = {
val content = new String(src.content)
val start = content.indexOf(rangeStartMarker) + rangeStartMarker.length
val end = content.indexOf(rangeEndMarker, start)
compiler.rangePos(src, start, start, end)
}

private def existsText(where: Any, text: String): Boolean = where match {
case s: String => s contains text
case s: Seq[_] => s exists (existsText(_, text))
case p: Product => p.productIterator exists (existsText(_, text))
}
}
Expand Up @@ -147,4 +147,36 @@ class FreshFile {
// verify
assertNoErrors(unitB)
}
}

@Test
def libraryDocumentation(): Unit =
project.withPresentationCompiler { compiler =>
import compiler.{ reload => _, parseAndEnter => _, _ }
import definitions.ListClass
val unit = ask { () => findCompilationUnit(ListClass).get }
reload(unit)
parseAndEnter(unit)
unit.doWithSourceFile { (source, _) =>
val documented = ask { () =>
// Only check if doc comment is present in the class itself.
// This doesn't include symbols that are inherited from documented symbols.
// An alternative would be to check allOverriddenSymbols, but
// that would require getting sourceFiles for those as well. I'm a bit lazy :)
ListClass.info.decls filter { sym =>
unitOf(source).body exists {
case DocDef(_, defn: DefTree) if defn.name eq sym.name => true
case _ => false
}
}
}
Assert.assertTrue("Couldn't find documented declarations", documented.nonEmpty)
for (sym <- documented) {
Assert.assertTrue(s"Couldn't retrieve $sym documentation",
parsedDocComment(sym, sym.enclClass).isDefined)
}
}
} {
Assert.fail("shouldn't happen")
}

}
Expand Up @@ -6,6 +6,7 @@ import org.eclipse.jdt.core.JavaCore
import org.junit.Assert.assertNotNull
import scala.tools.eclipse.ScalaProject
import org.eclipse.jdt.core.ICompilationUnit
import scala.tools.eclipse.InteractiveCompilationUnit
import scala.tools.eclipse.javaelements.ScalaSourceFile
import scala.tools.eclipse.javaelements.ScalaCompilationUnit
import org.eclipse.jdt.core.IProblemRequestor
Expand Down Expand Up @@ -82,6 +83,14 @@ class TestProjectSetup(projectName: String, srcRoot: String = "/%s/src/", val bu
}()
}

def parseAndEnter(unit: InteractiveCompilationUnit) {
project.withSourceFile(unit) { (src, compiler) =>
val dummy = new compiler.Response[compiler.Tree]
compiler.askParsedEntered(src, false, dummy)
dummy.get
}()
}

def findMarker(marker: String) = SDTTestUtils.findMarker(marker)

/** Emulate the opening of a scala source file (i.e., it tries to
Expand Down
7 changes: 7 additions & 0 deletions org.scala-ide.sdt.core.tests/test-workspace/pc_doc/.classpath
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="src" path="src"/>
<classpathentry kind="con" path="org.scala-ide.sdt.launching.SCALA_CONTAINER"/>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.6"/>
<classpathentry kind="output" path="bin"/>
</classpath>
18 changes: 18 additions & 0 deletions org.scala-ide.sdt.core.tests/test-workspace/pc_doc/.project
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>pc_doc</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.scala-ide.sdt.core.scalabuilder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.scala-ide.sdt.core.scalanature</nature>
<nature>org.eclipse.jdt.core.javanature</nature>
</natures>
</projectDescription>
@@ -0,0 +1,13 @@

trait T {
/**
* @todo implement me
*/
def foo(): Unit
}

abstract class C extends T {
override def foo() {}

/*s*/foo/*e*/()
}
11 changes: 11 additions & 0 deletions org.scala-ide.sdt.core.tests/test-workspace/pc_doc/src/varz.scala
@@ -0,0 +1,11 @@

/**
* @define BaseComment correctly got $BaseVar comment
*/
trait TV

/**
* $BaseComment
* @define BaseVar derived
*/
class /*s*/CV/*e*/ extends TV
Expand Up @@ -30,7 +30,7 @@ trait LocateSymbol { self : ScalaPresentationCompiler =>
val javaProject = project.javaProject.asInstanceOf[JavaProject]
val pfs = new SearchableEnvironment(javaProject, null: WorkingCopyOwner).nameLookup.findPackageFragments(packName, false)
if (pfs eq null) None else pfs.toStream flatMap { pf =>
val name = ask { () =>
val name = {
val top = sym.enclosingTopLevelClass
if (sym.owner.isPackageObjectClass) "package$.class" else top.name + (if (top.isModuleClass) "$" else "") + ".class"
}
Expand All @@ -50,9 +50,7 @@ trait LocateSymbol { self : ScalaPresentationCompiler =>
val javaProject = project.javaProject.asInstanceOf[JavaProject]
val nameLookup = new SearchableEnvironment(javaProject, null: WorkingCopyOwner).nameLookup

val name = ask { () =>
if (sym.owner.isPackageObject) sym.owner.owner.fullName + ".package" else sym.enclosingTopLevelClass.fullName
}
val name = if (sym.owner.isPackageObject) sym.owner.owner.fullName + ".package" else sym.enclosingTopLevelClass.fullName
logger.debug("Looking for compilation unit " + name)
Option(nameLookup.findCompilationUnit(name)) map (_.getResource().getFullPath())
}
Expand All @@ -67,7 +65,7 @@ trait LocateSymbol { self : ScalaPresentationCompiler =>
}
} else
findPath

findSourceFile.fold(findClassFile) { f =>
SourceFileProviderRegistry.getProvider(f) flatMap (_.createFrom(f))
}
Expand Down
75 changes: 52 additions & 23 deletions org.scala-ide.sdt.core/src/scala/tools/eclipse/ScalaHover.scala
Expand Up @@ -5,45 +5,74 @@

package scala.tools.eclipse

import ui.BrowserControlCreator
import org.eclipse.jdt.core.ICodeAssist
import org.eclipse.jface.text.{ ITextViewer, IRegion, ITextHover }
import org.eclipse.jface.text.{ ITextViewer, IRegion, ITextHover, ITextHoverExtension, ITextHoverExtension2 }
import org.eclipse.swt.SWT
import org.eclipse.swt.widgets.Shell
import org.eclipse.jface.text.{IInformationControlCreator, DefaultInformationControl}

import scala.tools.nsc.symtab.Flags
import scala.tools.eclipse.util.EclipseUtils._

class ScalaHover(val icu: InteractiveCompilationUnit) extends ITextHover {
class ScalaHover(val icu: InteractiveCompilationUnit) extends ITextHover with ITextHoverExtension with ITextHoverExtension2 {

private val NoHoverInfo = "" // could return null, but prefer to return empty (see API of ITextHover).

override def getHoverInfo(viewer: ITextViewer, region: IRegion) = {
val start = region.getOffset
val end = start + region.getLength
override def getHoverInfo(viewer: ITextViewer, region: IRegion) = null

override def getHoverInfo2(viewer: ITextViewer, region: IRegion): Object =
icu.withSourceFile({ (src, compiler) =>
import compiler._

def hoverInfo(t: Tree): Option[String] = askOption { () =>
def compose(ss: List[String]): String = ss.filter("" !=).mkString("", " ", "")
def defString(sym: Symbol, tpe: Type): String = {
// NoType is returned for defining occurrences, in this case we want to display symbol info itself.
val tpeinfo = if (tpe ne NoType) tpe.widen else sym.info
compose(List(sym.flagString(Flags.ExplicitFlags), sym.keyString, sym.varianceString + sym.nameString +
sym.infoString(tpeinfo)))
}
def hoverInfo(t: Tree): Option[Object] = {
val askedOpt = askOption { () =>
def compose(ss: List[String]): String = ss.filterNot(_.isEmpty).mkString(" ")
def defString(sym: Symbol, tpe: Type): String = {
compose(List(sym.flagString(Flags.ExplicitFlags), sym.keyString, sym.varianceString + sym.nameString +
sym.infoString(tpe)))
}

for (tsym <- Option(t.symbol)) yield {
def pre(t: Tree): Type = t match {
case Apply(fun, _) => pre(fun)
case Select(qual, _) => qual.tpe
case _ if tsym.enclClass ne NoSymbol => ThisType(tsym.enclClass)
case _ => NoType
}
val pt = pre(t)
val site = pt.typeSymbol
val sym = if(tsym.isCaseApplyOrUnapply) site else tsym
val header = if (sym.isClass || sym.isModule) sym.fullName else {
val tpe = sym.tpe.asSeenFrom(pt.widen, site)
defString(sym, tpe)
}
(sym, site, header)
}
}.flatten

for (sym <- Option(t.symbol); tpe <- Option(t.tpe))
yield if (sym.isClass || sym.isModule) sym.fullName else defString(sym, tpe)
} getOrElse None
for ((sym, site, header) <- askedOpt) yield
browserInput(sym, site, header) getOrElse {
val html = "<html><body><b>" + header + "</b></body></html>"
new BrowserInput(html, sym)
}
}

val wordPos = region.toRangePos(src)
val pos = unitOfFile(src.file).body find {
case Apply(fun, _) if fun.pos.isRange && fun.pos.end == wordPos.end => true
case _ => false
} map (_.pos) getOrElse wordPos
val resp = new Response[Tree]
askTypeAt(region.toRangePos(src), resp)
(for (
t <- resp.get.left.toOption;
hover <- hoverInfo(t)
) yield hover) getOrElse NoHoverInfo
askTypeAt(pos, resp)
resp.get.left.toOption flatMap hoverInfo getOrElse NoHoverInfo
})(NoHoverInfo)
}

override def getHoverRegion(viewer: ITextViewer, offset: Int) = {
def getHoverRegion(viewer: ITextViewer, offset: Int) = {
ScalaWordFinder.findWord(viewer.getDocument, offset)
}

def getHoverControlCreator() = new IInformationControlCreator {
def createInformationControl(shell: Shell) = new DefaultInformationControl(shell, false)
}
}
Expand Up @@ -10,9 +10,9 @@ import scala.collection.mutable
import scala.collection.mutable.{ ArrayBuffer, SynchronizedMap }
import org.eclipse.jdt.core.compiler.IProblem
import org.eclipse.jdt.internal.compiler.problem.{ DefaultProblem, ProblemSeverities }
import scala.tools.nsc.Settings
import scala.tools.nsc.interactive.{ Global, InteractiveReporter, Problem }
import scala.tools.nsc.io.AbstractFile
import scala.tools.nsc.Settings
import scala.tools.nsc.reporters.Reporter
import scala.reflect.internal.util.{ BatchSourceFile, Position, SourceFile }
import scala.tools.eclipse.javaelements.{
Expand Down Expand Up @@ -52,7 +52,10 @@ class ScalaPresentationCompiler(val project: ScalaProject, settings: Settings) e
with JavaSig
with JVMUtils
with LocateSymbol
with HasLogger { self =>
with HasLogger
with Scaladoc { self =>

override def forScaladoc = true

def presentationReporter = reporter.asInstanceOf[ScalaPresentationCompiler.PresentationReporter]
presentationReporter.compiler = this
Expand Down Expand Up @@ -317,7 +320,8 @@ class ScalaPresentationCompiler(val project: ScalaProject, settings: Settings) e
} else scalaParamNames
}

import scala.tools.eclipse.completion.HasArgs
val docFun = () => browserInput(sym, sym.enclClass) // TODO: proper site. How?

CompletionProposal(kind,
start,
name,
Expand All @@ -328,7 +332,8 @@ class ScalaPresentationCompiler(val project: ScalaProject, settings: Settings) e
getParamNames,
paramTypes,
sym.fullName,
false)
false,
docFun)
}

override def inform(msg: String): Unit =
Expand Down

0 comments on commit 5a6447c

Please sign in to comment.