Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Improved UI for Scala completions.

- when a proposal is chosen, its parameter names are inserted
  in place of the arguments, and the editor is put in 'link' mode:
  Tab moves the caret to the next argument, Esc and ';' move the caret
  to the end of the argument list (and exits the linked mode). Hitting
  <enter> moves the caret to the end, unless it comes after an opening
  brace -> that means a closure is needed, and the editor is put out
  of the linked mode

- context information works now. A method signature (parameter names and
  types) are shown above the completion point, and the current argument
  is correctly highlighted (bold). When the cursor leaves the argument list,
  the context information window is now removed.
  • Loading branch information...
commit 28d379096a61018d01e6c39e5298ea9ea26945b1 1 parent 4c40dde
@dragos dragos authored
View
81 org.scala-ide.sdt.core.tests/src/scala/tools/eclipse/completion/CompletionTests.scala
@@ -13,8 +13,8 @@ import org.junit.Test
import scala.tools.eclipse.testsetup.TestProjectSetup
import org.eclipse.jdt.core.search.{SearchEngine, IJavaSearchConstants, IJavaSearchScope, SearchPattern, TypeNameRequestor}
import org.eclipse.jdt.core.IJavaElement
-
import org.junit.Ignore
+import scala.tools.nsc.util.OffsetPosition
object CompletionTests extends TestProjectSetup("completion")
@@ -26,10 +26,7 @@ class CompletionTests {
import org.eclipse.jface.text.IDocument
import org.eclipse.jdt.ui.text.java.ContentAssistInvocationContext
- /**
- * @param withImportProposal take in account proposal for types not imported yet
- */
- private def runTest(path2source: String, withImportProposal: Boolean)(expectedCompletions: List[String]*) {
+ private def withCompletions(path2source: String)(body: (Int, OffsetPosition, List[CompletionProposal]) => Unit) {
val unit = compilationUnit(path2source).asInstanceOf[ScalaCompilationUnit]
// first, 'open' the file by telling the compiler to load it
@@ -49,8 +46,8 @@ class CompletionTests {
val content = unit.getContents.mkString
val completion = new ScalaCompletions
- for (i <- 0 until positions.size) yield {
- val pos = positions(i)
+ for (i <- 0 until positions.size) {
+ val pos = positions(i)
val position = new scala.tools.nsc.util.OffsetPosition(src, pos)
var wordRegion = ScalaWordFinder.findWord(content, position.offset.get)
@@ -74,35 +71,42 @@ class CompletionTests {
val completions: List[ICompletionProposal] = completion.computeCompletionProposals(context, monitor).map(_.asInstanceOf[ICompletionProposal]).toList
*/
- var completions = completion.findCompletions(wordRegion)(pos+1, unit)(src, compiler)
-
- if (!withImportProposal)
- completions= completions.filter((c) => !c.needImport)
-
- // remove parens as the compiler trees' printer has been slightly modified in 2.10
- // (and we need the test to pass for 2.9.0/-1 and 2.8.x as well).
- val completionsNoParens: List[String] = completions.map(c => normalizeCompletion(c.display)).sorted
- val expectedNoParens: List[String] = expectedCompletions(i).map(normalizeCompletion).sorted
-
- println("Found following completions @ position %d (%d,%d):".format(pos, position.line, position.column))
- completionsNoParens.foreach(e => println("\t" + e))
- println()
-
- println("Expected completions:")
- expectedNoParens.foreach(e => println("\t" + e))
- println()
-
- assertTrue("Found %d completions @ position %d (%d,%d), Expected %d"
- .format(completionsNoParens.size, pos, position.line, position.column, expectedNoParens.size),
- completionsNoParens.size == expectedNoParens.size) // <-- checked condition
-
- completionsNoParens.zip(expectedNoParens).foreach {
- case (found, expected) =>
- assertTrue("Found `%s`, expected `%s`".format(found, expected), found == expected)
- }
+ body(i, position, completion.findCompletions(wordRegion)(pos+1, unit)(src, compiler))
}
}()
}
+
+ /** @param withImportProposal take in account proposal for types not imported yet
+ */
+ private def runTest(path2source: String, withImportProposal: Boolean)(expectedCompletions: List[String]*) {
+
+ withCompletions(path2source) { (i, position, compl) =>
+
+ var completions = if (!withImportProposal) compl.filter(!_.needImport) else compl
+
+ // remove parens as the compiler trees' printer has been slightly modified in 2.10
+ // (and we need the test to pass for 2.9.0/-1 and 2.8.x as well).
+ val completionsNoParens: List[String] = completions.map(c => normalizeCompletion(c.display)).sorted
+ val expectedNoParens: List[String] = expectedCompletions(i).map(normalizeCompletion).sorted
+
+ println("Found following completions @ position (%d,%d):".format(position.line, position.column))
+ completionsNoParens.foreach(e => println("\t" + e))
+ println()
+
+ println("Expected completions:")
+ expectedNoParens.foreach(e => println("\t" + e))
+ println()
+
+ assertTrue("Found %d completions @ position (%d,%d), Expected %d"
+ .format(completionsNoParens.size, position.line, position.column, expectedNoParens.size),
+ completionsNoParens.size == expectedNoParens.size) // <-- checked condition
+
+ completionsNoParens.zip(expectedNoParens).foreach {
+ case (found, expected) =>
+ assertTrue("Found `%s`, expected `%s`".format(found, expected), found == expected)
+ }
+ }
+ }
/** Transform the given completion proposal into a string that is (hopefully)
* compiler-version independent.
@@ -142,5 +146,14 @@ class CompletionTests {
runTest("ticket_1000654/Ticket1000654.scala", true)(oraclePos10_13)
}
-
+
+ @Test
+ def ticket1000772() {
+ val OracleNames = List(List("param1", "param2"), List("secondSectionParam1"))
+ withCompletions("ticket_1000772/CompletionsWithName.scala") { (idx, position, completions) =>
+ assertEquals("Only one completion expected at (%d, %d)".format(position.line, position.column), 1, completions.size)
+ assertEquals("Expected the following names: %s".format(OracleNames),
+ OracleNames, completions(0).explicitParamNames)
+ }
+ }
}
View
15 org.scala-ide.sdt.core.tests/test-workspace/completion/src/ticket_1000772/CompletionsWithName.scala
@@ -0,0 +1,15 @@
+package ticket_1000772
+
+class ForTesting {
+ def method(param1: String, param2: String)(secondSectionParam1: Int)(implicit x: Int) {
+ }
+}
+
+class Foo {
+
+ def bar {
+ val x: ForTesting = null
+
+ x.m /*!*/
+ }
+}
View
6 org.scala-ide.sdt.core/src/scala/tools/eclipse/ScalaPresentationCompiler.scala
@@ -253,6 +253,11 @@ class ScalaPresentationCompiler(project : ScalaProject, settings : Settings)
val contextString = sym.paramss.map(_.map(p => "%s: %s".format(p.decodedName, p.tpe)).mkString("(", ", ", ")")).mkString("")
+ val paramNames = for {
+ section <- sym.paramss
+ if section.nonEmpty && !section.head.isImplicit
+ } yield for (param <- section) yield param.name.toString
+
import scala.tools.eclipse.completion.HasArgs
CompletionProposal(kind,
start,
@@ -263,6 +268,7 @@ class ScalaPresentationCompiler(project : ScalaProject, settings : Settings)
relevance,
HasArgs.from(sym.paramss),
sym.isJavaDefined,
+ paramNames,
sym.fullName,
false)
}
View
1  org.scala-ide.sdt.core/src/scala/tools/eclipse/completion/CompletionProposal.scala
@@ -28,6 +28,7 @@ case class CompletionProposal(kind: MemberKind.Value,
relevance: Int,
hasArgs: HasArgs.Value,
isJava: Boolean,
+ explicitParamNames: List[List[String]], // parameter names (excluding any implicit parameter sections)
fullyQualifiedName: String, // for Class, Trait, Type, Objects: the fully qualified name
needImport: Boolean // for Class, Trait, Type, Objects: import statement has to be added
)
View
1  org.scala-ide.sdt.core/src/scala/tools/eclipse/completion/ScalaCompletions.scala
@@ -117,6 +117,7 @@ class ScalaCompletions extends HasLogger {
50,
HasArgs.NoArgs,
true,
+ List(),
fullyQualifiedName,
true)
}
View
108 org.scala-ide.sdt.core/src/scala/tools/eclipse/ui/ScalaCompletionProposal.scala
@@ -13,6 +13,14 @@ import org.eclipse.jdt.internal.ui.JavaPluginImages
import refactoring.EditorHelpers
import refactoring.EditorHelpers._
import scala.tools.refactoring.implementations.AddImportStatement
+import org.eclipse.jface.text.link._
+import org.eclipse.jface.text.Position
+import org.eclipse.ui.texteditor.link.EditorLinkedModeUI
+import org.eclipse.jdt.internal.ui.text.java.AbstractJavaCompletionProposal.ExitPolicy
+import org.eclipse.jface.text.link.LinkedModeUI.IExitPolicy
+import org.eclipse.swt.events.VerifyEvent
+import org.eclipse.jface.text.link.LinkedModeUI.ExitFlags
+import org.eclipse.swt.SWT
/** A UI class for displaying completion proposals.
@@ -47,11 +55,28 @@ class ScalaCompletionProposal(proposal: CompletionProposal, selectionProvider: I
def getImage = image
- val completionString = if (hasArgs == HasArgs.NoArgs) completion else completion + "()"
+ /** The string that will be inserted in the document if this proposal is chosen.
+ * It consists of the method name, followed by all explicit parameter sections,
+ * and inside each section the parameter names, delimited by commas.
+ */
+ val completionString =
+ if (hasArgs == HasArgs.NoArgs)
+ completion
+ else {
+ val buffer = new StringBuffer(completion)
+
+ for (section <- explicitParamNames)
+ buffer.append(section.mkString("(", ", ", ")"))
+ buffer.toString
+ }
+ /** Position after the opening parenthesis of this proposal */
+ val startOfArgumentList = startPos + completion.length + 1
+
+ /** The information that is displayed in a small hover window above the completion, showing parameter names and types. */
def getContextInformation(): IContextInformation =
- if (tooltip.size > 0)
- new ScalaContextInformation(display, tooltip, image)
+ if (tooltip.length > 0)
+ new ScalaContextInformation(display, tooltip, image, startOfArgumentList)
else null
/**
@@ -64,7 +89,7 @@ class ScalaCompletionProposal(proposal: CompletionProposal, selectionProvider: I
*/
def getStyledDisplayString() : StyledString = {
val styledString= new StyledString(display)
- if (displayDetail != null && displayDetail.size > 0)
+ if (displayDetail != null && displayDetail.length > 0)
styledString.append(" - ", StyledString.QUALIFIER_STYLER).append(displayDetail, StyledString.QUALIFIER_STYLER)
styledString
}
@@ -86,6 +111,7 @@ class ScalaCompletionProposal(proposal: CompletionProposal, selectionProvider: I
// present in the source file.
val viewCaretOffset = viewer.getTextWidget().getCaretOffset()
viewer.getTextWidget().setCaretOffset(viewCaretOffset -1 )
+ addArgumentTemplates(d, viewer)
case _ => ()
}
if (needImport) { // add an import statement if required
@@ -101,9 +127,81 @@ class ScalaCompletionProposal(proposal: CompletionProposal, selectionProvider: I
}
}
def getTriggerCharacters = null
- def getContextInformationPosition = 0
+ def getContextInformationPosition = startOfArgumentList
+
def isValidFor(d: IDocument, pos: Int) =
prefixMatches(completion.toArray, d.get.substring(startPos, pos).toArray)
+
+ /** Insert a completion proposal, with placeholders for each explicit argument.
+ * For each argument, it inserts its name, and puts the editor in linked mode.
+ * This means that TAB can be used to navigate to the next argument, and Enter or Esc
+ * can be used to exit this mode.
+ */
+ def addArgumentTemplates(document: IDocument, textViewer: ITextViewer) {
+ val model= new LinkedModeModel()
+
+ document.addPositionCategory(ScalaProposalCategory)
+ var offset = startPos + completion.length
+
+ for (section <- explicitParamNames) {
+ offset += 1 // open parenthesis
+ var idx = 0 // the index of the current argument
+ for (proposal <- section) {
+ val group = new LinkedPositionGroup();
+ val positionOffset = offset + 2 * idx // each argument is followed by ", "
+ val positionLength = proposal.length
+ offset += positionLength
+
+ document.addPosition(ScalaProposalCategory, new Position(positionOffset, positionLength))
+ group.addPosition(new LinkedPosition(document, positionOffset, positionLength, LinkedPositionGroup.NO_STOP))
+ model.addGroup(group);
+ idx += 1
+ }
+ offset += 1 + 2 * (idx - 1) // close parenthesis around section (and the last argument isn't followed by comma and space)
+ }
+
+ model.addLinkingListener(new ILinkedModeListener() {
+ def left(environment: LinkedModeModel, flags: Int) {
+ document.removePositionCategory(ScalaProposalCategory);
+ }
+
+ def suspend(environment: LinkedModeModel) {}
+ def resume(environment: LinkedModeModel, flags: Int) {}
+ })
+
+ model.forceInstall();
+
+ val ui = mkEditorLinkedMode(document, textViewer, model)
+ ui.enter();
+ }
+
+ /** Prepare a linked mode for the given editor. */
+ private def mkEditorLinkedMode(document: IDocument, textViewer: ITextViewer, model: LinkedModeModel): EditorLinkedModeUI = {
+ val ui = new EditorLinkedModeUI(model, textViewer)
+ ui.setExitPosition(textViewer, startPos + completionString.length(), 0, Integer.MAX_VALUE)
+ ui.setExitPolicy(new IExitPolicy {
+ def doExit(environment: LinkedModeModel, event: VerifyEvent, offset: Int, length: Int) = {
+ event.character match {
+ case ';' =>
+ // go to the end of the completion proposal
+ new ExitFlags(ILinkedModeListener.UPDATE_CARET, !environment.anyPositionContains(offset))
+
+ case SWT.CR if (offset > 0 && document.getChar(offset - 1) == '{') =>
+ // if we hit enter after opening a brace, it's probably a closure. Exit linked mode
+ new ExitFlags(ILinkedModeListener.EXIT_ALL, true)
+
+ case _ =>
+ // stay in linked mode otherwise
+ null
+ }
+ }
+ });
+ ui.setCyclingMode(LinkedModeUI.CYCLE_WHEN_NO_PARENT);
+ ui.setDoContextInfo(true);
+ ui
+ }
+
+ private val ScalaProposalCategory = "ScalaProposal"
}
object ScalaCompletionProposal {
View
4 org.scala-ide.sdt.core/src/scala/tools/eclipse/ui/ScalaContextInformation.scala
@@ -4,12 +4,12 @@ import org.eclipse.jface.text.contentassist.{IContextInformation,IContextInforma
import org.eclipse.swt.graphics.Image
class ScalaContextInformation(
- display: String, info: String, image: Image)
+ display: String, info: String, image: Image, pos: Int)
extends IContextInformation
with IContextInformationExtension {
def getContextDisplayString() = display
def getImage() = image
def getInformationDisplayString() = info
- def getContextInformationPosition(): Int = 0
+ def getContextInformationPosition(): Int = pos
}

1 comment on commit 28d3790

@ijuma

Nice to see work being done in polish items like this.

Please sign in to comment.
Something went wrong with that request. Please try again.