From b5d87b2cc113009aaf73f81abb5f42eeedaa7ec3 Mon Sep 17 00:00:00 2001 From: Alex Melnik Date: Mon, 5 Jan 2026 12:12:50 -0800 Subject: [PATCH] Add autocomplete support for 'outer' keyword Implements type computation for the 'outer' keyword, allowing autocomplete to work when accessing members of the enclosing object scope. Changes: - Add ComputeOuterType.kt to compute the type of outer references - Update ComputeExprType.kt to use computeOuterType() for outer expressions - Add test case for outer completion in CompletionTest.kt The implementation walks up the PSI tree to find the immediate enclosing object, then continues to the next enclosing object and returns its type, enabling proper member resolution. --- .../org/pkl/intellij/type/ComputeExprType.kt | 2 +- .../org/pkl/intellij/type/ComputeOuterType.kt | 137 ++++++++++++++++++ .../pkl/intellij/completion/CompletionTest.kt | 30 ++++ 3 files changed, 168 insertions(+), 1 deletion(-) create mode 100644 src/main/kotlin/org/pkl/intellij/type/ComputeOuterType.kt diff --git a/src/main/kotlin/org/pkl/intellij/type/ComputeExprType.kt b/src/main/kotlin/org/pkl/intellij/type/ComputeExprType.kt index a83b2133..19fdc0db 100644 --- a/src/main/kotlin/org/pkl/intellij/type/ComputeExprType.kt +++ b/src/main/kotlin/org/pkl/intellij/type/ComputeExprType.kt @@ -130,7 +130,7 @@ private fun PsiElement.doComputeExprType( (type?.toType(base, bindings, context) ?: inferExprTypeFromContext(base, bindings, context)) .instantiated(base, context) is PklThisExpr -> computeThisType(base, bindings, context) - is PklOuterExpr -> Type.Unknown // TODO + is PklOuterExpr -> computeOuterType(base, bindings, context) is PklSubscriptBinExpr -> { val receiverType = leftExpr.computeExprType(base, bindings, context) doComputeSubscriptExprType(receiverType, base, context) diff --git a/src/main/kotlin/org/pkl/intellij/type/ComputeOuterType.kt b/src/main/kotlin/org/pkl/intellij/type/ComputeOuterType.kt new file mode 100644 index 00000000..f6c56141 --- /dev/null +++ b/src/main/kotlin/org/pkl/intellij/type/ComputeOuterType.kt @@ -0,0 +1,137 @@ +/** + * Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@file:Suppress("DuplicatedCode") + +package org.pkl.intellij.type + +import com.intellij.psi.PsiElement +import org.pkl.intellij.packages.dto.PklProject +import org.pkl.intellij.psi.* + +/** + * Computes the type of `outer` for the given PSI element. + * + * The `outer` keyword refers to the enclosing object scope that is one level up from + * the immediate object scope. This function walks up the PSI tree to find the immediate + * enclosing object, then continues walking to find the next enclosing object and returns + * its type. + */ +fun PsiElement.computeOuterType( + base: PklBaseModule, + bindings: TypeParameterBindings, + context: PklProject? +): Type { + var element: PsiElement? = this + var memberPredicateExprSeen = false + var objectBodySeen = false + var skipNextObjectBody = false + var foundFirstObject = false + + while (element != null) { + when (element) { + is PklAmendExpr, + is PklNewExpr -> { + if (objectBodySeen) { + if (foundFirstObject) { + // This is the outer object we're looking for + val type = element.computeExprType(base, bindings, context).amending(base, context) + return when { + memberPredicateExprSeen -> { + val classType = type.toClassType(base, context) ?: return Type.Unknown + when { + classType.classEquals(base.listingType) -> classType.typeArguments[0] + classType.classEquals(base.mappingType) -> classType.typeArguments[1] + else -> Type.Unknown + } + } + else -> type + } + } else { + // This is the immediate object, mark it as found and continue searching + foundFirstObject = true + objectBodySeen = false + memberPredicateExprSeen = false + } + } + } + is PklExpr -> { + val parent = element.parent + if ( + parent is PklWhenGenerator && element === parent.conditionExpr || + parent is PklForGenerator && element === parent.iterableExpr || + parent is PklObjectEntry && element === parent.keyExpr + ) { + skipNextObjectBody = true + } else if (parent is PklMemberPredicate && element === parent.conditionExpr) { + memberPredicateExprSeen = true + } + } + is PklObjectBody -> + when { + skipNextObjectBody -> skipNextObjectBody = false + else -> objectBodySeen = true + } + is PklProperty, + is PklObjectElement, + is PklObjectEntry, + is PklMemberPredicate -> { + if (objectBodySeen) { + if (foundFirstObject) { + // This is the outer object we're looking for + val type = + element.computeResolvedImportType(base, bindings, context).amending(base, context) + return when { + memberPredicateExprSeen -> { + val classType = type.toClassType(base, context) ?: return Type.Unknown + when { + classType.classEquals(base.listingType) -> classType.typeArguments[0] + classType.classEquals(base.mappingType) -> classType.typeArguments[1] + else -> Type.Unknown + } + } + else -> type + } + } else { + // This is the immediate object, mark it as found and continue searching + foundFirstObject = true + objectBodySeen = false + memberPredicateExprSeen = false + } + } + } + is PklConstrainedType -> { + if (foundFirstObject) { + return element.type.toType(base, bindings, context) + } + } + is PklModule, + is PklClass, + is PklTypeAlias -> { + if (foundFirstObject) { + return element.computeResolvedImportType(base, bindings, context) + } + } + is PklAnnotation -> { + if (foundFirstObject) { + return element.typeName?.resolve(context).computeResolvedImportType(base, bindings, context) + } + } + } + element = element.parent + } + + return Type.Unknown +} diff --git a/src/test/kotlin/org/pkl/intellij/completion/CompletionTest.kt b/src/test/kotlin/org/pkl/intellij/completion/CompletionTest.kt index 7a36a4aa..bf23e790 100644 --- a/src/test/kotlin/org/pkl/intellij/completion/CompletionTest.kt +++ b/src/test/kotlin/org/pkl/intellij/completion/CompletionTest.kt @@ -100,6 +100,36 @@ class CompletionTest : PklTestCase() { assertThat(lookupStrings).contains("configMaps = ") } + fun `test complete from outer`() { + myFixture.configureByText( + PklFileType, + """ + class BirdSpec { + name: String + songs: Listing + function generateSong(_tune: String): Song = new { + tune = _tune + } + } + + class Song { + tune: String + } + + peacock: BirdSpec = new { + name = "peacock" + songs { + outer. + } + } + """ + .trimIndent() + ) + myFixture.completeBasic() + val lookupStrings = myFixture.lookupElementStrings + assertThat(lookupStrings).contains("generateSong", "name", "songs") + } + override val fixtureDir: Path? get() = Path.of("src/test/resources/completion") }