Skip to content

Commit

Permalink
More abstract dealings with Scala compilation units.
Browse files Browse the repository at this point in the history
We need a way to treat uniformly different kinds of Scala-based sources. Until now
we routed everything through `ScalaCompilationUnit`, that extends the JDT `CompilationUnit`
class. This tight connection is counter-productive, and in most cases, unnecessary.

This commit is a step towards extracting a set of interfaces for other Scala-based
sources to benefit of reconciliation ('errors-as-you-type'), hyperlinking and completion
engine that already exists.

* extracted `InteractiveCompilationUnit` as a super type of `ScalaCompilationUnit`
* switched from `ScalaCompilationUnit` to `InteractiveCompilationUnit` wherever possible, to allow
hyperlinking to work in the scala-worksheet project
* added a utility method to retrieve the compilation unit associated with an open editor that
uses the platform adapter protocol to allow other plugins to participate.
  • Loading branch information
dragos committed Jul 27, 2012
1 parent 2b1ac15 commit af7db24
Show file tree
Hide file tree
Showing 19 changed files with 194 additions and 79 deletions.
Expand Up @@ -81,7 +81,7 @@ class ImplicitsHighlightingTest {
}

def implicits(compiler: ScalaPresentationCompiler, scu: ScalaCompilationUnit) = {
val implicits = ImplicitHighlightingPresenter.findAllImplicitConversions(compiler, scu, scu.createSourceFile)
val implicits = ImplicitHighlightingPresenter.findAllImplicitConversions(compiler, scu, scu.sourceFile())
implicits.toList map {
case (ann, p) =>
ann.getText() +" ["+ p.getOffset() + ", "+ p.getLength() +"]"
Expand Down
@@ -0,0 +1,72 @@
package scala.tools.eclipse

import org.eclipse.core.resources.IFile
import org.eclipse.jdt.core.compiler.IProblem

import scala.tools.nsc.util.BatchSourceFile
import scala.tools.nsc.util.SourceFile
import scala.tools.nsc.interactive.Response
import scala.tools.nsc.io.AbstractFile

/** A Scala compilation unit. It can be backed up by a `ScalaCompilationUnit` in usual
* Scala projects, or any other implementation (such as a specialized Scala DSL, a
* Script file, an Sbt build file, etc.).
*
*/
trait InteractiveCompilationUnit {

/** The `AbstractFile` that the Scala compiler uses to read this compilation unit. */
def file: AbstractFile

/** The workspace file corresponding to this compilation unit. */
def workspaceFile: IFile

/** Does this unit exist in the workspace? */
def exists(): Boolean

/** The Scala project to which this compilation unit belongs. */
def scalaProject: ScalaProject

/** Return a compiler `SourceFile` implementation with the given contents. The implementation decides
* if this is a batch file or a script/other kind of source file.
*/
def sourceFile(contents: Array[Char] = getContents): SourceFile

/** Return a compiler batch source file (that needs a top-level definition). */
def batchSourceFile(contents: Array[Char] = getContents): BatchSourceFile

/** Reconcile the unit. Return all compilation errors.
*
* Blocks until the unit is type-checked.
*/
def reconcile(newContents: String): List[IProblem]

/** Return all compilation errors from this unit. Waits until the unit is type-checked.
* It may be long running, but it won't force retype-checking. If the unit was already typed,
* the answer is fast.
*/
def currentProblems(): List[IProblem]

/** Return the current contents of this compilation unit. */
def getContents: Array[Char]

/** Perform a side-effecting operation on the source file, with the current presentation compiler. */
def doWithSourceFile(op: (SourceFile, ScalaPresentationCompiler) => Unit) {
scalaProject.withSourceFile(this)(op)(())
}

/** Perform an operation on the source file, with the current presentation compiler.
*
* @param op The operation to be performed
* @param orElse A recovery option in case the presentation compiler is not available (for instance, if it cannot be
* started because of classpath issues)
*/
def withSourceFile[T](op: (SourceFile, ScalaPresentationCompiler) => T)(orElse: => T = scalaProject.defaultOrElse): T = {
scalaProject.withSourceFile(this)(op)(orElse)
}

/** Schedule the unit for reconciliation. Not blocking. Used by the usual Scala editor to signal a need for `askReload`,
* ensuring faster response when calling `getProblems`.
*/
def scheduleReconcile(): Response[Unit]
}
Expand Up @@ -23,7 +23,7 @@ import javaelements.{ScalaSourceFile, ScalaClassFile, ScalaCompilationUnit}

trait LocateSymbol { self : ScalaPresentationCompiler =>

def locate(sym : Symbol, scu : ScalaCompilationUnit): Option[(ScalaCompilationUnit, Int)] = {
def locate(sym : Symbol, scu : InteractiveCompilationUnit): Option[(ScalaCompilationUnit, Int)] = {
def find[T, V](arr : Array[T])(f : T => Option[V]) : Option[V] = {
for(e <- arr) {
f(e) match {
Expand All @@ -36,7 +36,7 @@ trait LocateSymbol { self : ScalaPresentationCompiler =>
def findClassFile = {
logger.debug("Looking for a classfile for " + sym.fullName)
val packName = sym.enclosingPackage.fullName
val project = scu.getJavaProject.asInstanceOf[JavaProject]
val project = scu.scalaProject.javaProject.asInstanceOf[JavaProject]
val pfs = new SearchableEnvironment(project, null: WorkingCopyOwner).nameLookup.findPackageFragments(packName, false)
if (pfs eq null) None else find(pfs) { pf =>
val top = sym.toplevelClass
Expand All @@ -54,7 +54,7 @@ trait LocateSymbol { self : ScalaPresentationCompiler =>

def findCompilationUnit() = {
logger.info("Looking for a compilation unit for " + sym.fullName)
val project = scu.getJavaProject.asInstanceOf[JavaProject]
val project = scu.scalaProject.javaProject.asInstanceOf[JavaProject]
val nameLookup = new SearchableEnvironment(project, null: WorkingCopyOwner).nameLookup

val name = if (sym.owner.isPackageObject) sym.owner.owner.fullName + ".package" else sym.toplevelClass.fullName
Expand Down
Expand Up @@ -44,9 +44,9 @@ class ScalaPresentationCompiler(project : ScalaProject, settings : Settings)
* This map is populated by having a default source file created when calling 'apply',
* which currently happens in 'withSourceFile'.
*/
private val sourceFiles = new mutable.HashMap[ScalaCompilationUnit, BatchSourceFile] {
override def default(k : ScalaCompilationUnit) = {
val v = k.createSourceFile
private val sourceFiles = new mutable.HashMap[InteractiveCompilationUnit, SourceFile] {
override def default(k : InteractiveCompilationUnit) = {
val v = k.sourceFile()
ScalaPresentationCompiler.this.synchronized {
get(k) match {
case Some(v) => v
Expand All @@ -58,7 +58,7 @@ class ScalaPresentationCompiler(project : ScalaProject, settings : Settings)

/** Return the Scala compilation units that are currently maintained by this presentation compiler.
*/
def compilationUnits: Seq[ScalaCompilationUnit] = {
def compilationUnits: Seq[InteractiveCompilationUnit] = {
val managedFiles = unitOfFile.keySet
(for {
(cu, sourceFile) <- sourceFiles
Expand Down Expand Up @@ -86,7 +86,7 @@ class ScalaPresentationCompiler(project : ScalaProject, settings : Settings)
/** Run the operation on the given compilation unit. If the source file is not yet tracked by
* the presentation compiler, a new BatchSourceFile is created and kept for future reference.
*/
def withSourceFile[T](scu : ScalaCompilationUnit)(op : (SourceFile, ScalaPresentationCompiler) => T) : T =
def withSourceFile[T](scu : InteractiveCompilationUnit)(op : (SourceFile, ScalaPresentationCompiler) => T) : T =
op(sourceFiles(scu), this)

def body(sourceFile : SourceFile) = {
Expand Down
Expand Up @@ -238,7 +238,11 @@ class ScalaProject private (val underlying: IProject) extends ClasspathManagemen
* and they are handles only (may not exist).
*/
def outputFolders: Seq[IPath] =
sourceOutputFolders map (_._2.getFullPath())
sourceOutputFolders map (_._2.getFullPath)

/** The output folder file-system absolute paths. */
def outputFolderLocations: Seq[IPath] =
sourceOutputFolders map (_._2.getLocation)

/** Return the source folders and their corresponding output locations
* without relying on NameEnvironment. Does not create folders if they
Expand Down Expand Up @@ -503,7 +507,7 @@ class ScalaProject private (val underlying: IProject) extends ClasspathManagemen
}
}

def withSourceFile[T](scu: ScalaCompilationUnit)(op: (SourceFile, ScalaPresentationCompiler) => T)(orElse: => T = defaultOrElse): T = {
def withSourceFile[T](scu: InteractiveCompilationUnit)(op: (SourceFile, ScalaPresentationCompiler) => T)(orElse: => T = defaultOrElse): T = {
withPresentationCompiler { compiler =>
compiler.withSourceFile(scu)(op)
} {orElse}
Expand All @@ -517,7 +521,7 @@ class ScalaProject private (val underlying: IProject) extends ClasspathManagemen
*/
def resetPresentationCompiler(): Boolean =
if (presentationCompiler.initialized) {
val units: Seq[ScalaCompilationUnit] = withPresentationCompiler(_.compilationUnits)(Nil)
val units: Seq[InteractiveCompilationUnit] = withPresentationCompiler(_.compilationUnits)(Nil)

shutDownPresentationCompiler()

Expand Down
Expand Up @@ -4,6 +4,7 @@ import org.eclipse.jface.text.IRegion
import org.eclipse.jface.text.hyperlink.IHyperlink
import scala.tools.eclipse.ScalaPresentationCompiler
import scala.tools.eclipse.javaelements.ScalaCompilationUnit
import scala.tools.eclipse.InteractiveCompilationUnit

/** A factory that builds IHyperlink instances from compiler Symbols.
*
Expand Down Expand Up @@ -31,7 +32,7 @@ import scala.tools.eclipse.javaelements.ScalaCompilationUnit
abstract class HyperlinkFactory {
protected val global: ScalaPresentationCompiler

def create(createHyperlink: Hyperlink.Factory, scu: ScalaCompilationUnit, sym: global.Symbol, region: IRegion): Option[IHyperlink] = {
def create(createHyperlink: Hyperlink.Factory, scu: InteractiveCompilationUnit, sym: global.Symbol, region: IRegion): Option[IHyperlink] = {
global.askOption { () =>
global.locate(sym, scu) map {
case (f, pos) =>
Expand Down
@@ -1,12 +1,13 @@
package scala.tools.eclipse.hyperlink.text.detector

import org.eclipse.jdt.internal.ui.javaeditor.EditorUtility
import org.eclipse.jface.text.IRegion
import org.eclipse.jface.text.ITextViewer
import org.eclipse.jface.text.hyperlink.AbstractHyperlinkDetector
import org.eclipse.jface.text.hyperlink.IHyperlink
import org.eclipse.ui.texteditor.ITextEditor
import scala.tools.eclipse.javaelements.ScalaCompilationUnit

import scala.tools.eclipse.InteractiveCompilationUnit
import scala.tools.eclipse.util.EditorUtils

abstract class BaseHyperlinkDetector extends AbstractHyperlinkDetector {

Expand All @@ -17,9 +18,10 @@ abstract class BaseHyperlinkDetector extends AbstractHyperlinkDetector {

final def detectHyperlinks(textEditor: ITextEditor, currentSelection: IRegion, canShowMultipleHyperlinks: Boolean): Array[IHyperlink] = {
if (textEditor == null) null // can be null if generated through ScalaPreviewerFactory
else
EditorUtility.getEditorInputJavaElement(textEditor, false) match {
case scu: ScalaCompilationUnit =>
else {
EditorUtils.getEditorScalaInput(textEditor) match {
case scu: InteractiveCompilationUnit =>

val hyperlinks = runDetectionStrategy(scu, textEditor, currentSelection)
hyperlinks match {
// I know you will be tempted to remove this, but don't do it, JDT expects null when no hyperlinks are found.
Expand All @@ -31,7 +33,8 @@ abstract class BaseHyperlinkDetector extends AbstractHyperlinkDetector {

case _ => null
}
}
}

protected[detector] def runDetectionStrategy(scu: ScalaCompilationUnit, textEditor: ITextEditor, currentSelection: IRegion): List[IHyperlink]
protected[detector] def runDetectionStrategy(scu: InteractiveCompilationUnit, textEditor: ITextEditor, currentSelection: IRegion): List[IHyperlink]
}
Expand Up @@ -12,26 +12,36 @@ import scala.tools.eclipse.javaelements.ScalaCompilationUnit
import scala.tools.eclipse.javaelements.ScalaSelectionEngine
import scala.tools.eclipse.javaelements.ScalaSelectionRequestor
import scala.tools.eclipse.logging.HasLogger
import scala.tools.eclipse.InteractiveCompilationUnit
import org.eclipse.jdt.internal.core.JavaProject
import org.eclipse.jdt.internal.core.DefaultWorkingCopyOwner

class DeclarationHyperlinkDetector extends BaseHyperlinkDetector with HasLogger {

private val resolver: ScalaDeclarationHyperlinkComputer = new ScalaDeclarationHyperlinkComputer

override protected[detector] def runDetectionStrategy(scu: ScalaCompilationUnit, textEditor: ITextEditor, currentSelection: IRegion): List[IHyperlink] = {
val wordRegion = ScalaWordFinder.findWord(scu.getContents, currentSelection.getOffset)
override protected[detector] def runDetectionStrategy(scu: InteractiveCompilationUnit, textEditor: ITextEditor, currentSelection: IRegion): List[IHyperlink] = {
val input = textEditor.getEditorInput
val doc = textEditor.getDocumentProvider.getDocument(input)
val wordRegion = ScalaWordFinder.findWord(doc.get, currentSelection.getOffset)

resolver.findHyperlinks(scu, wordRegion) match {
case None => List()
case Some(List()) => javaDeclarationHyperlinkComputer(textEditor, wordRegion, scu)
case Some(hyperlinks) => hyperlinks
case Some(List()) =>
scu match {
case scalaCU: ScalaCompilationUnit => javaDeclarationHyperlinkComputer(textEditor, wordRegion, scalaCU)
case _ => Nil
}
case Some(hyperlinks) =>
hyperlinks
}
}

private def javaDeclarationHyperlinkComputer(textEditor: ITextEditor, wordRegion: IRegion, scu: ScalaCompilationUnit): List[IHyperlink] = {
try {
val environment = scu.newSearchableEnvironment()
val requestor = new ScalaSelectionRequestor(environment.nameLookup, scu)
val engine = new ScalaSelectionEngine(environment, requestor, scu.getJavaProject.getOptions(true))
val engine = new ScalaSelectionEngine(environment, requestor, scu.scalaProject.javaProject.getOptions(true))
val offset = wordRegion.getOffset
engine.select(scu, offset, offset + wordRegion.getLength - 1)
val elements = requestor.getElements.toList
Expand Down
@@ -1,17 +1,17 @@
package scala.tools.eclipse.hyperlink.text.detector

import org.eclipse.jface.text.IRegion
import org.eclipse.jface.text.ITextViewer
import org.eclipse.jface.text.hyperlink.AbstractHyperlinkDetector
import org.eclipse.jface.text.hyperlink.IHyperlink
import scala.tools.eclipse.javaelements.ScalaCompilationUnit
import org.eclipse.ui.texteditor.ITextEditor

import scala.tools.eclipse.InteractiveCompilationUnit

private class HyperlinksDetector extends BaseHyperlinkDetector {

private val strategies: List[BaseHyperlinkDetector] = List(DeclarationHyperlinkDetector(), ImplicitHyperlinkDetector())

override protected[detector] def runDetectionStrategy(scu: ScalaCompilationUnit, textEditor: ITextEditor, currentSelection: IRegion): List[IHyperlink] =
override protected[detector] def runDetectionStrategy(scu: InteractiveCompilationUnit, textEditor: ITextEditor, currentSelection: IRegion): List[IHyperlink] =
strategies flatMap { _.runDetectionStrategy(scu, textEditor, currentSelection) }
}

Expand Down
@@ -1,18 +1,17 @@
package scala.tools.eclipse.hyperlink.text.detector

import org.eclipse.jface.text.IRegion
import org.eclipse.jface.text.hyperlink.AbstractHyperlinkDetector
import org.eclipse.jface.text.hyperlink.IHyperlink
import org.eclipse.ui.texteditor.ITextEditor
import scala.tools.eclipse.javaelements.ScalaCompilationUnit

import scala.tools.eclipse.InteractiveCompilationUnit
import scala.tools.eclipse.semantichighlighting.implicits.ImplicitConversionAnnotation
import scala.tools.eclipse.util.EditorUtils.getAnnotationsAtOffset
import scala.tools.eclipse.util.EditorUtils.openEditorAndApply

private class ImplicitHyperlinkDetector extends BaseHyperlinkDetector {

override protected[detector] def runDetectionStrategy(scu: ScalaCompilationUnit, textEditor: ITextEditor, currentSelection: IRegion): List[IHyperlink] =
findHyperlinkToImplicit(scu, currentSelection.getOffset)
override protected[detector] def runDetectionStrategy(scu: InteractiveCompilationUnit, textEditor: ITextEditor, currentSelection: IRegion): List[IHyperlink] =
findHyperlinkToImplicit(scu, currentSelection.getOffset, textEditor)

/**
* Check if an {{{ImplicitConversionAnnotation}}} at the given {{{offset}}} exists in the
Expand All @@ -23,18 +22,16 @@ private class ImplicitHyperlinkDetector extends BaseHyperlinkDetector {
// FIXME: I quite dislike the current implementation, for the following reasons:
// 1) We go through all the editor's annotations to find if an implicit conversion is applied at the given {{{offset}}}.
// 2) Because we use the editor's annotation model, this functionality cannot be tested in a UI-less environment.
private def findHyperlinkToImplicit(scu: ScalaCompilationUnit, offset: Int): List[IHyperlink] = {
private def findHyperlinkToImplicit(scu: InteractiveCompilationUnit, offset: Int, editor: ITextEditor): List[IHyperlink] = {
import scala.tools.eclipse.semantichighlighting.implicits.ImplicitConversionAnnotation
import scala.tools.eclipse.util.EditorUtils.{ openEditorAndApply, getAnnotationsAtOffset }
import scala.tools.eclipse.util.EditorUtils.getAnnotationsAtOffset

var hyperlinks = List[IHyperlink]()

openEditorAndApply(scu) { editor =>
for ((ann, pos) <- getAnnotationsAtOffset(editor, offset)) ann match {
case a: ImplicitConversionAnnotation if a.sourceLink.isDefined =>
hyperlinks = a.sourceLink.get :: hyperlinks
case _ => ()
}
for ((ann, pos) <- getAnnotationsAtOffset(editor, offset)) ann match {
case a: ImplicitConversionAnnotation if a.sourceLink.isDefined =>
hyperlinks = a.sourceLink.get :: hyperlinks
case _ => ()
}

hyperlinks
Expand Down
Expand Up @@ -8,9 +8,10 @@ import scala.tools.eclipse.{ScalaPresentationCompiler => compiler}
import scala.tools.eclipse.javaelements.ScalaCompilationUnit
import scala.tools.eclipse.logging.HasLogger
import scala.tools.eclipse.hyperlink.text._
import scala.tools.eclipse.InteractiveCompilationUnit

private[hyperlink] class ScalaDeclarationHyperlinkComputer extends HasLogger {
def findHyperlinks(scu: ScalaCompilationUnit, wordRegion: IRegion): Option[List[IHyperlink]] = {
def findHyperlinks(scu: InteractiveCompilationUnit, wordRegion: IRegion): Option[List[IHyperlink]] = {
scu.withSourceFile({ (sourceFile, compiler) =>
object DeclarationHyperlinkFactory extends HyperlinkFactory {
protected val global: compiler.type = compiler
Expand Down
Expand Up @@ -36,6 +36,8 @@ class ScalaClassFile(parent : PackageFragment, name : String, sourceFile : Strin
if (e == this) null else e
}

def reconcile(contents: String): List[IProblem] = Nil

def getCorrespondingElement(element : IJavaElement) : Option[IJavaElement] = {
if (!validateExistence(resource).isOK)
None
Expand Down Expand Up @@ -100,7 +102,7 @@ class ScalaClassFile(parent : PackageFragment, name : String, sourceFile : Strin
if ((underlyingResource ne null) && !underlyingResource.isAccessible) newDoesNotExistStatus() else JavaModelStatus.VERIFIED_OK
}

def getProblems : Array[IProblem] = null
def currentProblems: List[IProblem] = Nil

def closeBuffer0() = super.closeBuffer()
def closing0(info : AnyRef) = super.closing(info)
Expand Down

0 comments on commit af7db24

Please sign in to comment.