idea plugin 0.0.2-SNAPSHOT, improced, added reformat code. Formatting tools improved in lynglib. Site information added
This commit is contained in:
parent
06e8e1579d
commit
ec49bbbf52
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
*.zip filter=lfs diff=lfs merge=lfs -text
|
||||||
@ -46,6 +46,7 @@ esac
|
|||||||
|
|
||||||
die() { echo "ERROR: $*" 1>&2 ; exit 1; }
|
die() { echo "ERROR: $*" 1>&2 ; exit 1; }
|
||||||
|
|
||||||
|
#./gradlew site:clean site:jsBrowserDistribution || die "compilation failed"
|
||||||
./gradlew site:clean site:jsBrowserDistribution || die "compilation failed"
|
./gradlew site:clean site:jsBrowserDistribution || die "compilation failed"
|
||||||
|
|
||||||
if [[ $? != 0 ]]; then
|
if [[ $? != 0 ]]; then
|
||||||
@ -77,8 +78,8 @@ rsync -e "ssh -p ${SSH_PORT}" -avz -r -d --delete ${SRC}/* ${SSH_HOST}:${ROOT}/b
|
|||||||
checkState
|
checkState
|
||||||
#rsync -e "ssh -p ${SSH_PORT}" -avz ./static/* ${SSH_HOST}:${ROOT}/build/dist
|
#rsync -e "ssh -p ${SSH_PORT}" -avz ./static/* ${SSH_HOST}:${ROOT}/build/dist
|
||||||
#checkState
|
#checkState
|
||||||
#rsync -e "ssh -p ${SSH_PORT}" -avz -r -d --delete private_data/* ${SSH_HOST}:${ROOT}/build/private_data
|
rsync -e "ssh -p ${SSH_PORT}" -avz -r -d --delete distributables/* ${SSH_HOST}:${ROOT}/build/dist/distributables
|
||||||
#checkState
|
checkState
|
||||||
|
|
||||||
echo
|
echo
|
||||||
echo finalizing the deploy...
|
echo finalizing the deploy...
|
||||||
|
|||||||
@ -21,7 +21,10 @@ set -e
|
|||||||
|
|
||||||
file=./lyng/build/bin/linuxX64/releaseExecutable/lyng.kexe
|
file=./lyng/build/bin/linuxX64/releaseExecutable/lyng.kexe
|
||||||
|
|
||||||
./gradlew :lyng:linkReleaseExecutableLinuxX64
|
#./gradlew :lyng:linkReleaseExecutableLinuxX64
|
||||||
strip $file
|
#strip $file
|
||||||
upx $file
|
#upx $file
|
||||||
cp $file ~/bin/lyng
|
cp $file ~/bin/lyng
|
||||||
|
cp $file ./distributables/lyng
|
||||||
|
zip ./distributables/lyng-linuxX64 ./distributables/lyng
|
||||||
|
rm ./distributables/lyng
|
||||||
|
|||||||
BIN
distributables/lyng-idea-0.0.2-SNAPSHOT.zip
(Stored with Git LFS)
Normal file
BIN
distributables/lyng-idea-0.0.2-SNAPSHOT.zip
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
distributables/lyng-linuxX64.zip
(Stored with Git LFS)
Normal file
BIN
distributables/lyng-linuxX64.zip
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
distributables/lyng-textmate.zip
(Stored with Git LFS)
Normal file
BIN
distributables/lyng-textmate.zip
(Stored with Git LFS)
Normal file
Binary file not shown.
29
docs/idea_plugin.md
Normal file
29
docs/idea_plugin.md
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
# Plugin for IntelliJ IDEA
|
||||||
|
|
||||||
|
[//]: # (excludeFromIndex)
|
||||||
|
|
||||||
|
We introduce the alpha version of the plugin for IntelliJ IDEA 2024.3.x+ IDE variants. It is compatible with 2025.x and
|
||||||
|
should be compatible with other IDEA flavors, notably [OpenIDE](https://openide.ru/). It supports the following features:
|
||||||
|
|
||||||
|
- syntax highlighting (2 stage, fast and more accurate that analyses in background)
|
||||||
|
- reformat code (indents, spaces)
|
||||||
|
- reformat on paste
|
||||||
|
- smart enter key
|
||||||
|
|
||||||
|
Features are configurable via the plugin settings page, in system settings.
|
||||||
|
|
||||||
|
> Recommended for IntelliJ-based IDEs: While IntelliJ can import TextMate bundles
|
||||||
|
> (Settings/Preferences → Editor → TextMate Bundles), the native Lyng plugin provides
|
||||||
|
> better support (formatting, smart enter, background analysis, etc.). Prefer installing
|
||||||
|
> this plugin over using a TextMate bundle.
|
||||||
|
|
||||||
|
### Install
|
||||||
|
|
||||||
|
- From ZIP: download the archive below, then in IntelliJ IDEA open Settings/Preferences → Plugins →
|
||||||
|
gear icon → Install Plugin from Disk… and select the downloaded ZIP. Restart IDE if prompted.
|
||||||
|
- Alternatively, if/when the plugin is published to a marketplace, you will be able to install it
|
||||||
|
directly from the “Marketplace” tab (not yet available).
|
||||||
|
|
||||||
|
### [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)
|
||||||
@ -1,4 +1,4 @@
|
|||||||
//#!/bin/env lyng
|
#!/bin/env lyng
|
||||||
|
|
||||||
import lyng.io.fs
|
import lyng.io.fs
|
||||||
import lyng.stdlib
|
import lyng.stdlib
|
||||||
@ -6,16 +6,8 @@ import lyng.stdlib
|
|||||||
val files = Path("../..").list().toList()
|
val files = Path("../..").list().toList()
|
||||||
val longestNameLength = files.maxOf { it.name.length }
|
val longestNameLength = files.maxOf { it.name.length }
|
||||||
|
|
||||||
/*
|
|
||||||
The comment for our test1.
|
|
||||||
There are _more_ data
|
|
||||||
*/
|
|
||||||
fun test21() {
|
|
||||||
21
|
|
||||||
}
|
|
||||||
|
|
||||||
|
val format = "%"+(longestNameLength+1) +"s %d"
|
||||||
val format = "%-"+(longestNameLength+1) +"s %d"
|
|
||||||
for( f in files )
|
for( f in files )
|
||||||
{
|
{
|
||||||
var name = f.name
|
var name = f.name
|
||||||
@ -24,5 +16,4 @@ name += "/"
|
|||||||
println( format(name, f.size()) )
|
println( format(name, f.size()) )
|
||||||
}
|
}
|
||||||
|
|
||||||
test21()
|
|
||||||
|
|
||||||
|
|||||||
77
docs/textmate_bundle.md
Normal file
77
docs/textmate_bundle.md
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
# TextMate bundle
|
||||||
|
|
||||||
|
[//]: # (excludeFromIndex)
|
||||||
|
|
||||||
|
The TextMate-format bundle contains a syntax definition for initial language support in
|
||||||
|
popular editors that understand TextMate grammars: TextMate, Visual Studio Code, Sublime Text, etc.
|
||||||
|
|
||||||
|
- [Download TextMate Bundle for Lyng](https://lynglang.com/distributables/lyng-textmate.zip)
|
||||||
|
|
||||||
|
> Note for IntelliJ-based IDEs (IntelliJ IDEA, Fleet, etc.): although you can import TextMate
|
||||||
|
> bundles there (Settings/Preferences → Editor → TextMate Bundles), we strongly recommend using the
|
||||||
|
> dedicated plugin instead — it provides much better support (formatting, smart enter, background
|
||||||
|
> analysis, etc.). See: [IDEA Plugin](#/docs/idea_plugin.md).
|
||||||
|
|
||||||
|
## Visual Studio Code
|
||||||
|
|
||||||
|
VS Code uses TextMate grammars packaged as extensions. A minimal local extension is easy to set up:
|
||||||
|
|
||||||
|
1) Download and unzip the bundle above. Inside you will find the grammar file (usually
|
||||||
|
`*.tmLanguage.json` or `*.tmLanguage` plist).
|
||||||
|
2) Create a new folder somewhere, e.g. `lyng-textmate-vscode/` with the following structure:
|
||||||
|
|
||||||
|
```
|
||||||
|
lyng-textmate-vscode/
|
||||||
|
package.json
|
||||||
|
syntaxes/
|
||||||
|
lyng.tmLanguage.json # copy the grammar file here (rename if needed)
|
||||||
|
```
|
||||||
|
|
||||||
|
3) Put this minimal `package.json` into that folder (adjust file names if needed):
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
"name": "lyng-textmate",
|
||||||
|
"displayName": "Lyng (TextMate grammar)",
|
||||||
|
"publisher": "local",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"engines": { "vscode": "^1.70.0" },
|
||||||
|
"contributes": {
|
||||||
|
"languages": [
|
||||||
|
{ "id": "lyng", "aliases": ["Lyng"], "extensions": [".lyng"] }
|
||||||
|
],
|
||||||
|
"grammars": [
|
||||||
|
{
|
||||||
|
"language": "lyng",
|
||||||
|
"scopeName": "source.lyng",
|
||||||
|
"path": "./syntaxes/lyng.tmLanguage.json"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
4) Open a terminal in `lyng-textmate-vscode/` and run:
|
||||||
|
|
||||||
|
```
|
||||||
|
code --install-extension .
|
||||||
|
```
|
||||||
|
|
||||||
|
Alternatively, open the folder in VS Code and press F5 to run an Extension Development Host.
|
||||||
|
5) Reload VS Code. Files with the `.lyng` extension should now get Lyng highlighting.
|
||||||
|
|
||||||
|
## Sublime Text 3/4
|
||||||
|
|
||||||
|
1) Download and unzip the bundle.
|
||||||
|
2) In Sublime Text, use “Preferences → Browse Packages…”, then copy the unzipped bundle
|
||||||
|
to a folder like `Packages/Lyng/`.
|
||||||
|
3) Open a `.lyng` file; Sublime should pick up the syntax automatically. If not, use
|
||||||
|
“View → Syntax → Lyng”.
|
||||||
|
|
||||||
|
## TextMate 2
|
||||||
|
|
||||||
|
1) Download and unzip the bundle.
|
||||||
|
2) Double‑click the `.tmBundle`/grammar package or drag it onto TextMate to install, or place
|
||||||
|
it into `~/Library/Application Support/TextMate/Bundles/`.
|
||||||
|
3) Restart TextMate if needed and open a `.lyng` file.
|
||||||
|
|
||||||
@ -21,7 +21,7 @@ plugins {
|
|||||||
}
|
}
|
||||||
|
|
||||||
group = "net.sergeych.lyng"
|
group = "net.sergeych.lyng"
|
||||||
version = "0.0.1-SNAPSHOT"
|
version = "0.0.2-SNAPSHOT"
|
||||||
|
|
||||||
kotlin {
|
kotlin {
|
||||||
jvmToolchain(17)
|
jvmToolchain(17)
|
||||||
@ -41,8 +41,10 @@ dependencies {
|
|||||||
|
|
||||||
intellij {
|
intellij {
|
||||||
type.set("IC")
|
type.set("IC")
|
||||||
// Run sandbox on IntelliJ IDEA 2024.3.x
|
// Build against a modern baseline. Install range is controlled by since/until below.
|
||||||
version.set("2024.3.1")
|
version.set("2024.3.1")
|
||||||
|
// We manage <idea-version> ourselves in plugin.xml to keep it open-ended (no upper cap)
|
||||||
|
updateSinceUntilBuild.set(false)
|
||||||
// Include only available bundled plugins for this IDE build
|
// Include only available bundled plugins for this IDE build
|
||||||
plugins.set(listOf(
|
plugins.set(listOf(
|
||||||
"com.intellij.java"
|
"com.intellij.java"
|
||||||
@ -51,8 +53,24 @@ intellij {
|
|||||||
|
|
||||||
tasks {
|
tasks {
|
||||||
patchPluginXml {
|
patchPluginXml {
|
||||||
// Compatible with 2024.3+
|
// Keep version and other metadata patched by Gradle, but since/until are controlled in plugin.xml.
|
||||||
sinceBuild.set("243")
|
// (intellij.updateSinceUntilBuild=false prevents Gradle from injecting an until-build cap)
|
||||||
untilBuild.set(null as String?)
|
}
|
||||||
|
|
||||||
|
// Build an installable plugin zip and copy it to $PROJECT_ROOT/distributables
|
||||||
|
// Usage: ./gradlew :lyng-idea:buildInstallablePlugin
|
||||||
|
// It depends on buildPlugin and overwrites any existing file with the same name
|
||||||
|
register<Copy>("buildInstallablePlugin") {
|
||||||
|
dependsOn("buildPlugin")
|
||||||
|
|
||||||
|
// The Gradle IntelliJ Plugin produces: build/distributions/<project.name>-<version>.zip
|
||||||
|
val zipName = "${project.name}-${project.version}.zip"
|
||||||
|
val sourceZip = layout.buildDirectory.file("distributions/$zipName")
|
||||||
|
|
||||||
|
from(sourceZip)
|
||||||
|
into(rootProject.layout.projectDirectory.dir("distributables"))
|
||||||
|
|
||||||
|
// Overwrite if a file with the same name exists
|
||||||
|
duplicatesStrategy = DuplicatesStrategy.INCLUDE
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -28,6 +28,10 @@ import com.intellij.psi.PsiFile
|
|||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import net.sergeych.lyng.Compiler
|
import net.sergeych.lyng.Compiler
|
||||||
import net.sergeych.lyng.Source
|
import net.sergeych.lyng.Source
|
||||||
|
import net.sergeych.lyng.binding.Binder
|
||||||
|
import net.sergeych.lyng.binding.SymbolKind
|
||||||
|
import net.sergeych.lyng.highlight.HighlightKind
|
||||||
|
import net.sergeych.lyng.highlight.SimpleLyngHighlighter
|
||||||
import net.sergeych.lyng.highlight.offsetOf
|
import net.sergeych.lyng.highlight.offsetOf
|
||||||
import net.sergeych.lyng.idea.highlight.LyngHighlighterColors
|
import net.sergeych.lyng.idea.highlight.LyngHighlighterColors
|
||||||
import net.sergeych.lyng.idea.util.IdeLenientImportProvider
|
import net.sergeych.lyng.idea.util.IdeLenientImportProvider
|
||||||
@ -59,7 +63,8 @@ class LyngExternalAnnotator : ExternalAnnotator<LyngExternalAnnotator.Input, Lyn
|
|||||||
val src = Source("<ide>", text)
|
val src = Source("<ide>", text)
|
||||||
val provider = IdeLenientImportProvider.create()
|
val provider = IdeLenientImportProvider.create()
|
||||||
runBlocking { Compiler.compileWithMini(src, provider, sink) }
|
runBlocking { Compiler.compileWithMini(src, provider, sink) }
|
||||||
} catch (_: Throwable) {
|
} catch (e: Throwable) {
|
||||||
|
if (e is com.intellij.openapi.progress.ProcessCanceledException) throw e
|
||||||
// Fail softly: no semantic layer this pass
|
// Fail softly: no semantic layer this pass
|
||||||
return Result(collectedInfo.modStamp, emptyList())
|
return Result(collectedInfo.modStamp, emptyList())
|
||||||
}
|
}
|
||||||
@ -67,7 +72,7 @@ class LyngExternalAnnotator : ExternalAnnotator<LyngExternalAnnotator.Input, Lyn
|
|||||||
val mini = sink.build() ?: return Result(collectedInfo.modStamp, emptyList())
|
val mini = sink.build() ?: return Result(collectedInfo.modStamp, emptyList())
|
||||||
val source = Source("<ide>", text)
|
val source = Source("<ide>", text)
|
||||||
|
|
||||||
val out = ArrayList<Span>(64)
|
val out = ArrayList<Span>(256)
|
||||||
|
|
||||||
fun putRange(start: Int, end: Int, key: com.intellij.openapi.editor.colors.TextAttributesKey) {
|
fun putRange(start: Int, end: Int, key: com.intellij.openapi.editor.colors.TextAttributesKey) {
|
||||||
if (start in 0..end && end <= text.length && start < end) out += Span(start, end, key)
|
if (start in 0..end && end <= text.length && start < end) out += Span(start, end, key)
|
||||||
@ -85,7 +90,7 @@ class LyngExternalAnnotator : ExternalAnnotator<LyngExternalAnnotator.Input, Lyn
|
|||||||
// Declarations
|
// Declarations
|
||||||
for (d in mini.declarations) {
|
for (d in mini.declarations) {
|
||||||
when (d) {
|
when (d) {
|
||||||
is MiniFunDecl -> putName(d.nameStart, d.name, LyngHighlighterColors.FUNCTION)
|
is MiniFunDecl -> putName(d.nameStart, d.name, LyngHighlighterColors.FUNCTION_DECLARATION)
|
||||||
is MiniClassDecl -> putName(d.nameStart, d.name, LyngHighlighterColors.TYPE)
|
is MiniClassDecl -> putName(d.nameStart, d.name, LyngHighlighterColors.TYPE)
|
||||||
is MiniValDecl -> putName(
|
is MiniValDecl -> putName(
|
||||||
d.nameStart,
|
d.nameStart,
|
||||||
@ -142,6 +147,86 @@ class LyngExternalAnnotator : ExternalAnnotator<LyngExternalAnnotator.Input, Lyn
|
|||||||
}
|
}
|
||||||
|
|
||||||
ProgressManager.checkCanceled()
|
ProgressManager.checkCanceled()
|
||||||
|
|
||||||
|
// Semantic usages via Binder (best-effort)
|
||||||
|
try {
|
||||||
|
val binding = Binder.bind(text, mini)
|
||||||
|
|
||||||
|
// Map declaration ranges to avoid duplicating them as usages
|
||||||
|
val declKeys = HashSet<Pair<Int, Int>>(binding.symbols.size * 2)
|
||||||
|
for (sym in binding.symbols) declKeys += (sym.declStart to sym.declEnd)
|
||||||
|
|
||||||
|
fun keyForKind(k: SymbolKind) = when (k) {
|
||||||
|
SymbolKind.Function -> LyngHighlighterColors.FUNCTION
|
||||||
|
SymbolKind.Class, SymbolKind.Enum -> LyngHighlighterColors.TYPE
|
||||||
|
SymbolKind.Param -> LyngHighlighterColors.PARAMETER
|
||||||
|
SymbolKind.Val -> LyngHighlighterColors.VALUE
|
||||||
|
SymbolKind.Var -> LyngHighlighterColors.VARIABLE
|
||||||
|
}
|
||||||
|
|
||||||
|
// Track covered ranges to not override later heuristics
|
||||||
|
val covered = HashSet<Pair<Int, Int>>()
|
||||||
|
|
||||||
|
for (ref in binding.references) {
|
||||||
|
val key = ref.start to ref.end
|
||||||
|
if (declKeys.contains(key)) continue
|
||||||
|
val sym = binding.symbols.firstOrNull { it.id == ref.symbolId } ?: continue
|
||||||
|
val color = keyForKind(sym.kind)
|
||||||
|
putRange(ref.start, ref.end, color)
|
||||||
|
covered += key
|
||||||
|
}
|
||||||
|
|
||||||
|
// Heuristics on top of binder: function call-sites and simple name-based roles
|
||||||
|
ProgressManager.checkCanceled()
|
||||||
|
|
||||||
|
val tokens = try { SimpleLyngHighlighter().highlight(text) } catch (_: Throwable) { emptyList() }
|
||||||
|
|
||||||
|
fun isFollowedByParenOrBlock(rangeEnd: Int): Boolean {
|
||||||
|
var i = rangeEnd
|
||||||
|
while (i < text.length) {
|
||||||
|
val ch = text[i]
|
||||||
|
if (ch == ' ' || ch == '\t' || ch == '\r' || ch == '\n') { i++; continue }
|
||||||
|
return ch == '(' || ch == '{'
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build simple name -> role map for top-level vals/vars and parameters
|
||||||
|
val nameRole = HashMap<String, com.intellij.openapi.editor.colors.TextAttributesKey>(8)
|
||||||
|
for (d in mini.declarations) when (d) {
|
||||||
|
is MiniValDecl -> nameRole[d.name] = if (d.mutable) LyngHighlighterColors.VARIABLE else LyngHighlighterColors.VALUE
|
||||||
|
is MiniFunDecl -> d.params.forEach { p -> nameRole[p.name] = LyngHighlighterColors.PARAMETER }
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (s in tokens) if (s.kind == HighlightKind.Identifier) {
|
||||||
|
val start = s.range.start
|
||||||
|
val end = s.range.endExclusive
|
||||||
|
val key = start to end
|
||||||
|
if (key in covered || key in declKeys) continue
|
||||||
|
|
||||||
|
// Call-site detection first so it wins over var/param role
|
||||||
|
if (isFollowedByParenOrBlock(end)) {
|
||||||
|
putRange(start, end, LyngHighlighterColors.FUNCTION)
|
||||||
|
covered += key
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simple role by known names
|
||||||
|
val ident = try { text.substring(start, end) } catch (_: Throwable) { null }
|
||||||
|
if (ident != null) {
|
||||||
|
val roleKey = nameRole[ident]
|
||||||
|
if (roleKey != null) {
|
||||||
|
putRange(start, end, roleKey)
|
||||||
|
covered += key
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
// Must rethrow cancellation; otherwise ignore binder failures (best-effort)
|
||||||
|
if (e is com.intellij.openapi.progress.ProcessCanceledException) throw e
|
||||||
|
}
|
||||||
|
|
||||||
return Result(collectedInfo.modStamp, out)
|
return Result(collectedInfo.modStamp, out)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -60,6 +60,7 @@ class LyngColorSettingsPage : ColorSettingsPage {
|
|||||||
AttributesDescriptor("Variable (semantic)", LyngHighlighterColors.VARIABLE),
|
AttributesDescriptor("Variable (semantic)", LyngHighlighterColors.VARIABLE),
|
||||||
AttributesDescriptor("Value (semantic)", LyngHighlighterColors.VALUE),
|
AttributesDescriptor("Value (semantic)", LyngHighlighterColors.VALUE),
|
||||||
AttributesDescriptor("Function (semantic)", LyngHighlighterColors.FUNCTION),
|
AttributesDescriptor("Function (semantic)", LyngHighlighterColors.FUNCTION),
|
||||||
|
AttributesDescriptor("Function declaration (semantic)", LyngHighlighterColors.FUNCTION_DECLARATION),
|
||||||
AttributesDescriptor("Type (semantic)", LyngHighlighterColors.TYPE),
|
AttributesDescriptor("Type (semantic)", LyngHighlighterColors.TYPE),
|
||||||
AttributesDescriptor("Namespace (semantic)", LyngHighlighterColors.NAMESPACE),
|
AttributesDescriptor("Namespace (semantic)", LyngHighlighterColors.NAMESPACE),
|
||||||
AttributesDescriptor("Parameter (semantic)", LyngHighlighterColors.PARAMETER),
|
AttributesDescriptor("Parameter (semantic)", LyngHighlighterColors.PARAMETER),
|
||||||
|
|||||||
@ -46,15 +46,22 @@ object LyngHighlighterColors {
|
|||||||
"LYNG_PUNCT", DefaultLanguageHighlighterColors.DOT
|
"LYNG_PUNCT", DefaultLanguageHighlighterColors.DOT
|
||||||
)
|
)
|
||||||
|
|
||||||
// Semantic layer keys (placeholders for now)
|
// Semantic layer keys
|
||||||
val VARIABLE: TextAttributesKey = TextAttributesKey.createTextAttributesKey(
|
val VARIABLE: TextAttributesKey = TextAttributesKey.createTextAttributesKey(
|
||||||
"LYNG_VARIABLE", DefaultLanguageHighlighterColors.LOCAL_VARIABLE
|
// Use a distinctive default to ensure visibility across common themes.
|
||||||
|
// Users can still customize it separately from VALUE.
|
||||||
|
"LYNG_VARIABLE", DefaultLanguageHighlighterColors.INSTANCE_FIELD
|
||||||
)
|
)
|
||||||
val VALUE: TextAttributesKey = TextAttributesKey.createTextAttributesKey(
|
val VALUE: TextAttributesKey = TextAttributesKey.createTextAttributesKey(
|
||||||
"LYNG_VALUE", DefaultLanguageHighlighterColors.INSTANCE_FIELD
|
"LYNG_VALUE", DefaultLanguageHighlighterColors.INSTANCE_FIELD
|
||||||
)
|
)
|
||||||
val FUNCTION: TextAttributesKey = TextAttributesKey.createTextAttributesKey(
|
val FUNCTION: TextAttributesKey = TextAttributesKey.createTextAttributesKey(
|
||||||
"LYNG_FUNCTION", DefaultLanguageHighlighterColors.FUNCTION_CALL
|
// Primary approach: make function calls as visible as declarations by default
|
||||||
|
// (users can still customize separately in the color scheme UI).
|
||||||
|
"LYNG_FUNCTION", DefaultLanguageHighlighterColors.FUNCTION_DECLARATION
|
||||||
|
)
|
||||||
|
val FUNCTION_DECLARATION: TextAttributesKey = TextAttributesKey.createTextAttributesKey(
|
||||||
|
"LYNG_FUNCTION_DECLARATION", DefaultLanguageHighlighterColors.FUNCTION_DECLARATION
|
||||||
)
|
)
|
||||||
val TYPE: TextAttributesKey = TextAttributesKey.createTextAttributesKey(
|
val TYPE: TextAttributesKey = TextAttributesKey.createTextAttributesKey(
|
||||||
"LYNG_TYPE", DefaultLanguageHighlighterColors.CLASS_REFERENCE
|
"LYNG_TYPE", DefaultLanguageHighlighterColors.CLASS_REFERENCE
|
||||||
|
|||||||
@ -16,13 +16,17 @@
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
<idea-plugin>
|
<idea-plugin>
|
||||||
|
<!-- Open-ended compatibility: 2024.3+ (build 243 and newer) -->
|
||||||
|
<idea-version since-build="243"/>
|
||||||
<id>net.sergeych.lyng.idea</id>
|
<id>net.sergeych.lyng.idea</id>
|
||||||
<name>Lyng Language Support</name>
|
<name>Lyng Language Support</name>
|
||||||
<vendor email="real.sergeych@gmail.com">Sergeych Works</vendor>
|
<vendor email="real.sergeych@gmail.com">Sergey Chernov</vendor>
|
||||||
|
|
||||||
<description>
|
<description>
|
||||||
<![CDATA[
|
<![CDATA[
|
||||||
Basic Lyng language support: file type, syntax highlighting scaffold, and quick docs/highlighter stubs.
|
Basic Lyng language support: file type, syntax highlighting,
|
||||||
|
editing assistance (on Enter indent), reformatting code (indents and spaces),
|
||||||
|
and quick docs.
|
||||||
]]>
|
]]>
|
||||||
</description>
|
</description>
|
||||||
|
|
||||||
|
|||||||
@ -15,16 +15,17 @@
|
|||||||
- limitations under the License.
|
- limitations under the License.
|
||||||
-
|
-
|
||||||
-->
|
-->
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" role="img" aria-label="Lyng favicon">
|
||||||
<svg width="40" height="40" viewBox="0 0 40 40" xmlns="http://www.w3.org/2000/svg" aria-labelledby="title" role="img">
|
|
||||||
<title>Lyng Plugin Icon (temporary λ)</title>
|
|
||||||
<defs>
|
|
||||||
<style>
|
<style>
|
||||||
.glyph { fill: none; stroke: currentColor; stroke-width: 3; stroke-linecap: round; stroke-linejoin: round; }
|
:root { color-scheme: light dark; }
|
||||||
|
.mark { fill: currentColor; }
|
||||||
|
.math { font-family: 'STIX Two Math', 'Cambria Math', 'Times New Roman', serif; }
|
||||||
</style>
|
</style>
|
||||||
</defs>
|
<g class="mark math">
|
||||||
<g transform="translate(2,2)">
|
<!-- Keep favicon legible: lambda with superscript y only -->
|
||||||
<path class="glyph" d="M12 6 L18 22 C19.2 25.5 22.2 28 26 28 L32 28"/>
|
<text x="3" y="17" font-size="19" font-weight="700"
|
||||||
<path class="glyph" d="M18 22 L8 34"/>
|
color="#009000" stroke="#002000" stroke-width="0.3">λ</text>
|
||||||
|
<text x="11.2" y="4" font-size="16" color="#009000" stroke="#002000"
|
||||||
|
stroke-width="0.1">y</text>
|
||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.3 KiB |
@ -15,16 +15,17 @@
|
|||||||
- limitations under the License.
|
- limitations under the License.
|
||||||
-
|
-
|
||||||
-->
|
-->
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" role="img" aria-label="Lyng favicon">
|
||||||
<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" aria-labelledby="title" role="img">
|
|
||||||
<title>Lyng File Icon (temporary λ)</title>
|
|
||||||
<defs>
|
|
||||||
<style>
|
<style>
|
||||||
.g { fill: none; stroke: currentColor; stroke-width: 1.6; stroke-linecap: round; stroke-linejoin: round; }
|
:root { color-scheme: light dark; }
|
||||||
|
.mark { fill: currentColor; }
|
||||||
|
.math { font-family: 'STIX Two Math', 'Cambria Math', 'Times New Roman', serif; }
|
||||||
</style>
|
</style>
|
||||||
</defs>
|
<g class="mark math">
|
||||||
<g transform="translate(0.5,0.5)">
|
<!-- Keep favicon legible: lambda with superscript y only -->
|
||||||
<path class="g" d="M4.5 2.5 L7 9.5 C7.6 11.2 9.0 12.5 11.0 12.5 L14.5 12.5"/>
|
<text x="3" y="17" font-size="19" font-weight="700"
|
||||||
<path class="g" d="M7 9.5 L2.5 14.5"/>
|
color="#009000" stroke="#002000" stroke-width="0.3">λ</text>
|
||||||
|
<text x="11.2" y="4" font-size="16" color="#009000" stroke="#002000"
|
||||||
|
stroke-width="0.1">y</text>
|
||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.3 KiB |
@ -198,6 +198,58 @@ object Binder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Extra pass: detect local val/var declarations that are not present in mini.declarations
|
||||||
|
// (e.g., locals inside blocks and top-level statements). Use token stream heuristics.
|
||||||
|
run {
|
||||||
|
val spans = highlighter.highlight(text)
|
||||||
|
var iTok = 0
|
||||||
|
while (iTok < spans.size) {
|
||||||
|
val t = spans[iTok]
|
||||||
|
if (t.kind == HighlightKind.Keyword) {
|
||||||
|
val kw = try { text.substring(t.range.start, t.range.endExclusive) } catch (_: Throwable) { "" }
|
||||||
|
if (kw.equals("val", ignoreCase = true) || kw.equals("var", ignoreCase = true)) {
|
||||||
|
// Find the next Identifier span which should be the declared name
|
||||||
|
var j = iTok + 1
|
||||||
|
var nameStart = -1
|
||||||
|
var nameEnd = -1
|
||||||
|
while (j < spans.size) {
|
||||||
|
val s = spans[j]
|
||||||
|
if (s.kind == HighlightKind.Identifier) {
|
||||||
|
nameStart = s.range.start
|
||||||
|
nameEnd = s.range.endExclusive
|
||||||
|
break
|
||||||
|
}
|
||||||
|
// Stop if we hit newline/comment too far ahead (avoid wide scans)
|
||||||
|
if (s.kind == HighlightKind.Comment) break
|
||||||
|
j++
|
||||||
|
}
|
||||||
|
if (nameStart >= 0 && nameEnd > nameStart) {
|
||||||
|
// Skip if a symbol with exactly this decl range already exists
|
||||||
|
val exists = symbols.any { it.declStart == nameStart && it.declEnd == nameEnd }
|
||||||
|
if (!exists) {
|
||||||
|
// Determine enclosing function body (if any) to attach as local; else treat as top-level
|
||||||
|
val inFn = functions.asSequence()
|
||||||
|
.filter { it.rangeEnd > it.rangeStart && nameStart >= it.rangeStart && nameStart <= it.rangeEnd }
|
||||||
|
.maxByOrNull { it.rangeEnd - it.rangeStart }
|
||||||
|
val kind = if (kw.equals("var", true)) SymbolKind.Var else SymbolKind.Val
|
||||||
|
if (inFn != null) {
|
||||||
|
val localSym = Symbol(nextId++, text.substring(nameStart, nameEnd), kind, nameStart, nameEnd, containerId = inFn.id)
|
||||||
|
symbols += localSym
|
||||||
|
inFn.locals += localSym.id
|
||||||
|
} else {
|
||||||
|
val localSym = Symbol(nextId++, text.substring(nameStart, nameEnd), kind, nameStart, nameEnd, containerId = null)
|
||||||
|
symbols += localSym
|
||||||
|
topLevelByName.getOrPut(localSym.name) { mutableListOf() }.add(localSym.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
iTok = j // continue from the name or comment
|
||||||
|
}
|
||||||
|
}
|
||||||
|
iTok++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Build name -> symbol ids index per function (locals+params) and per class (fields) for faster resolution
|
// Build name -> symbol ids index per function (locals+params) and per class (fields) for faster resolution
|
||||||
data class Idx(val byName: Map<String, List<Int>>)
|
data class Idx(val byName: Map<String, List<Int>>)
|
||||||
fun buildIndex(ids: List<Int>): Idx {
|
fun buildIndex(ids: List<Int>): Idx {
|
||||||
|
|||||||
182
lynglib/src/commonTest/kotlin/BindingHighlightTest.kt
Normal file
182
lynglib/src/commonTest/kotlin/BindingHighlightTest.kt
Normal file
@ -0,0 +1,182 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2025 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
|
||||||
|
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import net.sergeych.lyng.binding.Binder
|
||||||
|
import net.sergeych.lyng.binding.SymbolKind
|
||||||
|
import net.sergeych.lyng.miniast.MiniAstBuilder
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
import kotlin.test.assertNotNull
|
||||||
|
import kotlin.test.assertTrue
|
||||||
|
|
||||||
|
class BindingHighlightTest {
|
||||||
|
|
||||||
|
private suspend fun compileWithMini(code: String): Pair<Script, MiniAstBuilder> {
|
||||||
|
val sink = MiniAstBuilder()
|
||||||
|
val script = Compiler.compileWithMini(code.trimIndent(), sink)
|
||||||
|
return script to sink
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun binder_registers_top_level_var_and_binds_usages() = runTest {
|
||||||
|
val code = """
|
||||||
|
var counter = 0
|
||||||
|
counter = counter + 1
|
||||||
|
println(counter)
|
||||||
|
"""
|
||||||
|
|
||||||
|
val text = code.trimIndent()
|
||||||
|
val (_, sink) = compileWithMini(text)
|
||||||
|
val mini = sink.build()
|
||||||
|
assertNotNull(mini, "Mini-AST must be built")
|
||||||
|
|
||||||
|
val binding = Binder.bind(text, mini!!)
|
||||||
|
|
||||||
|
// Find the top-level symbol for counter and ensure it is mutable (Var)
|
||||||
|
val sym = binding.symbols.firstOrNull { it.name == "counter" }
|
||||||
|
assertNotNull(sym, "Top-level var 'counter' must be registered as a symbol")
|
||||||
|
assertEquals(SymbolKind.Var, sym.kind, "'counter' declared with var should be SymbolKind.Var")
|
||||||
|
|
||||||
|
// Declaration position
|
||||||
|
val declRange = sym.declStart to sym.declEnd
|
||||||
|
|
||||||
|
// Collect all references to counter (excluding the declaration itself)
|
||||||
|
val refs = binding.references.filter { it.symbolId == sym.id && (it.start to it.end) != declRange }
|
||||||
|
assertTrue(refs.isNotEmpty(), "Usages of top-level var 'counter' should be bound")
|
||||||
|
|
||||||
|
// Expect at least two usages: assignment LHS and println argument
|
||||||
|
assertTrue(refs.size >= 2, "Expected at least two usages of 'counter'")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun binder_registers_top_level_val_and_binds_usages() = runTest {
|
||||||
|
val code = """
|
||||||
|
val answer = 41
|
||||||
|
val next = answer + 1
|
||||||
|
println(answer)
|
||||||
|
"""
|
||||||
|
|
||||||
|
val text = code.trimIndent()
|
||||||
|
val (_, sink) = compileWithMini(text)
|
||||||
|
val mini = sink.build()
|
||||||
|
assertNotNull(mini, "Mini-AST must be built")
|
||||||
|
|
||||||
|
val binding = Binder.bind(text, mini!!)
|
||||||
|
|
||||||
|
val sym = binding.symbols.firstOrNull { it.name == "answer" }
|
||||||
|
assertNotNull(sym, "Top-level val 'answer' must be registered as a symbol")
|
||||||
|
assertEquals(SymbolKind.Val, sym.kind, "'answer' declared with val should be SymbolKind.Val")
|
||||||
|
|
||||||
|
val declRange = sym.declStart to sym.declEnd
|
||||||
|
val refs = binding.references.filter { it.symbolId == sym.id && (it.start to it.end) != declRange }
|
||||||
|
assertTrue(refs.isNotEmpty(), "Usages of top-level val 'answer' should be bound")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun binder_binds_locals_in_top_level_block_and_function_call() = runTest {
|
||||||
|
val code = """
|
||||||
|
fun test21() {
|
||||||
|
21
|
||||||
|
}
|
||||||
|
|
||||||
|
val format = "%" + "s"
|
||||||
|
|
||||||
|
for( f in files ) {
|
||||||
|
var name = f.name
|
||||||
|
if( f.isDirectory() )
|
||||||
|
println("is directory")
|
||||||
|
name += "/"
|
||||||
|
println( format(name, f.size()) )
|
||||||
|
}
|
||||||
|
|
||||||
|
test21()
|
||||||
|
"""
|
||||||
|
|
||||||
|
val text = code.trimIndent()
|
||||||
|
val (_, sink) = compileWithMini(text)
|
||||||
|
val mini = sink.build()
|
||||||
|
assertNotNull(mini, "Mini-AST must be built")
|
||||||
|
|
||||||
|
val binding = Binder.bind(text, mini!!)
|
||||||
|
|
||||||
|
// Ensure we registered the local var/val symbol for `name`
|
||||||
|
val nameSym = binding.symbols.firstOrNull { it.name == "name" }
|
||||||
|
assertNotNull(nameSym, "Local variable 'name' should be registered as a symbol")
|
||||||
|
assertEquals(SymbolKind.Var, nameSym.kind, "'name' is declared with var and must be SymbolKind.Var")
|
||||||
|
|
||||||
|
// Ensure there is at least one usage reference to `name` (not just the declaration)
|
||||||
|
val nameRefs = binding.references.filter { it.symbolId == nameSym.id }
|
||||||
|
println("[DEBUG_LOG] name decl at ${nameSym.declStart}..${nameSym.declEnd}")
|
||||||
|
println("[DEBUG_LOG] name refs: ${nameRefs.map { it.start to it.end }}")
|
||||||
|
assertTrue(nameRefs.isNotEmpty(), "Usages of 'name' should be bound to its declaration")
|
||||||
|
|
||||||
|
// We expect at least two usages of `name`: in "+=" and in the call argument.
|
||||||
|
assertTrue(nameRefs.size >= 2, "Binder should bind multiple usages of 'name'")
|
||||||
|
|
||||||
|
// Ensure function call at top-level is bound to the function symbol
|
||||||
|
val fnSym = binding.symbols.firstOrNull { it.name == "test21" && it.kind == SymbolKind.Function }
|
||||||
|
assertNotNull(fnSym, "Function 'test21' symbol must be present")
|
||||||
|
val callIdx = text.lastIndexOf("test21()")
|
||||||
|
assertTrue(callIdx > 0, "Test snippet must contain a 'test21()' call")
|
||||||
|
val callRef = binding.references.firstOrNull { it.symbolId == fnSym.id && it.start == callIdx && it.end == callIdx + "test21".length }
|
||||||
|
assertNotNull(callRef, "Binder should bind the top-level call 'test21()' to its declaration")
|
||||||
|
|
||||||
|
// Sanity: no references point exactly to the declaration range of test21
|
||||||
|
val declStart = fnSym.declStart
|
||||||
|
val declEnd = fnSym.declEnd
|
||||||
|
assertTrue(binding.references.none { it.start == declStart && it.end == declEnd }, "Declaration should not be duplicated as a reference")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun binder_binds_name_used_in_string_literal_invoke() = runTest {
|
||||||
|
val code = """
|
||||||
|
val format = "%" + "s"
|
||||||
|
|
||||||
|
for( f in files ) {
|
||||||
|
var name = f.name
|
||||||
|
if( f.isDirectory() )
|
||||||
|
println("%s is directory"(name))
|
||||||
|
name += "/"
|
||||||
|
println( format(name, f.size()) )
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
val text = code.trimIndent()
|
||||||
|
val (_, sink) = compileWithMini(text)
|
||||||
|
val mini = sink.build()
|
||||||
|
assertNotNull(mini, "Mini-AST must be built")
|
||||||
|
|
||||||
|
val binding = Binder.bind(text, mini!!)
|
||||||
|
|
||||||
|
val nameSym = binding.symbols.firstOrNull { it.name == "name" && (it.kind == SymbolKind.Var || it.kind == SymbolKind.Val) }
|
||||||
|
assertNotNull(nameSym, "Local variable 'name' should be registered as a symbol")
|
||||||
|
|
||||||
|
// Find the specific usage inside string-literal invocation: "%s is directory"(name)
|
||||||
|
val pattern = "\"%s is directory\"(name)"
|
||||||
|
val lineIdx = text.indexOf(pattern)
|
||||||
|
assertTrue(lineIdx >= 0, "Pattern with string invoke should be present in the snippet")
|
||||||
|
val nameStart = lineIdx + pattern.indexOf("name")
|
||||||
|
val nameEnd = nameStart + "name".length
|
||||||
|
|
||||||
|
val hasRefAtInvoke = binding.references.any { it.symbolId == nameSym.id && it.start == nameStart && it.end == nameEnd }
|
||||||
|
println("[DEBUG_LOG] refs for 'name': ${binding.references.filter { it.symbolId == nameSym.id }.map { it.start to it.end }}")
|
||||||
|
assertTrue(hasRefAtInvoke, "Binder should bind 'name' used as an argument to a string-literal invocation")
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -228,6 +228,20 @@
|
|||||||
|
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
|
<!-- IDE support dropdown -->
|
||||||
|
<li class="nav-item dropdown">
|
||||||
|
<a class="nav-link dropdown-toggle" href="#/docs/idea_plugin.md" role="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||||
|
IDE support
|
||||||
|
</a>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
<li>
|
||||||
|
<a class="dropdown-item" href="#/docs/textmate_bundle.md" data-route="docs">Textmate bundle</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a class="dropdown-item" href="#/docs/idea_plugin.md" data-route="docs">IDEA plugin</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" href="#/tryling" data-route="tryling">Try in browser</a>
|
<a class="nav-link" href="#/tryling" data-route="tryling">Try in browser</a>
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user