better processing of lyng.d. files
This commit is contained in:
parent
9f10786a94
commit
8e10540257
@ -35,3 +35,27 @@ tasks.register<Exec>("generateDocs") {
|
|||||||
description = "Generates a single-file documentation HTML using bin/generate_docs.sh"
|
description = "Generates a single-file documentation HTML using bin/generate_docs.sh"
|
||||||
commandLine("./bin/generate_docs.sh")
|
commandLine("./bin/generate_docs.sh")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sample generator task for .lyng.d definition files (not wired into build).
|
||||||
|
// Usage: ./gradlew generateLyngDefsSample
|
||||||
|
tasks.register("generateLyngDefsSample") {
|
||||||
|
group = "lyng"
|
||||||
|
description = "Generate a sample .lyng.d file under build/generated/lyng/defs"
|
||||||
|
outputs.dir(layout.buildDirectory.dir("generated/lyng/defs"))
|
||||||
|
doLast {
|
||||||
|
val outDir = layout.buildDirectory.dir("generated/lyng/defs").get().asFile
|
||||||
|
outDir.mkdirs()
|
||||||
|
val outFile = outDir.resolve("sample.lyng.d")
|
||||||
|
outFile.writeText(
|
||||||
|
"""
|
||||||
|
/** Generated API */
|
||||||
|
extern fun ping(): Int
|
||||||
|
|
||||||
|
/** Generated class */
|
||||||
|
class Generated(val name: String) {
|
||||||
|
fun greet(): String = "hi " + name
|
||||||
|
}
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -9,9 +9,12 @@ should be compatible with other IDEA flavors, notably [OpenIDE](https://openide.
|
|||||||
- reformat code (indents, spaces)
|
- reformat code (indents, spaces)
|
||||||
- reformat on paste
|
- reformat on paste
|
||||||
- smart enter key
|
- smart enter key
|
||||||
|
- `.lyng.d` definition files (merged into analysis for completion, navigation, Quick Docs, and error checking)
|
||||||
|
|
||||||
Features are configurable via the plugin settings page, in system settings.
|
Features are configurable via the plugin settings page, in system settings.
|
||||||
|
|
||||||
|
See `docs/lyng_d_files.md` for `.lyng.d` syntax and examples.
|
||||||
|
|
||||||
> Recommended for IntelliJ-based IDEs: While IntelliJ can import TextMate bundles
|
> Recommended for IntelliJ-based IDEs: While IntelliJ can import TextMate bundles
|
||||||
> (Settings/Preferences → Editor → TextMate Bundles), the native Lyng plugin provides
|
> (Settings/Preferences → Editor → TextMate Bundles), the native Lyng plugin provides
|
||||||
> better support (formatting, smart enter, background analysis, etc.). Prefer installing
|
> better support (formatting, smart enter, background analysis, etc.). Prefer installing
|
||||||
@ -26,4 +29,4 @@ Features are configurable via the plugin settings page, in system settings.
|
|||||||
|
|
||||||
### [Download plugin v0.0.2-SNAPSHOT](https://lynglang.com/distributables/lyng-idea-0.0.2-SNAPSHOT.zip)
|
### [Download plugin v0.0.2-SNAPSHOT](https://lynglang.com/distributables/lyng-idea-0.0.2-SNAPSHOT.zip)
|
||||||
|
|
||||||
Your ideas and bugreports are welcome on the [project gitea page](https://gitea.sergeych.net/SergeychWorks/lyng/issues)
|
Your ideas and bugreports are welcome on the [project gitea page](https://gitea.sergeych.net/SergeychWorks/lyng/issues)
|
||||||
|
|||||||
116
docs/lyng_d_files.md
Normal file
116
docs/lyng_d_files.md
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
# `.lyng.d` Definition Files
|
||||||
|
|
||||||
|
`.lyng.d` files declare Lyng symbols for tooling without shipping runtime implementations. The IntelliJ IDEA plugin merges
|
||||||
|
all `*.lyng.d` files from the current directory and its parent directories into the active file’s analysis, enabling:
|
||||||
|
|
||||||
|
- completion
|
||||||
|
- navigation
|
||||||
|
- error checking for declared symbols
|
||||||
|
- Quick Docs for declarations defined in `.lyng.d`
|
||||||
|
|
||||||
|
Place `*.lyng.d` files next to the code they describe (or in a parent folder). The plugin will pick them up automatically.
|
||||||
|
|
||||||
|
## Writing `.lyng.d` Files
|
||||||
|
|
||||||
|
You can declare any language-level symbol in a `.lyng.d` file. Use doc comments before declarations to make Quick Docs
|
||||||
|
work in the IDE. The doc parser accepts standard comments (`/** ... */` or `// ...`) and supports tags like `@param`.
|
||||||
|
|
||||||
|
### Full Example
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
/** Library entry point */
|
||||||
|
extern fun connect(url: String, timeoutMs: Int = 5000): Client
|
||||||
|
|
||||||
|
/** Type alias with generics */
|
||||||
|
type NameMap = Map<String, String>
|
||||||
|
|
||||||
|
/** Multiple inheritance via interfaces */
|
||||||
|
interface A { abstract fun a(): Int }
|
||||||
|
interface B { abstract fun b(): Int }
|
||||||
|
|
||||||
|
/** A concrete class implementing both */
|
||||||
|
class Multi(name: String) : A, B {
|
||||||
|
/** Public field */
|
||||||
|
val id: Int = 0
|
||||||
|
|
||||||
|
/** Mutable property with accessors */
|
||||||
|
var size: Int
|
||||||
|
get() = 0
|
||||||
|
set(v) { }
|
||||||
|
|
||||||
|
/** Instance method */
|
||||||
|
fun a(): Int = 1
|
||||||
|
fun b(): Int = 2
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Nullable and dynamic types */
|
||||||
|
extern val dynValue: dynamic
|
||||||
|
extern var dynVar: dynamic?
|
||||||
|
|
||||||
|
/** Delegated property */
|
||||||
|
class LazyBox(val create) {
|
||||||
|
fun getValue(thisRef, name) = create()
|
||||||
|
}
|
||||||
|
|
||||||
|
val cached by LazyBox { 42 }
|
||||||
|
|
||||||
|
/** Delegated function */
|
||||||
|
object RpcDelegate {
|
||||||
|
fun invoke(thisRef, name, args...) = Unset
|
||||||
|
}
|
||||||
|
|
||||||
|
fun remoteCall by RpcDelegate
|
||||||
|
|
||||||
|
/** Singleton object */
|
||||||
|
object Settings {
|
||||||
|
val version: String = "1.0"
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Class with documented members */
|
||||||
|
class Client {
|
||||||
|
/** Returns a greeting. */
|
||||||
|
fun greet(name: String): String = "hi " + name
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
See a runnable sample file in `docs/samples/definitions.lyng.d`.
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
- Use real bodies if the declaration is not `extern` or `abstract`.
|
||||||
|
- If you need purely declarative stubs, prefer `extern` members (see `embedding.md`).
|
||||||
|
|
||||||
|
## Doc Comment Format
|
||||||
|
|
||||||
|
Doc comments are picked up when they immediately precede a declaration.
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
/**
|
||||||
|
* A sample function.
|
||||||
|
* @param name user name
|
||||||
|
* @return greeting string
|
||||||
|
*/
|
||||||
|
fun greet(name: String): String = "hi " + name
|
||||||
|
```
|
||||||
|
|
||||||
|
## Generating `.lyng.d` Files
|
||||||
|
|
||||||
|
You can generate `.lyng.d` as part of your build. A common approach is to write a Gradle task that emits a file from a
|
||||||
|
template or a Kotlin data model.
|
||||||
|
|
||||||
|
Example (pseudo-code):
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
tasks.register("generateLyngDefs") {
|
||||||
|
doLast {
|
||||||
|
val out = file("src/main/lyng/api.lyng.d")
|
||||||
|
out.writeText(
|
||||||
|
"""
|
||||||
|
/** Generated API */
|
||||||
|
fun ping(): Int
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Place the generated file in your source tree, and the IDE will load it automatically.
|
||||||
65
docs/samples/definitions.lyng.d
Normal file
65
docs/samples/definitions.lyng.d
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
/**
|
||||||
|
* Sample .lyng.d file for IDE support.
|
||||||
|
* Demonstrates declarations and doc comments.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** Simple function with default and named parameters. */
|
||||||
|
extern fun connect(url: String, timeoutMs: Int = 5000): Client
|
||||||
|
|
||||||
|
/** Type alias with generics. */
|
||||||
|
type NameMap = Map<String, String>
|
||||||
|
|
||||||
|
/** Multiple inheritance via interfaces. */
|
||||||
|
interface A { abstract fun a(): Int }
|
||||||
|
interface B { abstract fun b(): Int }
|
||||||
|
|
||||||
|
/** A concrete class implementing both. */
|
||||||
|
class Multi(name: String) : A, B {
|
||||||
|
/** Public field. */
|
||||||
|
val id: Int = 0
|
||||||
|
|
||||||
|
/** Mutable property with accessors. */
|
||||||
|
var size: Int
|
||||||
|
get() = 0
|
||||||
|
set(v) { }
|
||||||
|
|
||||||
|
/** Instance method. */
|
||||||
|
fun a(): Int = 1
|
||||||
|
fun b(): Int = 2
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Nullable and dynamic types. */
|
||||||
|
extern val dynValue: dynamic
|
||||||
|
extern var dynVar: dynamic?
|
||||||
|
|
||||||
|
/** Delegated property provider. */
|
||||||
|
class LazyBox(val create) {
|
||||||
|
fun getValue(thisRef, name) = create()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Delegated property using provider. */
|
||||||
|
val cached by LazyBox { 42 }
|
||||||
|
|
||||||
|
/** Delegated function. */
|
||||||
|
object RpcDelegate {
|
||||||
|
fun invoke(thisRef, name, args...) = Unset
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Remote function proxy. */
|
||||||
|
fun remoteCall by RpcDelegate
|
||||||
|
|
||||||
|
/** Singleton object. */
|
||||||
|
object Settings {
|
||||||
|
/** Version string. */
|
||||||
|
val version: String = "1.0"
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Client API entry.
|
||||||
|
* @param name user name
|
||||||
|
* @return greeting string
|
||||||
|
*/
|
||||||
|
class Client {
|
||||||
|
/** Returns a greeting. */
|
||||||
|
fun greet(name: String): String = "hi " + name
|
||||||
|
}
|
||||||
@ -35,13 +35,7 @@ import kotlinx.coroutines.CoroutineScope
|
|||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.SupervisorJob
|
import kotlinx.coroutines.SupervisorJob
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import net.sergeych.lyng.ExecutionError
|
|
||||||
import net.sergeych.lyng.Script
|
|
||||||
import net.sergeych.lyng.Source
|
|
||||||
import net.sergeych.lyng.requireScope
|
|
||||||
import net.sergeych.lyng.idea.LyngIcons
|
import net.sergeych.lyng.idea.LyngIcons
|
||||||
import net.sergeych.lyng.obj.ObjVoid
|
|
||||||
import net.sergeych.lyng.obj.getLyngExceptionMessageWithStackTrace
|
|
||||||
|
|
||||||
class RunLyngScriptAction : AnAction(LyngIcons.FILE) {
|
class RunLyngScriptAction : AnAction(LyngIcons.FILE) {
|
||||||
private val scope = CoroutineScope(Dispatchers.Default + SupervisorJob())
|
private val scope = CoroutineScope(Dispatchers.Default + SupervisorJob())
|
||||||
@ -59,7 +53,9 @@ class RunLyngScriptAction : AnAction(LyngIcons.FILE) {
|
|||||||
val isLyng = psiFile?.name?.endsWith(".lyng") == true
|
val isLyng = psiFile?.name?.endsWith(".lyng") == true
|
||||||
e.presentation.isEnabledAndVisible = isLyng
|
e.presentation.isEnabledAndVisible = isLyng
|
||||||
if (isLyng) {
|
if (isLyng) {
|
||||||
e.presentation.text = "Run '${psiFile.name}'"
|
e.presentation.isEnabled = false
|
||||||
|
e.presentation.text = "Run '${psiFile.name}' (disabled)"
|
||||||
|
e.presentation.description = "Running scripts from the IDE is disabled; use the CLI."
|
||||||
} else {
|
} else {
|
||||||
e.presentation.text = "Run Lyng Script"
|
e.presentation.text = "Run Lyng Script"
|
||||||
}
|
}
|
||||||
@ -68,7 +64,6 @@ class RunLyngScriptAction : AnAction(LyngIcons.FILE) {
|
|||||||
override fun actionPerformed(e: AnActionEvent) {
|
override fun actionPerformed(e: AnActionEvent) {
|
||||||
val project = e.project ?: return
|
val project = e.project ?: return
|
||||||
val psiFile = getPsiFile(e) ?: return
|
val psiFile = getPsiFile(e) ?: return
|
||||||
val text = psiFile.text
|
|
||||||
val fileName = psiFile.name
|
val fileName = psiFile.name
|
||||||
|
|
||||||
val (console, toolWindow) = getConsoleAndToolWindow(project)
|
val (console, toolWindow) = getConsoleAndToolWindow(project)
|
||||||
@ -76,40 +71,9 @@ class RunLyngScriptAction : AnAction(LyngIcons.FILE) {
|
|||||||
|
|
||||||
toolWindow.show {
|
toolWindow.show {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
try {
|
console.print("--- Run is disabled ---\n", ConsoleViewContentType.SYSTEM_OUTPUT)
|
||||||
val lyngScope = Script.newScope()
|
console.print("Lyng now runs in bytecode-only mode; the IDE no longer evaluates scripts.\n", ConsoleViewContentType.NORMAL_OUTPUT)
|
||||||
lyngScope.addFn("print") {
|
console.print("Use the CLI to run scripts, e.g. `lyng run $fileName`.\n", ConsoleViewContentType.NORMAL_OUTPUT)
|
||||||
val sb = StringBuilder()
|
|
||||||
for ((i, arg) in args.list.withIndex()) {
|
|
||||||
if (i > 0) sb.append(" ")
|
|
||||||
sb.append(arg.toString(requireScope()).value)
|
|
||||||
}
|
|
||||||
console.print(sb.toString(), ConsoleViewContentType.NORMAL_OUTPUT)
|
|
||||||
ObjVoid
|
|
||||||
}
|
|
||||||
lyngScope.addFn("println") {
|
|
||||||
val sb = StringBuilder()
|
|
||||||
for ((i, arg) in args.list.withIndex()) {
|
|
||||||
if (i > 0) sb.append(" ")
|
|
||||||
sb.append(arg.toString(requireScope()).value)
|
|
||||||
}
|
|
||||||
console.print(sb.toString() + "\n", ConsoleViewContentType.NORMAL_OUTPUT)
|
|
||||||
ObjVoid
|
|
||||||
}
|
|
||||||
|
|
||||||
console.print("--- Running $fileName ---\n", ConsoleViewContentType.SYSTEM_OUTPUT)
|
|
||||||
val result = lyngScope.eval(Source(fileName, text))
|
|
||||||
console.print("\n--- Finished with result: ${result.inspect(lyngScope)} ---\n", ConsoleViewContentType.SYSTEM_OUTPUT)
|
|
||||||
} catch (t: Throwable) {
|
|
||||||
console.print("\n--- Error ---\n", ConsoleViewContentType.ERROR_OUTPUT)
|
|
||||||
if( t is ExecutionError ) {
|
|
||||||
val m = t.errorObject.getLyngExceptionMessageWithStackTrace()
|
|
||||||
console.print(m, ConsoleViewContentType.ERROR_OUTPUT)
|
|
||||||
}
|
|
||||||
else
|
|
||||||
console.print(t.message ?: t.toString(), ConsoleViewContentType.ERROR_OUTPUT)
|
|
||||||
console.print("\n", ConsoleViewContentType.ERROR_OUTPUT)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -24,6 +24,7 @@ import com.intellij.openapi.editor.Editor
|
|||||||
import com.intellij.openapi.util.TextRange
|
import com.intellij.openapi.util.TextRange
|
||||||
import com.intellij.psi.PsiElement
|
import com.intellij.psi.PsiElement
|
||||||
import com.intellij.psi.PsiFile
|
import com.intellij.psi.PsiFile
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
import net.sergeych.lyng.highlight.offsetOf
|
import net.sergeych.lyng.highlight.offsetOf
|
||||||
import net.sergeych.lyng.idea.LyngLanguage
|
import net.sergeych.lyng.idea.LyngLanguage
|
||||||
import net.sergeych.lyng.idea.util.LyngAstManager
|
import net.sergeych.lyng.idea.util.LyngAstManager
|
||||||
@ -75,7 +76,51 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
|
|||||||
|
|
||||||
// Single-source quick doc lookup
|
// Single-source quick doc lookup
|
||||||
LyngLanguageTools.docAt(analysis, offset)?.let { info ->
|
LyngLanguageTools.docAt(analysis, offset)?.let { info ->
|
||||||
renderDocFromInfo(info)?.let { return it }
|
val enriched = if (info.doc == null) {
|
||||||
|
findDocInDeclarationFiles(file, info.target.containerName, info.target.name)
|
||||||
|
?.let { info.copy(doc = it) } ?: info
|
||||||
|
} else {
|
||||||
|
info
|
||||||
|
}
|
||||||
|
renderDocFromInfo(enriched)?.let { return it }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: resolve references against merged MiniAst (including .lyng.d) when binder cannot
|
||||||
|
run {
|
||||||
|
val dotPos = DocLookupUtils.findDotLeft(text, idRange.startOffset)
|
||||||
|
if (dotPos != null) {
|
||||||
|
val receiverClass = DocLookupUtils.guessReceiverClassViaMini(mini, text, dotPos, imported, analysis.binding)
|
||||||
|
?: DocLookupUtils.guessReceiverClass(text, dotPos, imported, mini)
|
||||||
|
if (receiverClass != null) {
|
||||||
|
val resolved = DocLookupUtils.resolveMemberWithInheritance(imported, receiverClass, ident, mini)
|
||||||
|
if (resolved != null) {
|
||||||
|
val owner = resolved.first
|
||||||
|
val member = resolved.second
|
||||||
|
val withDoc = if (member.doc == null) {
|
||||||
|
findDocInDeclarationFiles(file, owner, member.name)?.let { doc ->
|
||||||
|
when (member) {
|
||||||
|
is MiniMemberFunDecl -> member.copy(doc = doc)
|
||||||
|
is MiniMemberValDecl -> member.copy(doc = doc)
|
||||||
|
is MiniMemberTypeAliasDecl -> member.copy(doc = doc)
|
||||||
|
else -> member
|
||||||
|
}
|
||||||
|
} ?: member
|
||||||
|
} else {
|
||||||
|
member
|
||||||
|
}
|
||||||
|
return when (withDoc) {
|
||||||
|
is MiniMemberFunDecl -> renderMemberFunDoc(owner, withDoc)
|
||||||
|
is MiniMemberValDecl -> renderMemberValDoc(owner, withDoc)
|
||||||
|
is MiniMemberTypeAliasDecl -> renderMemberTypeAliasDoc(owner, withDoc)
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
mini.declarations.firstOrNull { it.name == ident }?.let { decl ->
|
||||||
|
return renderDeclDoc(decl, text, mini, imported)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try resolve to: function param at position, function/class/val declaration at position
|
// Try resolve to: function param at position, function/class/val declaration at position
|
||||||
@ -570,6 +615,55 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
|
|||||||
return sb.toString()
|
return sb.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun findDocInDeclarationFiles(file: PsiFile, container: String?, name: String): MiniDoc? {
|
||||||
|
val declFiles = LyngAstManager.getDeclarationFiles(file)
|
||||||
|
if (declFiles.isEmpty()) return null
|
||||||
|
|
||||||
|
fun findInMini(mini: MiniScript): MiniDoc? {
|
||||||
|
if (container == null) {
|
||||||
|
mini.declarations.firstOrNull { it.name == name }?.let { return it.doc }
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
val cls = mini.declarations.filterIsInstance<MiniClassDecl>().firstOrNull { it.name == container } ?: return null
|
||||||
|
cls.members.firstOrNull { it.name == name }?.let { return it.doc }
|
||||||
|
cls.ctorFields.firstOrNull { it.name == name }?.let { return null }
|
||||||
|
cls.classFields.firstOrNull { it.name == name }?.let { return null }
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
for (df in declFiles) {
|
||||||
|
val mini = LyngAstManager.getMiniAst(df)
|
||||||
|
?: run {
|
||||||
|
try {
|
||||||
|
val res = runBlocking {
|
||||||
|
LyngLanguageTools.analyze(df.text, df.name)
|
||||||
|
}
|
||||||
|
res.mini
|
||||||
|
} catch (_: Throwable) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (mini != null) {
|
||||||
|
val doc = findInMini(mini)
|
||||||
|
if (doc != null) return doc
|
||||||
|
}
|
||||||
|
// Text fallback: parse preceding doc comment for the symbol
|
||||||
|
val parsed = parseDocFromText(df.text, name)
|
||||||
|
if (parsed != null) return parsed
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseDocFromText(text: String, name: String): MiniDoc? {
|
||||||
|
if (text.isBlank()) return null
|
||||||
|
val pattern = Regex("/\\*\\*([\\s\\S]*?)\\*/\\s*(?:public|private|protected|static|abstract|extern|open|closed|override\\s+)*\\s*(?:fun|val|var|class|interface|enum|type)\\s+$name\\b")
|
||||||
|
val m = pattern.find(text) ?: return null
|
||||||
|
val raw = m.groupValues.getOrNull(1)?.trim() ?: return null
|
||||||
|
if (raw.isBlank()) return null
|
||||||
|
val src = net.sergeych.lyng.Source("<doc>", raw)
|
||||||
|
return MiniDoc.parse(MiniRange(src.startPos, src.startPos), raw.lines())
|
||||||
|
}
|
||||||
|
|
||||||
private fun renderParamDoc(fn: MiniFunDecl, p: MiniParam): String {
|
private fun renderParamDoc(fn: MiniFunDecl, p: MiniParam): String {
|
||||||
val title = "parameter ${p.name}${typeOf(p.type)} in ${fn.name}${signatureOf(fn)}"
|
val title = "parameter ${p.name}${typeOf(p.type)} in ${fn.name}${signatureOf(fn)}"
|
||||||
val sb = StringBuilder()
|
val sb = StringBuilder()
|
||||||
|
|||||||
@ -20,12 +20,18 @@ package net.sergeych.lyng.idea.navigation
|
|||||||
import com.intellij.openapi.project.Project
|
import com.intellij.openapi.project.Project
|
||||||
import com.intellij.openapi.util.TextRange
|
import com.intellij.openapi.util.TextRange
|
||||||
import com.intellij.psi.*
|
import com.intellij.psi.*
|
||||||
|
import com.intellij.psi.search.FileTypeIndex
|
||||||
import com.intellij.psi.search.FilenameIndex
|
import com.intellij.psi.search.FilenameIndex
|
||||||
import com.intellij.psi.search.GlobalSearchScope
|
import com.intellij.psi.search.GlobalSearchScope
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
import net.sergeych.lyng.highlight.offsetOf
|
import net.sergeych.lyng.highlight.offsetOf
|
||||||
|
import net.sergeych.lyng.idea.LyngFileType
|
||||||
import net.sergeych.lyng.idea.util.LyngAstManager
|
import net.sergeych.lyng.idea.util.LyngAstManager
|
||||||
import net.sergeych.lyng.idea.util.TextCtx
|
import net.sergeych.lyng.idea.util.TextCtx
|
||||||
import net.sergeych.lyng.miniast.*
|
import net.sergeych.lyng.miniast.*
|
||||||
|
import net.sergeych.lyng.tools.IdeLenientImportProvider
|
||||||
|
import net.sergeych.lyng.tools.LyngAnalysisRequest
|
||||||
|
import net.sergeych.lyng.tools.LyngLanguageTools
|
||||||
|
|
||||||
class LyngPsiReference(element: PsiElement) : PsiPolyVariantReferenceBase<PsiElement>(element, TextRange(0, element.textLength)) {
|
class LyngPsiReference(element: PsiElement) : PsiPolyVariantReferenceBase<PsiElement>(element, TextRange(0, element.textLength)) {
|
||||||
|
|
||||||
@ -58,7 +64,7 @@ class LyngPsiReference(element: PsiElement) : PsiPolyVariantReferenceBase<PsiEle
|
|||||||
|
|
||||||
// We need to find the actual PSI element for this member
|
// We need to find the actual PSI element for this member
|
||||||
val targetFile = findFileForClass(file.project, owner) ?: file
|
val targetFile = findFileForClass(file.project, owner) ?: file
|
||||||
val targetMini = LyngAstManager.getMiniAst(targetFile)
|
val targetMini = loadMini(targetFile)
|
||||||
if (targetMini != null) {
|
if (targetMini != null) {
|
||||||
val targetSrc = targetMini.range.start.source
|
val targetSrc = targetMini.range.start.source
|
||||||
val off = targetSrc.offsetOf(member.nameStart)
|
val off = targetSrc.offsetOf(member.nameStart)
|
||||||
@ -123,24 +129,37 @@ class LyngPsiReference(element: PsiElement) : PsiPolyVariantReferenceBase<PsiEle
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun findFileForClass(project: Project, className: String): PsiFile? {
|
private fun findFileForClass(project: Project, className: String): PsiFile? {
|
||||||
val psiManager = PsiManager.getInstance(project)
|
|
||||||
|
|
||||||
// 1. Try file with matching name first (optimization)
|
// 1. Try file with matching name first (optimization)
|
||||||
val matchingFiles = FilenameIndex.getFilesByName(project, "$className.lyng", GlobalSearchScope.projectScope(project))
|
val scope = GlobalSearchScope.projectScope(project)
|
||||||
|
val psiManager = PsiManager.getInstance(project)
|
||||||
|
val matchingFiles = FileTypeIndex.getFiles(LyngFileType, scope)
|
||||||
|
.asSequence()
|
||||||
|
.filter { it.name == "$className.lyng" }
|
||||||
|
.mapNotNull { psiManager.findFile(it) }
|
||||||
|
.toList()
|
||||||
|
val matchingDeclFiles = FileTypeIndex.getFiles(LyngFileType, scope)
|
||||||
|
.asSequence()
|
||||||
|
.filter { it.name == "$className.lyng.d" }
|
||||||
|
.mapNotNull { psiManager.findFile(it) }
|
||||||
|
.toList()
|
||||||
for (file in matchingFiles) {
|
for (file in matchingFiles) {
|
||||||
val mini = LyngAstManager.getMiniAst(file) ?: continue
|
val mini = loadMini(file) ?: continue
|
||||||
if (mini.declarations.any { (it is MiniClassDecl && it.name == className) || (it is MiniEnumDecl && it.name == className) }) {
|
if (mini.declarations.any { isLocalDecl(mini, it) && ((it is MiniClassDecl && it.name == className) || (it is MiniEnumDecl && it.name == className)) }) {
|
||||||
|
return file
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (file in matchingDeclFiles) {
|
||||||
|
val mini = loadMini(file) ?: continue
|
||||||
|
if (mini.declarations.any { isLocalDecl(mini, it) && ((it is MiniClassDecl && it.name == className) || (it is MiniEnumDecl && it.name == className)) }) {
|
||||||
return file
|
return file
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Fallback to full project scan
|
// 2. Fallback to full project scan
|
||||||
val allFiles = FilenameIndex.getAllFilesByExt(project, "lyng", GlobalSearchScope.projectScope(project))
|
for (file in collectLyngFiles(project)) {
|
||||||
for (vFile in allFiles) {
|
if (matchingFiles.contains(file) || matchingDeclFiles.contains(file)) continue // already checked
|
||||||
val file = psiManager.findFile(vFile) ?: continue
|
val mini = loadMini(file) ?: continue
|
||||||
if (matchingFiles.contains(file)) continue // already checked
|
if (mini.declarations.any { isLocalDecl(mini, it) && ((it is MiniClassDecl && it.name == className) || (it is MiniEnumDecl && it.name == className)) }) {
|
||||||
val mini = LyngAstManager.getMiniAst(file) ?: continue
|
|
||||||
if (mini.declarations.any { (it is MiniClassDecl && it.name == className) || (it is MiniEnumDecl && it.name == className) }) {
|
|
||||||
return file
|
return file
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -148,7 +167,7 @@ class LyngPsiReference(element: PsiElement) : PsiPolyVariantReferenceBase<PsiEle
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun getPackageName(file: PsiFile): String? {
|
private fun getPackageName(file: PsiFile): String? {
|
||||||
val mini = LyngAstManager.getMiniAst(file) ?: return null
|
val mini = loadMini(file) ?: return null
|
||||||
return try {
|
return try {
|
||||||
val pkg = mini.range.start.source.extractPackageName()
|
val pkg = mini.range.start.source.extractPackageName()
|
||||||
if (pkg.startsWith("lyng.")) pkg else "lyng.$pkg"
|
if (pkg.startsWith("lyng.")) pkg else "lyng.$pkg"
|
||||||
@ -172,19 +191,19 @@ class LyngPsiReference(element: PsiElement) : PsiPolyVariantReferenceBase<PsiEle
|
|||||||
|
|
||||||
private fun resolveGlobally(project: Project, name: String, membersOnly: Boolean = false, allowedPackages: Set<String>? = null): List<ResolveResult> {
|
private fun resolveGlobally(project: Project, name: String, membersOnly: Boolean = false, allowedPackages: Set<String>? = null): List<ResolveResult> {
|
||||||
val results = mutableListOf<ResolveResult>()
|
val results = mutableListOf<ResolveResult>()
|
||||||
val files = FilenameIndex.getAllFilesByExt(project, "lyng", GlobalSearchScope.projectScope(project))
|
|
||||||
val psiManager = PsiManager.getInstance(project)
|
val psiManager = PsiManager.getInstance(project)
|
||||||
|
|
||||||
for (vFile in files) {
|
for (file in collectLyngFiles(project)) {
|
||||||
val file = psiManager.findFile(vFile) ?: continue
|
|
||||||
|
|
||||||
// Filter by package if requested
|
// Filter by package if requested
|
||||||
if (allowedPackages != null) {
|
if (allowedPackages != null) {
|
||||||
val pkg = getPackageName(file)
|
val pkg = getPackageName(file)
|
||||||
if (pkg == null || pkg !in allowedPackages) continue
|
if (pkg == null) {
|
||||||
|
if (!file.name.endsWith(".lyng.d")) continue
|
||||||
|
} else if (pkg !in allowedPackages) continue
|
||||||
}
|
}
|
||||||
|
|
||||||
val mini = LyngAstManager.getMiniAst(file) ?: continue
|
val mini = loadMini(file) ?: continue
|
||||||
val src = mini.range.start.source
|
val src = mini.range.start.source
|
||||||
|
|
||||||
fun addIfMatch(dName: String, nameStart: net.sergeych.lyng.Pos, dKind: String) {
|
fun addIfMatch(dName: String, nameStart: net.sergeych.lyng.Pos, dKind: String) {
|
||||||
@ -197,6 +216,7 @@ class LyngPsiReference(element: PsiElement) : PsiPolyVariantReferenceBase<PsiEle
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (d in mini.declarations) {
|
for (d in mini.declarations) {
|
||||||
|
if (!isLocalDecl(mini, d)) continue
|
||||||
if (!membersOnly) {
|
if (!membersOnly) {
|
||||||
val dKind = when(d) {
|
val dKind = when(d) {
|
||||||
is net.sergeych.lyng.miniast.MiniFunDecl -> "Function"
|
is net.sergeych.lyng.miniast.MiniFunDecl -> "Function"
|
||||||
@ -216,6 +236,7 @@ class LyngPsiReference(element: PsiElement) : PsiPolyVariantReferenceBase<PsiEle
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (m in members) {
|
for (m in members) {
|
||||||
|
if (m.range.start.source != src) continue
|
||||||
val mKind = when(m) {
|
val mKind = when(m) {
|
||||||
is net.sergeych.lyng.miniast.MiniMemberFunDecl -> "Function"
|
is net.sergeych.lyng.miniast.MiniMemberFunDecl -> "Function"
|
||||||
is net.sergeych.lyng.miniast.MiniMemberValDecl -> if (m.mutable) "Variable" else "Value"
|
is net.sergeych.lyng.miniast.MiniMemberValDecl -> if (m.mutable) "Variable" else "Value"
|
||||||
@ -229,5 +250,42 @@ class LyngPsiReference(element: PsiElement) : PsiPolyVariantReferenceBase<PsiEle
|
|||||||
return results
|
return results
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun collectLyngFiles(project: Project): List<PsiFile> {
|
||||||
|
val scope = GlobalSearchScope.projectScope(project)
|
||||||
|
val psiManager = PsiManager.getInstance(project)
|
||||||
|
val out = LinkedHashSet<PsiFile>()
|
||||||
|
|
||||||
|
val lyngFiles = FilenameIndex.getAllFilesByExt(project, "lyng", scope)
|
||||||
|
for (vFile in lyngFiles) {
|
||||||
|
psiManager.findFile(vFile)?.let { out.add(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Include declaration files (*.lyng.d) which are indexed as extension "d".
|
||||||
|
val dFiles = FilenameIndex.getAllFilesByExt(project, "d", scope)
|
||||||
|
for (vFile in dFiles) {
|
||||||
|
if (!vFile.name.endsWith(".lyng.d")) continue
|
||||||
|
psiManager.findFile(vFile)?.let { out.add(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
return out.toList()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadMini(file: PsiFile): MiniScript? {
|
||||||
|
LyngAstManager.getMiniAst(file)?.let { return it }
|
||||||
|
return try {
|
||||||
|
val provider = IdeLenientImportProvider.create()
|
||||||
|
runBlocking {
|
||||||
|
LyngLanguageTools.analyze(
|
||||||
|
LyngAnalysisRequest(text = file.text, fileName = file.name, importProvider = provider)
|
||||||
|
)
|
||||||
|
}.mini
|
||||||
|
} catch (_: Throwable) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isLocalDecl(mini: MiniScript, decl: MiniDecl): Boolean =
|
||||||
|
decl.range.start.source == mini.range.start.source
|
||||||
|
|
||||||
override fun getVariants(): Array<Any> = emptyArray()
|
override fun getVariants(): Array<Any> = emptyArray()
|
||||||
}
|
}
|
||||||
|
|||||||
@ -21,14 +21,22 @@ import com.intellij.openapi.application.runReadAction
|
|||||||
import com.intellij.openapi.util.Key
|
import com.intellij.openapi.util.Key
|
||||||
import com.intellij.psi.PsiFile
|
import com.intellij.psi.PsiFile
|
||||||
import com.intellij.psi.PsiManager
|
import com.intellij.psi.PsiManager
|
||||||
|
import com.intellij.psi.search.FileTypeIndex
|
||||||
|
import com.intellij.psi.search.FilenameIndex
|
||||||
|
import com.intellij.psi.search.GlobalSearchScope
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import net.sergeych.lyng.binding.BindingSnapshot
|
import net.sergeych.lyng.binding.BindingSnapshot
|
||||||
|
import net.sergeych.lyng.miniast.BuiltinDocRegistry
|
||||||
import net.sergeych.lyng.miniast.DocLookupUtils
|
import net.sergeych.lyng.miniast.DocLookupUtils
|
||||||
|
import net.sergeych.lyng.miniast.MiniEnumDecl
|
||||||
|
import net.sergeych.lyng.miniast.MiniRange
|
||||||
import net.sergeych.lyng.miniast.MiniScript
|
import net.sergeych.lyng.miniast.MiniScript
|
||||||
import net.sergeych.lyng.tools.IdeLenientImportProvider
|
import net.sergeych.lyng.tools.IdeLenientImportProvider
|
||||||
import net.sergeych.lyng.tools.LyngAnalysisRequest
|
import net.sergeych.lyng.tools.LyngAnalysisRequest
|
||||||
import net.sergeych.lyng.tools.LyngAnalysisResult
|
import net.sergeych.lyng.tools.LyngAnalysisResult
|
||||||
|
import net.sergeych.lyng.tools.LyngDiagnostic
|
||||||
import net.sergeych.lyng.tools.LyngLanguageTools
|
import net.sergeych.lyng.tools.LyngLanguageTools
|
||||||
|
import net.sergeych.lyng.idea.LyngFileType
|
||||||
|
|
||||||
object LyngAstManager {
|
object LyngAstManager {
|
||||||
private val MINI_KEY = Key.create<MiniScript>("lyng.mini.cache")
|
private val MINI_KEY = Key.create<MiniScript>("lyng.mini.cache")
|
||||||
@ -52,22 +60,65 @@ object LyngAstManager {
|
|||||||
|
|
||||||
private fun collectDeclarationFiles(file: PsiFile): List<PsiFile> = runReadAction {
|
private fun collectDeclarationFiles(file: PsiFile): List<PsiFile> = runReadAction {
|
||||||
val psiManager = PsiManager.getInstance(file.project)
|
val psiManager = PsiManager.getInstance(file.project)
|
||||||
var current = file.virtualFile?.parent
|
|
||||||
val seen = mutableSetOf<String>()
|
val seen = mutableSetOf<String>()
|
||||||
val result = mutableListOf<PsiFile>()
|
val result = mutableListOf<PsiFile>()
|
||||||
|
|
||||||
while (current != null) {
|
var currentDir = file.containingDirectory
|
||||||
for (child in current.children) {
|
while (currentDir != null) {
|
||||||
if (child.name.endsWith(".lyng.d") && child != file.virtualFile && seen.add(child.path)) {
|
for (child in currentDir.files) {
|
||||||
val psiD = psiManager.findFile(child) ?: continue
|
if (child.name.endsWith(".lyng.d") && child != file && seen.add(child.virtualFile.path)) {
|
||||||
result.add(psiD)
|
result.add(child)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
current = current.parent
|
currentDir = currentDir.parentDirectory
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.isNotEmpty()) return@runReadAction result
|
||||||
|
|
||||||
|
// Fallback for virtual/light files without a stable parent chain (e.g., tests)
|
||||||
|
val basePath = file.virtualFile?.path ?: return@runReadAction result
|
||||||
|
val scope = GlobalSearchScope.projectScope(file.project)
|
||||||
|
val dFiles = FilenameIndex.getAllFilesByExt(file.project, "d", scope)
|
||||||
|
for (vFile in dFiles) {
|
||||||
|
if (!vFile.name.endsWith(".lyng.d")) continue
|
||||||
|
if (vFile.path == basePath) continue
|
||||||
|
val parentPath = vFile.parent?.path ?: continue
|
||||||
|
if (basePath == parentPath || basePath.startsWith(parentPath.trimEnd('/') + "/")) {
|
||||||
|
if (seen.add(vFile.path)) {
|
||||||
|
psiManager.findFile(vFile)?.let { result.add(it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.isNotEmpty()) return@runReadAction result
|
||||||
|
|
||||||
|
// Fallback: scan all Lyng files in project index and filter by .lyng.d
|
||||||
|
val lyngFiles = FileTypeIndex.getFiles(LyngFileType, scope)
|
||||||
|
for (vFile in lyngFiles) {
|
||||||
|
if (!vFile.name.endsWith(".lyng.d")) continue
|
||||||
|
if (vFile.path == basePath) continue
|
||||||
|
if (seen.add(vFile.path)) {
|
||||||
|
psiManager.findFile(vFile)?.let { result.add(it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.isNotEmpty()) return@runReadAction result
|
||||||
|
|
||||||
|
// Final fallback: include all .lyng.d files in project scope
|
||||||
|
for (vFile in dFiles) {
|
||||||
|
if (!vFile.name.endsWith(".lyng.d")) continue
|
||||||
|
if (vFile.path == basePath) continue
|
||||||
|
if (seen.add(vFile.path)) {
|
||||||
|
psiManager.findFile(vFile)?.let { result.add(it) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getDeclarationFiles(file: PsiFile): List<PsiFile> = runReadAction {
|
||||||
|
collectDeclarationFiles(file)
|
||||||
|
}
|
||||||
|
|
||||||
fun getBinding(file: PsiFile): BindingSnapshot? = runReadAction {
|
fun getBinding(file: PsiFile): BindingSnapshot? = runReadAction {
|
||||||
getAnalysis(file)?.binding
|
getAnalysis(file)?.binding
|
||||||
}
|
}
|
||||||
@ -92,20 +143,38 @@ object LyngAstManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (built != null) {
|
if (built != null) {
|
||||||
val merged = built.mini
|
val isDecl = file.name.endsWith(".lyng.d")
|
||||||
if (merged != null && !file.name.endsWith(".lyng.d")) {
|
val merged = if (!isDecl && built.mini == null) {
|
||||||
|
MiniScript(MiniRange(built.source.startPos, built.source.startPos))
|
||||||
|
} else {
|
||||||
|
built.mini
|
||||||
|
}
|
||||||
|
if (merged != null && !isDecl) {
|
||||||
val dFiles = collectDeclarationFiles(file)
|
val dFiles = collectDeclarationFiles(file)
|
||||||
for (df in dFiles) {
|
for (df in dFiles) {
|
||||||
val dAnalysis = getAnalysis(df)
|
val dMini = getAnalysis(df)?.mini ?: run {
|
||||||
val dMini = dAnalysis?.mini ?: continue
|
val dText = df.viewProvider.contents.toString()
|
||||||
|
try {
|
||||||
|
val provider = IdeLenientImportProvider.create()
|
||||||
|
runBlocking {
|
||||||
|
LyngLanguageTools.analyze(
|
||||||
|
LyngAnalysisRequest(text = dText, fileName = df.name, importProvider = provider)
|
||||||
|
)
|
||||||
|
}.mini
|
||||||
|
} catch (_: Throwable) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
} ?: continue
|
||||||
merged.declarations.addAll(dMini.declarations)
|
merged.declarations.addAll(dMini.declarations)
|
||||||
merged.imports.addAll(dMini.imports)
|
merged.imports.addAll(dMini.imports)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val finalAnalysis = if (merged != null) {
|
val finalAnalysis = if (merged != null) {
|
||||||
|
val mergedImports = DocLookupUtils.canonicalImportedModules(merged, text)
|
||||||
built.copy(
|
built.copy(
|
||||||
mini = merged,
|
mini = merged,
|
||||||
importedModules = DocLookupUtils.canonicalImportedModules(merged, text)
|
importedModules = mergedImports,
|
||||||
|
diagnostics = filterDiagnostics(built.diagnostics, merged, text, mergedImports)
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
built
|
built
|
||||||
@ -118,4 +187,45 @@ object LyngAstManager {
|
|||||||
}
|
}
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun filterDiagnostics(
|
||||||
|
diagnostics: List<LyngDiagnostic>,
|
||||||
|
merged: MiniScript,
|
||||||
|
text: String,
|
||||||
|
importedModules: List<String>
|
||||||
|
): List<LyngDiagnostic> {
|
||||||
|
if (diagnostics.isEmpty()) return diagnostics
|
||||||
|
val declaredTopLevel = merged.declarations.map { it.name }.toSet()
|
||||||
|
|
||||||
|
val declaredMembers = linkedSetOf<String>()
|
||||||
|
val aggregatedClasses = DocLookupUtils.aggregateClasses(importedModules, merged)
|
||||||
|
for (cls in aggregatedClasses.values) {
|
||||||
|
cls.members.forEach { declaredMembers.add(it.name) }
|
||||||
|
cls.ctorFields.forEach { declaredMembers.add(it.name) }
|
||||||
|
cls.classFields.forEach { declaredMembers.add(it.name) }
|
||||||
|
}
|
||||||
|
merged.declarations.filterIsInstance<MiniEnumDecl>().forEach { en ->
|
||||||
|
DocLookupUtils.enumToSyntheticClass(en).members.forEach { declaredMembers.add(it.name) }
|
||||||
|
}
|
||||||
|
|
||||||
|
val builtinTopLevel = linkedSetOf<String>()
|
||||||
|
for (mod in importedModules) {
|
||||||
|
BuiltinDocRegistry.docsForModule(mod).forEach { builtinTopLevel.add(it.name) }
|
||||||
|
}
|
||||||
|
|
||||||
|
return diagnostics.filterNot { diag ->
|
||||||
|
val msg = diag.message
|
||||||
|
if (msg.startsWith("unresolved name: ")) {
|
||||||
|
val name = msg.removePrefix("unresolved name: ").trim()
|
||||||
|
name in declaredTopLevel || name in builtinTopLevel
|
||||||
|
} else if (msg.startsWith("unresolved member: ")) {
|
||||||
|
val name = msg.removePrefix("unresolved member: ").trim()
|
||||||
|
val range = diag.range
|
||||||
|
val dotLeft = if (range != null) DocLookupUtils.findDotLeft(text, range.start) else null
|
||||||
|
dotLeft != null && name in declaredMembers
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,127 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
|
||||||
|
*
|
||||||
|
* 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
|
||||||
|
*
|
||||||
|
* http://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.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
package net.sergeych.lyng.idea.definitions
|
||||||
|
|
||||||
|
import com.intellij.testFramework.fixtures.BasePlatformTestCase
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import net.sergeych.lyng.idea.docs.LyngDocumentationProvider
|
||||||
|
import net.sergeych.lyng.idea.navigation.LyngPsiReference
|
||||||
|
import net.sergeych.lyng.idea.settings.LyngFormatterSettings
|
||||||
|
import net.sergeych.lyng.idea.util.LyngAstManager
|
||||||
|
import net.sergeych.lyng.miniast.CompletionEngineLight
|
||||||
|
|
||||||
|
class LyngDefinitionFilesTest : BasePlatformTestCase() {
|
||||||
|
|
||||||
|
override fun getTestDataPath(): String = ""
|
||||||
|
|
||||||
|
private fun enableCompletion() {
|
||||||
|
LyngFormatterSettings.getInstance(project).enableLyngCompletionExperimental = true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun addDefinitionsFile() {
|
||||||
|
val defs = """
|
||||||
|
/** Utilities exposed via .lyng.d */
|
||||||
|
class Declared(val name: String) {
|
||||||
|
/** Size property */
|
||||||
|
val size: Int = 0
|
||||||
|
|
||||||
|
/** Returns greeting. */
|
||||||
|
fun greet(who: String): String = "hi " + who
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Top-level function. */
|
||||||
|
fun topFun(x: Int): Int = x + 1
|
||||||
|
""".trimIndent()
|
||||||
|
myFixture.addFileToProject("api.lyng.d", defs)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun test_CompletionsIncludeDefinitions() {
|
||||||
|
addDefinitionsFile()
|
||||||
|
enableCompletion()
|
||||||
|
run {
|
||||||
|
val code = """
|
||||||
|
val v = top<caret>
|
||||||
|
""".trimIndent()
|
||||||
|
myFixture.configureByText("main.lyng", code)
|
||||||
|
val text = myFixture.editor.document.text
|
||||||
|
val caret = myFixture.caretOffset
|
||||||
|
val analysis = LyngAstManager.getAnalysis(myFixture.file)
|
||||||
|
val engine = runBlocking { CompletionEngineLight.completeSuspend(text, caret, analysis?.mini, analysis?.binding).map { it.name } }
|
||||||
|
assertTrue("Expected topFun from .lyng.d; got=$engine", engine.contains("topFun"))
|
||||||
|
}
|
||||||
|
run {
|
||||||
|
val code = """
|
||||||
|
<caret>
|
||||||
|
""".trimIndent()
|
||||||
|
myFixture.configureByText("other.lyng", code)
|
||||||
|
val text = myFixture.editor.document.text
|
||||||
|
val caret = myFixture.caretOffset
|
||||||
|
val analysis = LyngAstManager.getAnalysis(myFixture.file)
|
||||||
|
val engine = runBlocking { CompletionEngineLight.completeSuspend(text, caret, analysis?.mini, analysis?.binding).map { it.name } }
|
||||||
|
assertTrue("Expected Declared from .lyng.d; got=$engine", engine.contains("Declared"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun test_GotoDefinitionResolvesToDefinitionFile() {
|
||||||
|
addDefinitionsFile()
|
||||||
|
val code = """
|
||||||
|
val x = topFun(1)
|
||||||
|
val y = Declared("x")
|
||||||
|
y.gre<caret>et("me")
|
||||||
|
""".trimIndent()
|
||||||
|
myFixture.configureByText("main.lyng", code)
|
||||||
|
val offset = myFixture.caretOffset
|
||||||
|
val element = myFixture.file.findElementAt(offset) ?: myFixture.file.findElementAt((offset - 1).coerceAtLeast(0))
|
||||||
|
assertNotNull("Expected element at caret for resolve", element)
|
||||||
|
val ref = LyngPsiReference(element!!)
|
||||||
|
val resolved = ref.resolve()
|
||||||
|
assertNotNull("Expected reference to resolve", resolved)
|
||||||
|
assertTrue("Expected .lyng.d target; got=${resolved!!.containingFile.name}", resolved.containingFile.name.endsWith(".lyng.d"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun test_QuickDocUsesDefinitionDocs() {
|
||||||
|
addDefinitionsFile()
|
||||||
|
val code = """
|
||||||
|
val y = Declared("x")
|
||||||
|
y.gre<caret>et("me")
|
||||||
|
""".trimIndent()
|
||||||
|
myFixture.configureByText("main.lyng", code)
|
||||||
|
val provider = LyngDocumentationProvider()
|
||||||
|
val offset = myFixture.caretOffset
|
||||||
|
val element = myFixture.file.findElementAt(offset) ?: myFixture.file.findElementAt((offset - 1).coerceAtLeast(0))
|
||||||
|
assertNotNull("Expected element at caret for doc", element)
|
||||||
|
val doc = provider.generateDoc(element, element)
|
||||||
|
assertNotNull("Expected Quick Doc", doc)
|
||||||
|
assertTrue("Doc should include summary; got=$doc", doc!!.contains("Returns greeting"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun test_DiagnosticsIgnoreDefinitionSymbols() {
|
||||||
|
addDefinitionsFile()
|
||||||
|
val code = """
|
||||||
|
val x = topFun(1)
|
||||||
|
val y = Declared("x")
|
||||||
|
y.greet("me")
|
||||||
|
""".trimIndent()
|
||||||
|
myFixture.configureByText("main.lyng", code)
|
||||||
|
val analysis = LyngAstManager.getAnalysis(myFixture.file)
|
||||||
|
val messages = analysis?.diagnostics?.map { it.message } ?: emptyList()
|
||||||
|
assertTrue("Should not report unresolved name for topFun", messages.none { it.contains("unresolved name: topFun") })
|
||||||
|
assertTrue("Should not report unresolved name for Declared", messages.none { it.contains("unresolved name: Declared") })
|
||||||
|
assertTrue("Should not report unresolved member for greet", messages.none { it.contains("unresolved member: greet") })
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -178,6 +178,7 @@ object CompletionEngineLight {
|
|||||||
is MiniMemberDecl -> node.range
|
is MiniMemberDecl -> node.range
|
||||||
else -> return
|
else -> return
|
||||||
}
|
}
|
||||||
|
if (range.start.source != src || range.end.source != src) return
|
||||||
val start = src.offsetOf(range.start)
|
val start = src.offsetOf(range.start)
|
||||||
val end = src.offsetOf(range.end).coerceAtMost(text.length)
|
val end = src.offsetOf(range.end).coerceAtMost(text.length)
|
||||||
|
|
||||||
@ -372,9 +373,12 @@ object CompletionEngineLight {
|
|||||||
val src = Source("<engine>", text)
|
val src = Source("<engine>", text)
|
||||||
val provider = LenientImportProvider.create()
|
val provider = LenientImportProvider.create()
|
||||||
Compiler.compileWithMini(src, provider, sink)
|
Compiler.compileWithMini(src, provider, sink)
|
||||||
sink.build()
|
sink.build() ?: MiniScript(MiniRange(src.startPos, src.startPos))
|
||||||
} catch (_: Throwable) {
|
} catch (_: Throwable) {
|
||||||
sink.build()
|
sink.build() ?: run {
|
||||||
|
val src = Source("<engine>", text)
|
||||||
|
MiniScript(MiniRange(src.startPos, src.startPos))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -387,6 +391,7 @@ object CompletionEngineLight {
|
|||||||
|
|
||||||
// Text helpers
|
// Text helpers
|
||||||
private fun prefixAt(text: String, offset: Int): String {
|
private fun prefixAt(text: String, offset: Int): String {
|
||||||
|
if (text.isEmpty()) return ""
|
||||||
val off = offset.coerceIn(0, text.length)
|
val off = offset.coerceIn(0, text.length)
|
||||||
var i = (off - 1).coerceAtLeast(0)
|
var i = (off - 1).coerceAtLeast(0)
|
||||||
while (i >= 0 && DocLookupUtils.isIdentChar(text[i])) i--
|
while (i >= 0 && DocLookupUtils.isIdentChar(text[i])) i--
|
||||||
|
|||||||
@ -1163,6 +1163,7 @@ object DocLookupUtils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun findDotLeft(text: String, offset: Int): Int? {
|
fun findDotLeft(text: String, offset: Int): Int? {
|
||||||
|
if (text.isEmpty()) return null
|
||||||
var i = (offset - 1).coerceAtLeast(0)
|
var i = (offset - 1).coerceAtLeast(0)
|
||||||
while (i >= 0 && text[i].isWhitespace()) i--
|
while (i >= 0 && text[i].isWhitespace()) i--
|
||||||
return if (i >= 0 && text[i] == '.') i else null
|
return if (i >= 0 && text[i] == '.') i else null
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user