Compare commits
2 Commits
555c9b94de
...
3ef68d8bb4
| Author | SHA1 | Date | |
|---|---|---|---|
| 3ef68d8bb4 | |||
| 72bb6ae67b |
@ -30,7 +30,6 @@ import com.intellij.patterns.PlatformPatterns
|
|||||||
import com.intellij.psi.PsiFile
|
import com.intellij.psi.PsiFile
|
||||||
import com.intellij.util.ProcessingContext
|
import com.intellij.util.ProcessingContext
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import net.sergeych.lyng.highlight.offsetOf
|
|
||||||
import net.sergeych.lyng.idea.LyngLanguage
|
import net.sergeych.lyng.idea.LyngLanguage
|
||||||
import net.sergeych.lyng.idea.highlight.LyngTokenTypes
|
import net.sergeych.lyng.idea.highlight.LyngTokenTypes
|
||||||
import net.sergeych.lyng.idea.settings.LyngFormatterSettings
|
import net.sergeych.lyng.idea.settings.LyngFormatterSettings
|
||||||
@ -144,11 +143,6 @@ class LyngCompletionContributor : CompletionContributor() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// In global context, add params in scope first (engine does not include them)
|
|
||||||
if (memberDotPos == null && mini != null) {
|
|
||||||
offerParamsInScope(emit, mini, text, caret)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Render engine items
|
// Render engine items
|
||||||
for (ci in engineItems) {
|
for (ci in engineItems) {
|
||||||
val builder = when (ci.kind) {
|
val builder = when (ci.kind) {
|
||||||
@ -167,7 +161,7 @@ class LyngCompletionContributor : CompletionContributor() {
|
|||||||
Kind.Enum -> LookupElementBuilder.create(ci.name)
|
Kind.Enum -> LookupElementBuilder.create(ci.name)
|
||||||
.withIcon(AllIcons.Nodes.Enum)
|
.withIcon(AllIcons.Nodes.Enum)
|
||||||
Kind.Value -> LookupElementBuilder.create(ci.name)
|
Kind.Value -> LookupElementBuilder.create(ci.name)
|
||||||
.withIcon(AllIcons.Nodes.Field)
|
.withIcon(AllIcons.Nodes.Variable)
|
||||||
.let { b -> if (!ci.typeText.isNullOrBlank()) b.withTypeText(ci.typeText, true) else b }
|
.let { b -> if (!ci.typeText.isNullOrBlank()) b.withTypeText(ci.typeText, true) else b }
|
||||||
Kind.Field -> LookupElementBuilder.create(ci.name)
|
Kind.Field -> LookupElementBuilder.create(ci.name)
|
||||||
.withIcon(AllIcons.Nodes.Field)
|
.withIcon(AllIcons.Nodes.Field)
|
||||||
@ -545,27 +539,6 @@ class LyngCompletionContributor : CompletionContributor() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- MiniAst-based inference helpers ---
|
|
||||||
|
|
||||||
private fun offerParamsInScope(emit: (com.intellij.codeInsight.lookup.LookupElement) -> Unit, mini: MiniScript, text: String, caret: Int) {
|
|
||||||
val src = mini.range.start.source
|
|
||||||
// Find function whose body contains caret or whose whole range contains caret
|
|
||||||
val fns = mini.declarations.filterIsInstance<MiniFunDecl>()
|
|
||||||
for (fn in fns) {
|
|
||||||
val start = src.offsetOf(fn.range.start)
|
|
||||||
val end = src.offsetOf(fn.range.end).coerceAtMost(text.length)
|
|
||||||
if (caret in start..end) {
|
|
||||||
for (p in fn.params) {
|
|
||||||
val builder = LookupElementBuilder.create(p.name)
|
|
||||||
.withIcon(AllIcons.Nodes.Variable)
|
|
||||||
.withTypeText(typeOf(p.type), true)
|
|
||||||
emit(builder)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Lenient textual import extractor (duplicated from QuickDoc privately)
|
// Lenient textual import extractor (duplicated from QuickDoc privately)
|
||||||
private fun extractImportsFromText(text: String): List<String> {
|
private fun extractImportsFromText(text: String): List<String> {
|
||||||
val result = LinkedHashSet<String>()
|
val result = LinkedHashSet<String>()
|
||||||
|
|||||||
@ -2030,12 +2030,11 @@ class Compiler(
|
|||||||
run {
|
run {
|
||||||
val declRange = MiniRange(startPos, cc.currentPos())
|
val declRange = MiniRange(startPos, cc.currentPos())
|
||||||
val bases = baseSpecs.map { it.name }
|
val bases = baseSpecs.map { it.name }
|
||||||
// Collect constructor fields declared as val/var in primary constructor
|
// Collect constructor fields declared in primary constructor
|
||||||
val ctorFields = mutableListOf<MiniCtorField>()
|
val ctorFields = mutableListOf<MiniCtorField>()
|
||||||
constructorArgsDeclaration?.let { ad ->
|
constructorArgsDeclaration?.let { ad ->
|
||||||
for (p in ad.params) {
|
for (p in ad.params) {
|
||||||
val at = p.accessType
|
val at = p.accessType
|
||||||
if (at != null) {
|
|
||||||
val mutable = at == AccessType.Var
|
val mutable = at == AccessType.Var
|
||||||
ctorFields += MiniCtorField(
|
ctorFields += MiniCtorField(
|
||||||
name = p.name,
|
name = p.name,
|
||||||
@ -2045,7 +2044,6 @@ class Compiler(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
val node = MiniClassDecl(
|
val node = MiniClassDecl(
|
||||||
range = declRange,
|
range = declRange,
|
||||||
name = nameToken.value,
|
name = nameToken.value,
|
||||||
|
|||||||
@ -42,6 +42,31 @@ object LyngFormatter {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun isAccessorRelated(code: String): Boolean {
|
||||||
|
val t = code.trim()
|
||||||
|
if (t.isEmpty()) return false
|
||||||
|
if (isPropertyAccessor(t)) return true
|
||||||
|
|
||||||
|
// If it contains 'fun' or 'fn' as a word, it's probably a function declaration, not an accessor
|
||||||
|
if (Regex("\\b(fun|fn)\\b").containsMatchIn(t)) return false
|
||||||
|
|
||||||
|
val hasDecl = startsWithWord(t, "var") || startsWithWord(t, "val") ||
|
||||||
|
startsWithWord(t, "private") || startsWithWord(t, "protected") ||
|
||||||
|
startsWithWord(t, "override") || startsWithWord(t, "public")
|
||||||
|
|
||||||
|
if (hasDecl) {
|
||||||
|
val getSetMatch = Regex("\\b(get|set)\\b").find(t)
|
||||||
|
if (getSetMatch != null) {
|
||||||
|
// Check it's not part of an assignment to the property itself (e.g. val x = get())
|
||||||
|
val equalIndex = t.indexOf('=')
|
||||||
|
if (equalIndex == -1 || equalIndex > getSetMatch.range.first) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
/** Returns the input with indentation recomputed from scratch, line by line. */
|
/** Returns the input with indentation recomputed from scratch, line by line. */
|
||||||
fun reindent(text: String, config: LyngFormatConfig = LyngFormatConfig()): String {
|
fun reindent(text: String, config: LyngFormatConfig = LyngFormatConfig()): String {
|
||||||
// Normalize tabs to spaces globally before any transformation; results must contain no tabs
|
// Normalize tabs to spaces globally before any transformation; results must contain no tabs
|
||||||
@ -82,7 +107,7 @@ object LyngFormatter {
|
|||||||
if (isIf || isElseIf || isElse) return true
|
if (isIf || isElseIf || isElse) return true
|
||||||
|
|
||||||
// property accessors ending with ) or =
|
// property accessors ending with ) or =
|
||||||
if (isPropertyAccessor(t)) {
|
if (isAccessorRelated(t)) {
|
||||||
return if (t.contains('=')) t.endsWith('=') else t.endsWith(')')
|
return if (t.contains('=')) t.endsWith('=') else t.endsWith(')')
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
@ -168,7 +193,8 @@ object LyngFormatter {
|
|||||||
}
|
}
|
||||||
val newBlockLevel = blockLevel
|
val newBlockLevel = blockLevel
|
||||||
if (newBlockLevel > oldBlockLevel) {
|
if (newBlockLevel > oldBlockLevel) {
|
||||||
val addedThisLine = (if (applyAwaiting) awaitingExtraIndent else 0) + (if (isAccessor) 1 else 0)
|
val isAccessorRelatedLine = isAccessor || (!inBlockComment && isAccessorRelated(code))
|
||||||
|
val addedThisLine = (if (applyAwaiting) awaitingExtraIndent else 0) + (if (isAccessorRelatedLine) 1 else 0)
|
||||||
repeat(newBlockLevel - oldBlockLevel) {
|
repeat(newBlockLevel - oldBlockLevel) {
|
||||||
extraIndents.add(addedThisLine)
|
extraIndents.add(addedThisLine)
|
||||||
}
|
}
|
||||||
@ -186,7 +212,8 @@ object LyngFormatter {
|
|||||||
val endsWithBrace = code.trimEnd().endsWith("{")
|
val endsWithBrace = code.trimEnd().endsWith("{")
|
||||||
if (!endsWithBrace && isControlHeaderNoBrace(code)) {
|
if (!endsWithBrace && isControlHeaderNoBrace(code)) {
|
||||||
// It's another header, increment
|
// It's another header, increment
|
||||||
awaitingExtraIndent += if (isAccessor) 2 else 1
|
val isAccessorRelatedLine = isAccessor || (!inBlockComment && isAccessorRelated(code))
|
||||||
|
awaitingExtraIndent += if (isAccessorRelatedLine) 2 else 1
|
||||||
} else {
|
} else {
|
||||||
// It's the body, reset
|
// It's the body, reset
|
||||||
awaitingExtraIndent = 0
|
awaitingExtraIndent = 0
|
||||||
@ -195,7 +222,8 @@ object LyngFormatter {
|
|||||||
// start awaiting if current line is a control header without '{'
|
// start awaiting if current line is a control header without '{'
|
||||||
val endsWithBrace = code.trimEnd().endsWith("{")
|
val endsWithBrace = code.trimEnd().endsWith("{")
|
||||||
if (!endsWithBrace && isControlHeaderNoBrace(code)) {
|
if (!endsWithBrace && isControlHeaderNoBrace(code)) {
|
||||||
awaitingExtraIndent = if (isAccessor) 2 else 1
|
val isAccessorRelatedLine = isAccessor || (!inBlockComment && isAccessorRelated(code))
|
||||||
|
awaitingExtraIndent = if (isAccessorRelatedLine) 2 else 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -23,6 +23,7 @@ package net.sergeych.lyng.miniast
|
|||||||
import net.sergeych.lyng.Compiler
|
import net.sergeych.lyng.Compiler
|
||||||
import net.sergeych.lyng.Script
|
import net.sergeych.lyng.Script
|
||||||
import net.sergeych.lyng.Source
|
import net.sergeych.lyng.Source
|
||||||
|
import net.sergeych.lyng.highlight.offsetOf
|
||||||
import net.sergeych.lyng.pacman.ImportProvider
|
import net.sergeych.lyng.pacman.ImportProvider
|
||||||
|
|
||||||
/** Minimal completion item description (IDE-agnostic). */
|
/** Minimal completion item description (IDE-agnostic). */
|
||||||
@ -96,7 +97,11 @@ object CompletionEngineLight {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Global identifiers: params > local decls > imported > stdlib; Functions > Classes > Values; alphabetical
|
// Global identifiers: params > local decls > imported > stdlib; Functions > Classes > Values; alphabetical
|
||||||
val decls = mini.declarations
|
if (mini != null) {
|
||||||
|
offerParamsInScope(out, prefix, mini, text, caret)
|
||||||
|
}
|
||||||
|
|
||||||
|
val decls = mini?.declarations ?: emptyList()
|
||||||
val funs = decls.filterIsInstance<MiniFunDecl>().sortedBy { it.name.lowercase() }
|
val funs = decls.filterIsInstance<MiniFunDecl>().sortedBy { it.name.lowercase() }
|
||||||
val classes = decls.filterIsInstance<MiniClassDecl>().sortedBy { it.name.lowercase() }
|
val classes = decls.filterIsInstance<MiniClassDecl>().sortedBy { it.name.lowercase() }
|
||||||
val enums = decls.filterIsInstance<MiniEnumDecl>().sortedBy { it.name.lowercase() }
|
val enums = decls.filterIsInstance<MiniEnumDecl>().sortedBy { it.name.lowercase() }
|
||||||
@ -130,6 +135,74 @@ object CompletionEngineLight {
|
|||||||
|
|
||||||
// --- Emission helpers ---
|
// --- Emission helpers ---
|
||||||
|
|
||||||
|
private fun offerParamsInScope(out: MutableList<CompletionItem>, prefix: String, mini: MiniScript, text: String, caret: Int) {
|
||||||
|
val src = mini.range.start.source
|
||||||
|
val already = mutableSetOf<String>()
|
||||||
|
|
||||||
|
fun add(ci: CompletionItem) {
|
||||||
|
if (ci.name.startsWith(prefix, true) && already.add(ci.name)) {
|
||||||
|
out.add(ci)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun checkNode(node: Any) {
|
||||||
|
val range: MiniRange = when (node) {
|
||||||
|
is MiniDecl -> node.range
|
||||||
|
is MiniMemberDecl -> node.range
|
||||||
|
else -> return
|
||||||
|
}
|
||||||
|
val start = src.offsetOf(range.start)
|
||||||
|
val end = src.offsetOf(range.end).coerceAtMost(text.length)
|
||||||
|
|
||||||
|
if (caret in start..end) {
|
||||||
|
when (node) {
|
||||||
|
is MiniFunDecl -> {
|
||||||
|
for (p in node.params) {
|
||||||
|
add(CompletionItem(p.name, Kind.Value, typeText = typeOf(p.type)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is MiniClassDecl -> {
|
||||||
|
// Propose constructor parameters (ctorFields)
|
||||||
|
for (p in node.ctorFields) {
|
||||||
|
add(CompletionItem(p.name, if (p.mutable) Kind.Value else Kind.Field, typeText = typeOf(p.type)))
|
||||||
|
}
|
||||||
|
// Propose class-level fields
|
||||||
|
for (p in node.classFields) {
|
||||||
|
add(CompletionItem(p.name, if (p.mutable) Kind.Value else Kind.Field, typeText = typeOf(p.type)))
|
||||||
|
}
|
||||||
|
// Process members (methods/fields)
|
||||||
|
for (m in node.members) {
|
||||||
|
// If the member itself contains the caret (like a method), recurse
|
||||||
|
checkNode(m)
|
||||||
|
|
||||||
|
// Also offer the member itself for the class scope
|
||||||
|
when (m) {
|
||||||
|
is MiniMemberFunDecl -> {
|
||||||
|
val params = m.params.joinToString(", ") { it.name }
|
||||||
|
add(CompletionItem(m.name, Kind.Method, tailText = "(${params})", typeText = typeOf(m.returnType)))
|
||||||
|
}
|
||||||
|
is MiniMemberValDecl -> {
|
||||||
|
add(CompletionItem(m.name, if (m.mutable) Kind.Value else Kind.Field, typeText = typeOf(m.type)))
|
||||||
|
}
|
||||||
|
is MiniInitDecl -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is MiniMemberFunDecl -> {
|
||||||
|
for (p in node.params) {
|
||||||
|
add(CompletionItem(p.name, Kind.Value, typeText = typeOf(p.type)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (decl in mini.declarations) {
|
||||||
|
checkNode(decl)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun offerDeclAdd(out: MutableList<CompletionItem>, prefix: String, d: MiniDecl) {
|
private fun offerDeclAdd(out: MutableList<CompletionItem>, prefix: String, d: MiniDecl) {
|
||||||
fun add(ci: CompletionItem) { if (ci.name.startsWith(prefix, true)) out += ci }
|
fun add(ci: CompletionItem) { if (ci.name.startsWith(prefix, true)) out += ci }
|
||||||
when (d) {
|
when (d) {
|
||||||
|
|||||||
@ -859,4 +859,31 @@ class LyngFormatterTest {
|
|||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
assertEquals(expected2, LyngFormatter.reindent(src2, LyngFormatConfig(indentSize = 4)))
|
assertEquals(expected2, LyngFormatter.reindent(src2, LyngFormatConfig(indentSize = 4)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun mixedPropertyAccessors() {
|
||||||
|
val src = """
|
||||||
|
class X {
|
||||||
|
var x get() {
|
||||||
|
11
|
||||||
|
}
|
||||||
|
set(value) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""".trimIndent()
|
||||||
|
|
||||||
|
val expected = """
|
||||||
|
class X {
|
||||||
|
var x get() {
|
||||||
|
11
|
||||||
|
}
|
||||||
|
set(value) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""".trimIndent()
|
||||||
|
|
||||||
|
val cfg = LyngFormatConfig(indentSize = 4, continuationIndentSize = 4)
|
||||||
|
val out = LyngFormatter.reindent(src, cfg)
|
||||||
|
assertEquals(expected, out)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -145,4 +145,33 @@ class CompletionEngineLightTest {
|
|||||||
// Should contain some iterator members
|
// Should contain some iterator members
|
||||||
assertTrue(ns.isNotEmpty(), "Iterator members should be suggested after lines() with shebang present")
|
assertTrue(ns.isNotEmpty(), "Iterator members should be suggested after lines() with shebang present")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun constructorParametersInMethod() = runBlocking {
|
||||||
|
val code = """
|
||||||
|
class MyClass(myParam) {
|
||||||
|
fun myMethod() {
|
||||||
|
myp<caret>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""".trimIndent()
|
||||||
|
val items = CompletionEngineLight.completeAtMarkerSuspend(code)
|
||||||
|
val ns = names(items)
|
||||||
|
assertTrue(ns.contains("myParam"), "Constructor parameter 'myParam' should be proposed, but got: $ns")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun classFieldsInMethod() = runBlocking {
|
||||||
|
val code = """
|
||||||
|
class MyClass {
|
||||||
|
val myField = 1
|
||||||
|
fun myMethod() {
|
||||||
|
myf<caret>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""".trimIndent()
|
||||||
|
val items = CompletionEngineLight.completeAtMarkerSuspend(code)
|
||||||
|
val ns = names(items)
|
||||||
|
assertTrue(ns.contains("myField"), "Class field 'myField' should be proposed, but got: $ns")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user