Smart completion: imports insertion + fixed both behaviour and presentation of qualified members in lookup

This commit is contained in:
Valentin Kipyatkov
2013-12-25 22:26:48 +04:00
parent f303b6dae5
commit 39f07b8354
25 changed files with 223 additions and 56 deletions
@@ -6,6 +6,9 @@
<item name='com.intellij.codeInsight.lookup.Lookup com.intellij.openapi.editor.Editor getEditor()'>
<annotation name='org.jetbrains.annotations.NotNull'/>
</item>
<item name='com.intellij.codeInsight.lookup.Lookup java.util.List&lt;com.intellij.codeInsight.lookup.LookupElement&gt; getItems()'>
<annotation name='org.jetbrains.annotations.NotNull'/>
</item>
<item name='com.intellij.codeInsight.lookup.LookupElement void handleInsert(com.intellij.codeInsight.completion.InsertionContext) 0'>
<annotation name='org.jetbrains.annotations.NotNull'/>
</item>
@@ -85,6 +85,7 @@ public final class FqNameUnsafe extends FqNameBase {
@Override
@NotNull
public String asString() {
return fqName;
@@ -267,6 +268,7 @@ public final class FqNameUnsafe extends FqNameBase {
@Override
@NotNull
public String toString() {
return isRoot() ? ROOT_NAME.asString() : fqName;
}
@@ -21,6 +21,9 @@ import org.jetbrains.jet.lang.descriptors.DeclarationDescriptor;
import org.jetbrains.jet.lang.descriptors.FunctionDescriptor;
import org.jetbrains.jet.lang.descriptors.ValueParameterDescriptor;
import org.jetbrains.jet.lang.types.JetType;
import org.jetbrains.jet.lang.types.TypeProjection;
import java.util.List;
public interface DescriptorRenderer extends Renderer<DeclarationDescriptor> {
DescriptorRenderer COMPACT_WITH_MODIFIERS = new DescriptorRendererBuilder().setWithDefinedIn(false).build();
@@ -61,6 +64,9 @@ public interface DescriptorRenderer extends Renderer<DeclarationDescriptor> {
@NotNull
String renderType(@NotNull JetType type);
@NotNull
String renderTypeArguments(@NotNull List<TypeProjection> typeArguments);
@NotNull
@Override
String render(@NotNull DeclarationDescriptor declarationDescriptor);
@@ -201,6 +201,17 @@ public class DescriptorRendererImpl implements DescriptorRenderer {
return escape(renderTypeWithoutEscape(type));
}
@NotNull
@Override
public String renderTypeArguments(@NotNull List<TypeProjection> typeArguments) {
if (typeArguments.isEmpty()) return "";
StringBuilder sb = new StringBuilder();
sb.append("<");
appendTypeProjections(typeArguments, sb);
sb.append(">");
return sb.toString();
}
private String renderTypeWithoutEscape(@NotNull JetType type) {
if (type == CANT_INFER_LAMBDA_PARAM_TYPE || type == CANT_INFER_TYPE_PARAMETER) {
return "???";
@@ -29,6 +29,9 @@ import com.intellij.psi.util.PsiTreeUtil
import com.intellij.openapi.editor.EditorModificationUtil
import org.jetbrains.jet.plugin.codeInsight.ImplementMethodsHandler
import com.intellij.psi.PsiDocumentManager
import com.intellij.openapi.application.ApplicationManager
import org.jetbrains.jet.plugin.project.AnalyzerFacadeWithCache
import org.jetbrains.jet.plugin.codeInsight.ShortenReferences
trait SmartCompletionData{
fun accepts(descriptor: DeclarationDescriptor): Boolean
@@ -133,30 +136,31 @@ private fun typeInstantiationItems(expectedType: JetType, resolveSession: Cancel
var lookupString = lookupElement.getLookupString()
val typeArgs = expectedType.getArguments()
//TODO: shouldn't be method in DescriptorRenderer to render type arguments?
val typeArgsText =
if (typeArgs.isEmpty())
""
else
typeArgs.map { DescriptorRenderer.TEXT.renderType(it.getType()) }.makeString(", ", "<", ">")
var itemText = lookupElement.getLookupString() + typeArgsText
var itemText = lookupString + DescriptorRenderer.TEXT.renderTypeArguments(typeArgs)
val insertHandler: InsertHandler<LookupElement>
var suppressAutoInsertion: Boolean = false
val typeText = DescriptorUtils.getFqName(classifier).toString() + DescriptorRenderer.SOURCE_CODE.renderTypeArguments(typeArgs)
if (classifier.getModality() == Modality.ABSTRACT) {
val constructorParenthesis = if (classifier.getKind() != ClassKind.TRAIT) "()" else ""
itemText += constructorParenthesis
itemText = "object: " + itemText + "{...}"
lookupString = "object" //?
insertHandler = object: InsertHandler<LookupElement>{
override fun handleInsert(context: InsertionContext, item: LookupElement) {
//TODO: insert import
val editor = context.getEditor()
EditorModificationUtil.insertStringAtCaret(editor, ": " + classifier.getName() + constructorParenthesis + " {}")
editor.getCaretModel().moveToOffset(editor.getCaretModel().getOffset() - 1)
PsiDocumentManager.getInstance(context.getProject()).commitAllDocuments();
ImplementMethodsHandler().invoke(context.getProject(), editor, context.getFile(), true)
insertHandler = InsertHandler<LookupElement> { (context, item) ->
val editor = context.getEditor()
val startOffset = context.getStartOffset()
val text = "object: $typeText$constructorParenthesis {}"
editor.getDocument().replaceString(startOffset, context.getTailOffset(), text)
editor.getCaretModel().moveToOffset(startOffset + text.length - 1)
PsiDocumentManager.getInstance(context.getProject()).commitAllDocuments();
val file = context.getFile() as JetFile
val element = PsiTreeUtil.findElementOfClassAtRange(file, startOffset, startOffset + text.length, javaClass<JetElement>())
if (element != null) {
ShortenReferences.process(element)
}
ImplementMethodsHandler().invoke(context.getProject(), editor, context.getFile(), true)
}
suppressAutoInsertion = true
}
@@ -170,7 +174,23 @@ private fun typeInstantiationItems(expectedType: JetType, resolveSession: Cancel
if (constructors.first().getValueParameters().isEmpty()) CaretPosition.AFTER_BRACKETS else CaretPosition.IN_BRACKETS
else
CaretPosition.IN_BRACKETS
insertHandler = JetFunctionInsertHandler(caretPosition, BracketType.PARENTHESIS)
insertHandler = InsertHandler<LookupElement> { (context, item) ->
val editor = context.getEditor()
val startOffset = context.getStartOffset()
val text = typeText + "()"
editor.getDocument().replaceString(startOffset, context.getTailOffset(), text)
val endOffset = startOffset + text.length
editor.getCaretModel().moveToOffset(if (caretPosition == CaretPosition.IN_BRACKETS) endOffset - 1 else endOffset)
//TODO: autopopup parameter info and other functionality from JetFunctionInsertHandler
PsiDocumentManager.getInstance(context.getProject()).commitAllDocuments();
val file = context.getFile() as JetFile
val element = PsiTreeUtil.findElementOfClassAtRange(file, startOffset, endOffset, javaClass<JetElement>())
if (element != null) {
ShortenReferences.process(element)
}
}
}
val lookupElementDecorated = object: LookupElementDecorator<LookupElement>(lookupElement){
@@ -311,36 +331,62 @@ private fun staticMembers(context: JetExpression, expectedType: JetType, resolve
fun toLookupElement(descriptor: DeclarationDescriptor): LookupElement {
val lookupElement = DescriptorLookupConverter.createLookupElement(resolveSession, bindingContext, descriptor)
val lookupString = classDescriptor.getName().asString() + "." + lookupElement.getLookupString()
var itemText = lookupString
val insertHandler: InsertHandler<LookupElement>?
val qualifierPresentation = classDescriptor.getName().asString()
val lookupString = qualifierPresentation + "." + lookupElement.getLookupString()
val qualifierText = DescriptorUtils.getFqName(classDescriptor).asString() //TODO: escape keywords
val caretPosition: CaretPosition?
if (descriptor is FunctionDescriptor) {
itemText += "()"
val caretPosition = if (descriptor.getValueParameters().empty) CaretPosition.AFTER_BRACKETS else CaretPosition.IN_BRACKETS
insertHandler = JetFunctionInsertHandler(caretPosition, BracketType.PARENTHESIS)
caretPosition = if (descriptor.getValueParameters().empty) CaretPosition.AFTER_BRACKETS else CaretPosition.IN_BRACKETS
}
else {
insertHandler = null
caretPosition = null
}
val insertHandler = InsertHandler<LookupElement> { (context, item) ->
val editor = context.getEditor()
val startOffset = context.getStartOffset()
var text = qualifierText + "." + descriptor.getName().asString() //TODO: escape
if (descriptor is FunctionDescriptor) {
text += "()"
//TODO: autopopup parameter info and other functionality from JetFunctionInsertHandler
}
editor.getDocument().replaceString(startOffset, context.getTailOffset(), text)
val endOffset = startOffset + text.length
editor.getCaretModel().moveToOffset(if (caretPosition == CaretPosition.IN_BRACKETS) endOffset - 1 else endOffset)
PsiDocumentManager.getInstance(context.getProject()).commitAllDocuments();
val file = context.getFile() as JetFile
val element = PsiTreeUtil.findElementOfClassAtRange(file, startOffset, startOffset + qualifierText.length, javaClass<JetElement>())
if (element != null) {
ShortenReferences.process(element)
}
}
return object: LookupElementDecorator<LookupElement>(lookupElement){
override fun getLookupString() = lookupString
override fun renderElement(presentation: LookupElementPresentation) {
lookupElement.renderElement(presentation)
presentation.setItemText(itemText)
presentation.setTailText(" (" + DescriptorUtils.getFqName(classDescriptor.getContainingDeclaration()) + ")")
presentation.setItemText(qualifierPresentation + "." + presentation.getItemText())
val tailText = " (" + DescriptorUtils.getFqName(classDescriptor.getContainingDeclaration()) + ")"
if (descriptor is FunctionDescriptor) {
presentation.appendTailText(tailText, true)
}
else{
presentation.setTailText(tailText, true)
}
if (presentation.getTypeText().isNullOrEmpty()) {
presentation.setTypeText(DescriptorRenderer.TEXT.renderType(classDescriptor.getDefaultType()))
}
}
override fun handleInsert(context: InsertionContext) {
if (insertHandler != null) {
insertHandler.handleInsert(context, lookupElement)
}
else{
lookupElement.handleInsert(context)
}
insertHandler.handleInsert(context, lookupElement)
}
}
}
@@ -0,0 +1 @@
val c: java.io.Closeable = <caret>
@@ -0,0 +1,7 @@
import java.io.Closeable
val c: java.io.Closeable = object: Closeable {
override fun close() {
throw UnsupportedOperationException()
}
}
@@ -0,0 +1,8 @@
import java.util.HashMap
import java.util.List
fun foo(p: HashMap<String, List<Int>>){}
fun f(){
foo(<caret>)
}
@@ -0,0 +1,8 @@
import java.util.HashMap
import java.util.List
fun foo(p: HashMap<String, List<Int>>){}
fun f(){
foo(HashMap<String, List<Int>>())
}
@@ -1,5 +1,7 @@
fun foo(p: java.util.BitSet){}
class X<T> {
fun foo(p: java.util.HashMap<java.io.File, T>){}
fun f(){
foo(<caret>)
fun f(){
foo(<caret>)
}
}
@@ -1,7 +1,10 @@
import java.util.BitSet
import java.util.HashMap
import java.io.File
fun foo(p: java.util.BitSet){}
class X<T> {
fun foo(p: java.util.HashMap<java.io.File, T>){}
fun f(){
foo(BitSet(<caret>))
fun f(){
foo(HashMap<File, T>(<caret>))
}
}
@@ -0,0 +1,7 @@
class X<T> {
fun foo(p: java.util.HashMap<T, java.util.AbstractMap<T, java.io.File>>){}
fun f(){
foo(<caret>)
}
}
@@ -0,0 +1,11 @@
import java.util.HashMap
import java.util.AbstractMap
import java.io.File
class X<T> {
fun foo(p: java.util.HashMap<T, java.util.AbstractMap<T, java.io.File>>){}
fun f(){
foo(HashMap<T, AbstractMap<T, File>>(<caret>))
}
}
@@ -0,0 +1,8 @@
enum class Foo {
X
Y
}
fun foo(){
val f : Foo = <caret>
}
@@ -0,0 +1,8 @@
enum class Foo {
X
Y
}
fun foo(){
val f : Foo = Foo.X<caret>
}
@@ -0,0 +1,3 @@
fun foo(){
val e : java.lang.annotation.ElementType = <caret>
}
@@ -0,0 +1,5 @@
import java.lang.annotation.ElementType
fun foo(){
val e : java.lang.annotation.ElementType = ElementType.FIELD<caret>
}
@@ -0,0 +1,5 @@
import java.util.Locale
fun foo(){
var l : Locale = <caret>
}
@@ -0,0 +1,5 @@
import java.util.Locale
fun foo(){
var l : Locale = Locale.ENGLISH<caret>
}
@@ -1,5 +1,5 @@
import java.util.Locale
fun foo(){
var l : java.util.Locale = Locale.ENGLISH
var l : java.util.Locale = Locale.ENGLISH<caret>
}
@@ -0,0 +1,3 @@
fun foo(){
val l : java.util.Calendar = <caret>
}
@@ -0,0 +1,5 @@
import java.util.Calendar
fun foo(){
val l : java.util.Calendar = Calendar.getInstance(<caret>)
}
@@ -1,5 +1,7 @@
fun foo(){
val l : java.lang.Thread = <caret>
val l : java.util.Calendar = <caret>
}
// EXIST: { lookupString:"Thread.currentThread", itemText:"Thread.currentThread()", tailText:" (java.lang)", typeText:"Thread" }
// EXIST: { lookupString:"Calendar.getInstance", itemText:"Calendar.getInstance", tailText:"() (java.util)", typeText:"Calendar" }
// EXIST: { lookupString:"Calendar.getInstance", itemText:"Calendar.getInstance", tailText:"(TimeZone) (java.util)", typeText:"Calendar" }
// EXIST: { lookupString:"Calendar.getInstance", itemText:"Calendar.getInstance", tailText:"(TimeZone, Locale) (java.util)", typeText:"Calendar" }
@@ -68,31 +68,31 @@ public abstract class CompletionHandlerTestBase() : JetLightCodeInsightFixtureTe
private fun getExistentLookupElement(lookupString : String?, tailText : String?) : LookupElement? {
val lookup = LookupManager.getInstance(getProject())?.getActiveLookup() as LookupImpl?
if (lookup == null) return null
var foundElement : LookupElement? = null
if (lookup != null) {
val presentation = LookupElementPresentation()
for (lookupElement in lookup.getItems()!!) {
val lookupOk : Boolean
if (lookupString != null) {
lookupOk = lookupElement.getLookupString().contains(lookupString)
}
else {
lookupOk = true
}
val presentation = LookupElementPresentation()
for (lookupElement in lookup.getItems()) {
val lookupOk : Boolean
if (lookupString != null) {
lookupOk = lookupElement.getLookupString().contains(lookupString)
}
else {
lookupOk = true
}
if (lookupOk) {
val tailOk : Boolean
if (tailText != null) {
lookupElement.renderElement(presentation)
val itemTailText : String? = presentation.getTailText()
tailOk = itemTailText != null && (itemTailText.contains(tailText))
tailOk = itemTailText != null && itemTailText.contains(tailText)
}
else {
tailOk = true
}
if (lookupOk && tailOk) {
if (tailOk) {
if (foundElement != null) {
Assert.fail("Several elements satisfy to completion restrictions")
}
@@ -102,6 +102,7 @@ public abstract class CompletionHandlerTestBase() : JetLightCodeInsightFixtureTe
}
}
if (foundElement == null) error("No element satisfy completion restrictions")
return foundElement
}
@@ -41,11 +41,17 @@ public class SmartCompletionHandlerTest() : CompletionHandlerTestBase() {
fun testConstructorWithParameters() = doTest()
fun testConstructorForNullable() = doTest()
fun testConstructorForJavaClass() = doTest()
//fun testConstructorInsertsImport() = doTest() //TODO
fun testConstructorForGenericType() = doTest()
fun testConstructorInsertsImport() = doTest()
fun testConstructorInsertsImport2() = doTest()
fun testJavaStaticMethod() = doTest(1, "Thread.currentThread", null, '\n')
fun testJavaStaticMethodInsertsImport() = doTest(1, "Calendar.getInstance", "(TimeZone)", '\n')
fun testClassObjectMethod1() = doTest(1, "K.bar", null, '\n')
fun testClassObjectMethod2() = doTest(1, "K.bar", null, '\n')
//fun testJavaStaticFieldInsertImport() = doTest(1, "Locale.ENGLISH", null, '\n') //TODO
fun testEnumMember() = doTest(1, "Foo.X", null, '\n')
fun testJavaEnumMemberInsertsImport() = doTest(1, "ElementType.FIELD", null, '\n')
fun testJavaStaticField() = doTest(1, "Locale.ENGLISH", null, '\n')
fun testJavaStaticFieldInsertImport() = doTest(1, "Locale.ENGLISH", null, '\n')
fun testTabReplaceIdentifier() = doTest(1, "ss", null, '\t')
fun testTabReplaceExpression() = doTest(1, "sss", null, '\t')
fun testTabReplaceExpression2() = doTest(1, "sss", null, '\t')
@@ -54,4 +60,5 @@ public class SmartCompletionHandlerTest() : CompletionHandlerTestBase() {
fun testAnonymousObject1() = doTest(1, "object", null, '\t')
fun testAnonymousObject2() = doTest(1, "object", null, '\t')
fun testAnonymousObject3() = doTest(1, "object", null, '\t')
fun testAnonymousObjectInsertsImport() = doTest(1, "object", null, '\t')
}