Skip to content

Commit

Permalink
Automatically select problem position on quick assist invocation
Browse files Browse the repository at this point in the history
This feature is supported by JDT quick assists, but got lost after we
got our own implementation for quick assist.

Fixes #1002305
  • Loading branch information
kiritsuku committed Nov 5, 2014
1 parent a7c06b2 commit 7f025a8
Showing 1 changed file with 83 additions and 29 deletions.
Expand Up @@ -2,8 +2,10 @@ package org.scalaide.core.internal.quickassist

import org.eclipse.core.commands.AbstractHandler
import org.eclipse.core.commands.ExecutionEvent
import org.eclipse.jdt.internal.ui.javaeditor.CompilationUnitDocumentProvider.ProblemAnnotation
import org.eclipse.jdt.internal.ui.javaeditor.IJavaAnnotation
import org.eclipse.jface.text.IDocument
import org.eclipse.jface.text.TextSelection
import org.eclipse.jface.text.contentassist.ICompletionProposal
import org.eclipse.jface.text.quickassist.IQuickAssistInvocationContext
import org.eclipse.jface.text.quickassist.IQuickAssistProcessor
Expand Down Expand Up @@ -77,50 +79,102 @@ final class QuickAssistProcessor(input: IEditorInput, id: String) extends IQuick
import QuickAssistProcessor._

override def computeQuickAssistProposals(ctx: IQuickAssistInvocationContext): Array[ICompletionProposal] = {
def problems = {
/**
* Searches for annotations whose position intersects with the cursor
* position and maps them to `AssistLocation`. In case there exists a
* problem annotation in the same line where also the cursor is located but
* it does not intersect with the cursor position, it is separately returned.
*
* The precedence rules to show quick assists (based on these two return
* values) are as follows:
*
* 1. Show quick assists considering the cursor position but only if a
* problem annotation exists at the cursor position.
* 2. Show quick assists considering the problem position of the same line
* if it exists.
* 3. Show quick assists considering the cursor position.
*
* Beside from that, the cursor needs to be moved to the position of the
* problem annotation of the same line unless it is already at the position
* of the annotation - in this case it should not move.
*/
def assistLocations = {
import collection.JavaConverters._
val model = ScalaPlugin().documentProvider.getAnnotationModel(input)
val iter = model.getAnnotationIterator.asScala

(iter foldLeft IndexedSeq[AssistLocation]()) {
case (ps, a: Annotation) if a.isInstanceOf[ScalaEditorAnnotation] || a.isInstanceOf[IJavaAnnotation] =>
val (start, end) = {
val p = model.getPosition(a)
(p.offset, p.offset+p.length)
}

def isOffsetInsidePos(offset: Int): Boolean =
offset == start || offset == end || (offset > start && offset < end)

def isPosInsideRange(rStart: Int, rEnd: Int): Boolean =
start >= rStart && end <= rEnd

def posNotYetFound =
ps.forall(a => !isPosInsideRange(a.offset, a.offset+a.length))

if (isOffsetInsidePos(ctx.getOffset) && posNotYetFound)
ps :+ AssistLocation(start, end-start, a)
else
ps
case (ps, _) =>
ps
}
val d = ctx.getSourceViewer.getDocument
val lineOfInvocation = d.getLineOfOffset(ctx.getOffset)

type Assists = IndexedSeq[AssistLocation]
type FirstProblem = Option[AssistLocation]

def loop(as: Assists, fp: FirstProblem): (Assists, FirstProblem) =
if (iter.isEmpty)
(as, fp)
else iter.next() match {
case a: Annotation if a.isInstanceOf[ScalaEditorAnnotation] || a.isInstanceOf[IJavaAnnotation] =>
val (start, end) = {
val p = model.getPosition(a)
(p.offset, p.offset+p.length)
}

def isOffsetInsidePos(offset: Int): Boolean =
offset == start || offset == end || (offset > start && offset < end)

def isPosInsideRange(rStart: Int, rEnd: Int): Boolean =
start >= rStart && end <= rEnd

def posNotYetFound =
as.forall(a => !isPosInsideRange(a.offset, a.offset+a.length))

def isProblemInLine(a: Annotation, offset: Int, end: Int) =
a.isInstanceOf[ProblemAnnotation] && lineOfInvocation == d.getLineOfOffset(offset)

if (isOffsetInsidePos(ctx.getOffset) && posNotYetFound)
loop(as :+ AssistLocation(start, end-start, a), fp)
else if (fp.isEmpty && posNotYetFound && isProblemInLine(a, start, end))
loop(as, Some(AssistLocation(start, end-start, a)))
else
loop(as, fp)
case _ =>
loop(as, fp)
}

loop(IndexedSeq(), None)
}

val ssf = IScalaPlugin().scalaCompilationUnit(input)
val assists =
val quickAssists =
if (id == DefaultId)
QuickAssists
else
QuickAssists.find(_.id == id).toSeq

if (assists.isEmpty || ssf.isEmpty)
if (quickAssists.isEmpty || ssf.isEmpty)
Array(NoProposals)
else {
val ictx = InvocationContext(ssf.get, ctx.getOffset, ctx.getLength, problems)
val proposals = assists flatMap (_ withInstance (_ compute ictx))
val (locations, problemInLine) = assistLocations
val problemAtCursor = locations.find(_.annotation.isInstanceOf[ProblemAnnotation])

// ctx.getLength is always -1, we need to retrieve it manually
def selLen = ctx.getSourceViewer.getSelectionProvider.getSelection match {
case s: TextSelection => s.getLength
case _ => 0
}

def cursorIctx = InvocationContext(ssf.get, ctx.getOffset, selLen, locations)
def problemAtCursorIctx = problemAtCursor map (_ => cursorIctx)
def problemIctx = problemInLine map (al => InvocationContext(ssf.get, al.offset, 0, locations :+ al))

val ictx = problemAtCursorIctx orElse problemIctx getOrElse cursorIctx
val proposals = quickAssists flatMap (_ withInstance (_ compute ictx))
val sorted = proposals.flatten.sortBy(-_.getRelevance())

EditorUtils.withCurrentEditor { e =>
e.selectAndReveal(ictx.selectionStart, ictx.selectionLength)
None
}

if (sorted.isEmpty)
Array(NoProposals)
else
Expand Down

0 comments on commit 7f025a8

Please sign in to comment.