diff --git a/.gitignore b/.gitignore index 3b3cc4df..21088d35 100644 --- a/.gitignore +++ b/.gitignore @@ -9,7 +9,6 @@ release/ tmp/ # IDE and editor files -.vscode .idea # OS-specific files diff --git a/nls/.agents/skills/coding-guidelines/SKILL.md b/nls/.agents/skills/coding-guidelines/SKILL.md new file mode 100644 index 00000000..2e00014d --- /dev/null +++ b/nls/.agents/skills/coding-guidelines/SKILL.md @@ -0,0 +1,95 @@ +--- +name: coding-guidelines +description: "Use when asking about Rust code style or best practices. Keywords: naming, formatting, comment, clippy, rustfmt, lint, code style, best practice, P.NAM, G.FMT, code review, naming convention, variable naming, function naming, type naming, 命名规范, 代码风格, 格式化, 最佳实践, 代码审查, 怎么命名" +source: https://rust-coding-guidelines.github.io/rust-coding-guidelines-zh/ +user-invocable: false +--- + +# Rust Coding Guidelines (50 Core Rules) + +## Naming (Rust-Specific) + +| Rule | Guideline | +|------|-----------| +| No `get_` prefix | `fn name()` not `fn get_name()` | +| Iterator convention | `iter()` / `iter_mut()` / `into_iter()` | +| Conversion naming | `as_` (cheap &), `to_` (expensive), `into_` (ownership) | +| Static var prefix | `G_CONFIG` for `static`, no prefix for `const` | + +## Data Types + +| Rule | Guideline | +|------|-----------| +| Use newtypes | `struct Email(String)` for domain semantics | +| Prefer slice patterns | `if let [first, .., last] = slice` | +| Pre-allocate | `Vec::with_capacity()`, `String::with_capacity()` | +| Avoid Vec abuse | Use arrays for fixed sizes | + +## Strings + +| Rule | Guideline | +|------|-----------| +| Prefer bytes | `s.bytes()` over `s.chars()` when ASCII | +| Use `Cow` | When might modify borrowed data | +| Use `format!` | Over string concatenation with `+` | +| Avoid nested iteration | `contains()` on string is O(n*m) | + +## Error Handling + +| Rule | Guideline | +|------|-----------| +| Use `?` propagation | Not `try!()` macro | +| `expect()` over `unwrap()` | When value guaranteed | +| Assertions for invariants | `assert!` at function entry | + +## Memory + +| Rule | Guideline | +|------|-----------| +| Meaningful lifetimes | `'src`, `'ctx` not just `'a` | +| `try_borrow()` for RefCell | Avoid panic | +| Shadowing for transformation | `let x = x.parse()?` | + +## Concurrency + +| Rule | Guideline | +|------|-----------| +| Identify lock ordering | Prevent deadlocks | +| Atomics for primitives | Not Mutex for bool/usize | +| Choose memory order carefully | Relaxed/Acquire/Release/SeqCst | + +## Async + +| Rule | Guideline | +|------|-----------| +| Sync for CPU-bound | Async is for I/O | +| Don't hold locks across await | Use scoped guards | + +## Macros + +| Rule | Guideline | +|------|-----------| +| Avoid unless necessary | Prefer functions/generics | +| Follow Rust syntax | Macro input should look like Rust | + +## Deprecated → Better + +| Deprecated | Better | Since | +|------------|--------|-------| +| `lazy_static!` | `std::sync::OnceLock` | 1.70 | +| `once_cell::Lazy` | `std::sync::LazyLock` | 1.80 | +| `std::sync::mpsc` | `crossbeam::channel` | - | +| `std::sync::Mutex` | `parking_lot::Mutex` | - | +| `failure`/`error-chain` | `thiserror`/`anyhow` | - | +| `try!()` | `?` operator | 2018 | + +## Quick Reference + +``` +Naming: snake_case (fn/var), CamelCase (type), SCREAMING_CASE (const) +Format: rustfmt (just use it) +Docs: /// for public items, //! for module docs +Lint: #![warn(clippy::all)] +``` + +Claude knows Rust conventions well. These are the non-obvious Rust-specific rules. diff --git a/nls/.agents/skills/coding-guidelines/index/rules-index.md b/nls/.agents/skills/coding-guidelines/index/rules-index.md new file mode 100644 index 00000000..19c27b81 --- /dev/null +++ b/nls/.agents/skills/coding-guidelines/index/rules-index.md @@ -0,0 +1,6 @@ +# Complete Rules Reference + +For the full 500+ rules, see: +- Source: https://rust-coding-guidelines.github.io/rust-coding-guidelines-zh/ + +Core rules are in `../SKILL.md`. diff --git a/nls/.agents/skills/m01-ownership/SKILL.md b/nls/.agents/skills/m01-ownership/SKILL.md new file mode 100644 index 00000000..0cdcb1d0 --- /dev/null +++ b/nls/.agents/skills/m01-ownership/SKILL.md @@ -0,0 +1,134 @@ +--- +name: m01-ownership +description: "CRITICAL: Use for ownership/borrow/lifetime issues. Triggers: E0382, E0597, E0506, E0507, E0515, E0716, E0106, value moved, borrowed value does not live long enough, cannot move out of, use of moved value, ownership, borrow, lifetime, 'a, 'static, move, clone, Copy, 所有权, 借用, 生命周期" +user-invocable: false +--- + +# Ownership & Lifetimes + +> **Layer 1: Language Mechanics** + +## Core Question + +**Who should own this data, and for how long?** + +Before fixing ownership errors, understand the data's role: +- Is it shared or exclusive? +- Is it short-lived or long-lived? +- Is it transformed or just read? + +--- + +## Error → Design Question + +| Error | Don't Just Say | Ask Instead | +|-------|----------------|-------------| +| E0382 | "Clone it" | Who should own this data? | +| E0597 | "Extend lifetime" | Is the scope boundary correct? | +| E0506 | "End borrow first" | Should mutation happen elsewhere? | +| E0507 | "Clone before move" | Why are we moving from a reference? | +| E0515 | "Return owned" | Should caller own the data? | +| E0716 | "Bind to variable" | Why is this temporary? | +| E0106 | "Add 'a" | What is the actual lifetime relationship? | + +--- + +## Thinking Prompt + +Before fixing an ownership error, ask: + +1. **What is this data's domain role?** + - Entity (unique identity) → owned + - Value Object (interchangeable) → clone/copy OK + - Temporary (computation result) → maybe restructure + +2. **Is the ownership design intentional?** + - By design → work within constraints + - Accidental → consider redesign + +3. **Fix symptom or redesign?** + - If Strike 3 (3rd attempt) → escalate to Layer 2 + +--- + +## Trace Up ↑ + +When errors persist, trace to design layer: + +``` +E0382 (moved value) + ↑ Ask: What design choice led to this ownership pattern? + ↑ Check: m09-domain (is this Entity or Value Object?) + ↑ Check: domain-* (what constraints apply?) +``` + +| Persistent Error | Trace To | Question | +|-----------------|----------|----------| +| E0382 repeated | m02-resource | Should use Arc/Rc for sharing? | +| E0597 repeated | m09-domain | Is scope boundary at right place? | +| E0506/E0507 | m03-mutability | Should use interior mutability? | + +--- + +## Trace Down ↓ + +From design decisions to implementation: + +``` +"Data needs to be shared immutably" + ↓ Use: Arc (multi-thread) or Rc (single-thread) + +"Data needs exclusive ownership" + ↓ Use: move semantics, take ownership + +"Data is read-only view" + ↓ Use: &T (immutable borrow) +``` + +--- + +## Quick Reference + +| Pattern | Ownership | Cost | Use When | +|---------|-----------|------|----------| +| Move | Transfer | Zero | Caller doesn't need data | +| `&T` | Borrow | Zero | Read-only access | +| `&mut T` | Exclusive borrow | Zero | Need to modify | +| `clone()` | Duplicate | Alloc + copy | Actually need a copy | +| `Rc` | Shared (single) | Ref count | Single-thread sharing | +| `Arc` | Shared (multi) | Atomic ref count | Multi-thread sharing | +| `Cow` | Clone-on-write | Alloc if mutated | Might modify | + +## Error Code Reference + +| Error | Cause | Quick Fix | +|-------|-------|-----------| +| E0382 | Value moved | Clone, reference, or redesign ownership | +| E0597 | Reference outlives owner | Extend owner scope or restructure | +| E0506 | Assign while borrowed | End borrow before mutation | +| E0507 | Move out of borrowed | Clone or use reference | +| E0515 | Return local reference | Return owned value | +| E0716 | Temporary dropped | Bind to variable | +| E0106 | Missing lifetime | Add `'a` annotation | + +--- + +## Anti-Patterns + +| Anti-Pattern | Why Bad | Better | +|--------------|---------|--------| +| `.clone()` everywhere | Hides design issues | Design ownership properly | +| Fight borrow checker | Increases complexity | Work with the compiler | +| `'static` for everything | Restricts flexibility | Use appropriate lifetimes | +| Leak with `Box::leak` | Memory leak | Proper lifetime design | + +--- + +## Related Skills + +| When | See | +|------|-----| +| Need smart pointers | m02-resource | +| Need interior mutability | m03-mutability | +| Data is domain entity | m09-domain | +| Learning ownership concepts | m14-mental-model | diff --git a/nls/.agents/skills/m01-ownership/comparison.md b/nls/.agents/skills/m01-ownership/comparison.md new file mode 100644 index 00000000..209574b6 --- /dev/null +++ b/nls/.agents/skills/m01-ownership/comparison.md @@ -0,0 +1,222 @@ +# Ownership: Comparison with Other Languages + +## Rust vs C++ + +### Memory Management + +| Aspect | Rust | C++ | +|--------|------|-----| +| Default | Move semantics | Copy semantics (pre-C++11) | +| Move | `let b = a;` (a invalidated) | `auto b = std::move(a);` (a valid but unspecified) | +| Copy | `let b = a.clone();` | `auto b = a;` | +| Safety | Compile-time enforcement | Runtime responsibility | + +### Rust Move vs C++ Move + +```rust +// Rust: after move, 'a' is INVALID +let a = String::from("hello"); +let b = a; // a moved +// println!("{}", a); // COMPILE ERROR + +// Equivalent in C++: +// std::string a = "hello"; +// std::string b = std::move(a); +// std::cout << a; // UNDEFINED (compiles but buggy) +``` + +### Smart Pointers + +| Rust | C++ | Purpose | +|------|-----|---------| +| `Box` | `std::unique_ptr` | Unique ownership | +| `Rc` | `std::shared_ptr` | Shared ownership | +| `Arc` | `std::shared_ptr` + atomic | Thread-safe shared | +| `RefCell` | (manual runtime checks) | Interior mutability | + +--- + +## Rust vs Go + +### Memory Model + +| Aspect | Rust | Go | +|--------|------|-----| +| Memory | Stack + heap, explicit | GC manages all | +| Ownership | Enforced at compile-time | None (GC handles) | +| Null | `Option` | `nil` for pointers | +| Concurrency | `Send`/`Sync` traits | Channels (less strict) | + +### Sharing Data + +```rust +// Rust: explicit about sharing +use std::sync::Arc; +let data = Arc::new(vec![1, 2, 3]); +let data_clone = Arc::clone(&data); +std::thread::spawn(move || { + println!("{:?}", data_clone); +}); + +// Go: implicit sharing +// data := []int{1, 2, 3} +// go func() { +// fmt.Println(data) // potential race condition +// }() +``` + +### Why No GC in Rust + +1. **Deterministic destruction**: Resources freed exactly when scope ends +2. **Zero-cost**: No GC pauses or overhead +3. **Embeddable**: Works in OS kernels, embedded systems +4. **Predictable latency**: Critical for real-time systems + +--- + +## Rust vs Java/C# + +### Reference Semantics + +| Aspect | Rust | Java/C# | +|--------|------|---------| +| Objects | Owned by default | Reference by default | +| Null | `Option` | `null` (nullable) | +| Immutability | Default | Must use `final`/`readonly` | +| Copy | Explicit `.clone()` | Reference copy (shallow) | + +### Comparison + +```rust +// Rust: clear ownership +fn process(data: Vec) { // takes ownership + // data is ours, will be freed at end +} + +let numbers = vec![1, 2, 3]; +process(numbers); +// numbers is invalid here + +// Java: ambiguous ownership +// void process(List data) { +// // Who owns data? Caller? Callee? Both? +// // Can caller still use it? +// } +``` + +--- + +## Rust vs Python + +### Memory Model + +| Aspect | Rust | Python | +|--------|------|--------| +| Typing | Static, compile-time | Dynamic, runtime | +| Memory | Ownership-based | Reference counting + GC | +| Mutability | Default immutable | Default mutable | +| Performance | Native, zero-cost | Interpreted, higher overhead | + +### Common Pattern Translation + +```rust +// Rust: borrowing iteration +let items = vec!["a", "b", "c"]; +for item in &items { + println!("{}", item); +} +// items still usable + +// Python: iteration doesn't consume +// items = ["a", "b", "c"] +// for item in items: +// print(item) +// items still usable (different reason - ref counting) +``` + +--- + +## Unique Rust Concepts + +### Concepts Other Languages Lack + +1. **Borrow Checker**: No other mainstream language has compile-time borrow checking +2. **Lifetimes**: Explicit annotation of reference validity +3. **Move by Default**: Values move, not copy +4. **No Null**: `Option` instead of null pointers +5. **Affine Types**: Values can be used at most once + +### Learning Curve Areas + +| Concept | Coming From | Key Insight | +|---------|-------------|-------------| +| Ownership | GC languages | Think about who "owns" data | +| Borrowing | C/C++ | Like references but checked | +| Lifetimes | Any | Explicit scope of validity | +| Move | C++ | Move is default, not copy | + +--- + +## Mental Model Shifts + +### From GC Languages (Java, Go, Python) + +``` +Before: "Memory just works, GC handles it" +After: "I explicitly decide who owns data and when it's freed" +``` + +Key shifts: +- Think about ownership at design time +- Returning references requires lifetime thinking +- No more `null` - use `Option` + +### From C/C++ + +``` +Before: "I manually manage memory and hope I get it right" +After: "Compiler enforces correctness, I fight the borrow checker" +``` + +Key shifts: +- Trust the compiler's errors +- Move is the default (unlike C++ copy) +- Smart pointers are idiomatic, not overhead + +### From Functional Languages (Haskell, ML) + +``` +Before: "Everything is immutable, copying is fine" +After: "Mutability is explicit, ownership prevents aliasing" +``` + +Key shifts: +- Mutability is safe because of ownership rules +- No persistent data structures needed (usually) +- Performance characteristics are explicit + +--- + +## Performance Trade-offs + +| Language | Memory Overhead | Latency | Throughput | +|----------|-----------------|---------|------------| +| Rust | Minimal (no GC) | Predictable | Excellent | +| C++ | Minimal | Predictable | Excellent | +| Go | GC overhead | GC pauses | Good | +| Java | GC overhead | GC pauses | Good | +| Python | High (ref counting + GC) | Variable | Lower | + +### When Rust Ownership Wins + +1. **Real-time systems**: No GC pauses +2. **Embedded**: No runtime overhead +3. **High-performance**: Zero-cost abstractions +4. **Concurrent**: Data races prevented at compile time + +### When GC Might Be Preferable + +1. **Rapid prototyping**: Less mental overhead +2. **Complex object graphs**: Cycles are tricky in Rust +3. **GUI applications**: Object lifetimes are dynamic +4. **Small programs**: Overhead doesn't matter diff --git a/nls/.agents/skills/m01-ownership/examples/best-practices.md b/nls/.agents/skills/m01-ownership/examples/best-practices.md new file mode 100644 index 00000000..ccaf3dc7 --- /dev/null +++ b/nls/.agents/skills/m01-ownership/examples/best-practices.md @@ -0,0 +1,339 @@ +# Ownership Best Practices + +## API Design Patterns + +### 1. Prefer Borrowing Over Ownership + +```rust +// BAD: takes ownership unnecessarily +fn print_name(name: String) { + println!("Name: {}", name); +} + +// GOOD: borrows instead +fn print_name(name: &str) { + println!("Name: {}", name); +} + +// Caller benefits: +let name = String::from("Alice"); +print_name(&name); // can reuse name +print_name(&name); // still valid +``` + +### 2. Return Owned Values from Constructors + +```rust +// GOOD: return owned value +impl User { + fn new(name: &str) -> Self { + User { + name: name.to_string(), + } + } +} + +// GOOD: accept Into for flexibility +impl User { + fn new(name: impl Into) -> Self { + User { + name: name.into(), + } + } +} + +// Usage: +let u1 = User::new("Alice"); // &str +let u2 = User::new(String::from("Bob")); // String +``` + +### 3. Use AsRef for Generic Borrowing + +```rust +// GOOD: accepts both &str and String +fn process>(input: S) { + let s = input.as_ref(); + println!("{}", s); +} + +process("literal"); // &str +process(String::from("owned")); // String +process(&String::from("ref")); // &String +``` + +### 4. Cow for Clone-on-Write + +```rust +use std::borrow::Cow; + +// Return borrowed when possible, owned when needed +fn maybe_modify(s: &str, uppercase: bool) -> Cow<'_, str> { + if uppercase { + Cow::Owned(s.to_uppercase()) // allocates + } else { + Cow::Borrowed(s) // zero-cost + } +} + +let input = "hello"; +let result = maybe_modify(input, false); +// result is borrowed, no allocation +``` + +--- + +## Struct Design Patterns + +### 1. Owned Fields vs References + +```rust +// Use owned fields for most cases +struct User { + name: String, + email: String, +} + +// Use references only when lifetime is clear +struct UserView<'a> { + name: &'a str, + email: &'a str, +} + +// Pattern: owned data + view for efficiency +impl User { + fn view(&self) -> UserView<'_> { + UserView { + name: &self.name, + email: &self.email, + } + } +} +``` + +### 2. Builder Pattern with Ownership + +```rust +#[derive(Default)] +struct RequestBuilder { + url: Option, + method: Option, + body: Option>, +} + +impl RequestBuilder { + fn new() -> Self { + Self::default() + } + + // Take self by value for chaining + fn url(mut self, url: impl Into) -> Self { + self.url = Some(url.into()); + self + } + + fn method(mut self, method: impl Into) -> Self { + self.method = Some(method.into()); + self + } + + fn build(self) -> Result { + Ok(Request { + url: self.url.ok_or(Error::MissingUrl)?, + method: self.method.unwrap_or_else(|| "GET".to_string()), + body: self.body.unwrap_or_default(), + }) + } +} + +// Usage: +let req = RequestBuilder::new() + .url("https://example.com") + .method("POST") + .build()?; +``` + +### 3. Interior Mutability When Needed + +```rust +use std::cell::RefCell; +use std::rc::Rc; + +// Shared mutable state in single-threaded context +struct Counter { + value: Rc>, +} + +impl Counter { + fn new() -> Self { + Counter { + value: Rc::new(RefCell::new(0)), + } + } + + fn increment(&self) { + *self.value.borrow_mut() += 1; + } + + fn get(&self) -> u32 { + *self.value.borrow() + } + + fn clone_handle(&self) -> Self { + Counter { + value: Rc::clone(&self.value), + } + } +} +``` + +--- + +## Collection Patterns + +### 1. Efficient Iteration + +```rust +let items = vec![1, 2, 3, 4, 5]; + +// Iterate by reference (no move) +for item in &items { + println!("{}", item); +} + +// Iterate by mutable reference +for item in &mut items.clone() { + *item *= 2; +} + +// Consume with into_iter when done +let sum: i32 = items.into_iter().sum(); +``` + +### 2. Collecting Results + +```rust +// Collect into owned collection +let strings: Vec = (0..5) + .map(|i| format!("item_{}", i)) + .collect(); + +// Collect references +let refs: Vec<&str> = strings.iter().map(|s| s.as_str()).collect(); + +// Collect with transformation +let result: Result, _> = ["1", "2", "3"] + .iter() + .map(|s| s.parse::()) + .collect(); +``` + +### 3. Entry API for Maps + +```rust +use std::collections::HashMap; + +let mut map: HashMap> = HashMap::new(); + +// Efficient: don't search twice +map.entry("key".to_string()) + .or_insert_with(Vec::new) + .push(42); + +// With entry modification +map.entry("key".to_string()) + .and_modify(|v| v.push(43)) + .or_insert_with(|| vec![43]); +``` + +--- + +## Error Handling with Ownership + +### 1. Preserve Context in Errors + +```rust +use std::error::Error; +use std::fmt; + +#[derive(Debug)] +struct ParseError { + input: String, // owns the problematic input + message: String, +} + +impl fmt::Display for ParseError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "Failed to parse '{}': {}", self.input, self.message) + } +} + +fn parse(input: &str) -> Result { + input.parse().map_err(|_| ParseError { + input: input.to_string(), // clone for error context + message: "not a valid integer".to_string(), + }) +} +``` + +### 2. Ownership in Result Chains + +```rust +fn process_data(path: &str) -> Result { + let content = std::fs::read_to_string(path)?; // owned String + let parsed = parse_content(&content)?; // borrow + let processed = transform(parsed)?; // ownership moves + Ok(processed) // return owned +} +``` + +--- + +## Performance Considerations + +### 1. Avoid Unnecessary Clones + +```rust +// BAD: cloning just to compare +fn contains_item(items: &[String], target: &str) -> bool { + items.iter().any(|s| s.clone() == target) // unnecessary clone +} + +// GOOD: compare references +fn contains_item(items: &[String], target: &str) -> bool { + items.iter().any(|s| s == target) // String implements PartialEq +} +``` + +### 2. Use Slices for Flexibility + +```rust +// BAD: requires Vec +fn sum(numbers: &Vec) -> i32 { + numbers.iter().sum() +} + +// GOOD: accepts any slice +fn sum(numbers: &[i32]) -> i32 { + numbers.iter().sum() +} + +// Now works with: +sum(&vec![1, 2, 3]); // Vec +sum(&[1, 2, 3]); // array +sum(&array[1..3]); // slice +``` + +### 3. In-Place Mutation + +```rust +// BAD: allocates new String +fn make_uppercase(s: &str) -> String { + s.to_uppercase() +} + +// GOOD when you own the data: mutate in place +fn make_uppercase(mut s: String) -> String { + s.make_ascii_uppercase(); // in-place for ASCII + s +} +``` diff --git a/nls/.agents/skills/m01-ownership/patterns/common-errors.md b/nls/.agents/skills/m01-ownership/patterns/common-errors.md new file mode 100644 index 00000000..c3efcf40 --- /dev/null +++ b/nls/.agents/skills/m01-ownership/patterns/common-errors.md @@ -0,0 +1,265 @@ +# Common Ownership Errors & Fixes + +## E0382: Use of Moved Value + +### Error Pattern +```rust +let s = String::from("hello"); +let s2 = s; // s moved here +println!("{}", s); // ERROR: value borrowed after move +``` + +### Fix Options + +**Option 1: Clone (if ownership not needed)** +```rust +let s = String::from("hello"); +let s2 = s.clone(); // s is cloned +println!("{}", s); // OK: s still valid +``` + +**Option 2: Borrow (if modification not needed)** +```rust +let s = String::from("hello"); +let s2 = &s; // borrow, not move +println!("{}", s); // OK +println!("{}", s2); // OK +``` + +**Option 3: Use Rc/Arc (for shared ownership)** +```rust +use std::rc::Rc; +let s = Rc::new(String::from("hello")); +let s2 = Rc::clone(&s); // shared ownership +println!("{}", s); // OK +println!("{}", s2); // OK +``` + +--- + +## E0597: Borrowed Value Does Not Live Long Enough + +### Error Pattern +```rust +fn get_str() -> &str { + let s = String::from("hello"); + &s // ERROR: s dropped here, but reference returned +} +``` + +### Fix Options + +**Option 1: Return owned value** +```rust +fn get_str() -> String { + String::from("hello") // return owned value +} +``` + +**Option 2: Use 'static lifetime** +```rust +fn get_str() -> &'static str { + "hello" // string literal has 'static lifetime +} +``` + +**Option 3: Accept reference parameter** +```rust +fn get_str<'a>(s: &'a str) -> &'a str { + s // return reference with same lifetime as input +} +``` + +--- + +## E0499: Cannot Borrow as Mutable More Than Once + +### Error Pattern +```rust +let mut s = String::from("hello"); +let r1 = &mut s; +let r2 = &mut s; // ERROR: second mutable borrow +println!("{}, {}", r1, r2); +``` + +### Fix Options + +**Option 1: Sequential borrows** +```rust +let mut s = String::from("hello"); +{ + let r1 = &mut s; + r1.push_str(" world"); +} // r1 goes out of scope +let r2 = &mut s; // OK: r1 no longer exists +``` + +**Option 2: Use RefCell for interior mutability** +```rust +use std::cell::RefCell; +let s = RefCell::new(String::from("hello")); +let mut r1 = s.borrow_mut(); +// drop r1 before borrowing again +drop(r1); +let mut r2 = s.borrow_mut(); +``` + +--- + +## E0502: Cannot Borrow as Mutable While Immutable Borrow Exists + +### Error Pattern +```rust +let mut v = vec![1, 2, 3]; +let first = &v[0]; // immutable borrow +v.push(4); // ERROR: mutable borrow while immutable exists +println!("{}", first); +``` + +### Fix Options + +**Option 1: Finish using immutable borrow first** +```rust +let mut v = vec![1, 2, 3]; +let first = v[0]; // copy value, not borrow +v.push(4); // OK +println!("{}", first); // OK: using copied value +``` + +**Option 2: Clone before mutating** +```rust +let mut v = vec![1, 2, 3]; +let first = v[0].clone(); // if T: Clone +v.push(4); +println!("{}", first); +``` + +--- + +## E0507: Cannot Move Out of Borrowed Content + +### Error Pattern +```rust +fn take_string(s: &String) { + let moved = *s; // ERROR: cannot move out of borrowed content +} +``` + +### Fix Options + +**Option 1: Clone** +```rust +fn take_string(s: &String) { + let cloned = s.clone(); +} +``` + +**Option 2: Take ownership in function signature** +```rust +fn take_string(s: String) { // take ownership + let moved = s; +} +``` + +**Option 3: Use mem::take for Option/Default types** +```rust +fn take_from_option(opt: &mut Option) -> Option { + std::mem::take(opt) // replaces with None, returns owned value +} +``` + +--- + +## E0515: Return Local Reference + +### Error Pattern +```rust +fn create_string() -> &String { + let s = String::from("hello"); + &s // ERROR: cannot return reference to local variable +} +``` + +### Fix Options + +**Option 1: Return owned value** +```rust +fn create_string() -> String { + String::from("hello") +} +``` + +**Option 2: Use static/const** +```rust +fn get_static_str() -> &'static str { + "hello" +} +``` + +--- + +## E0716: Temporary Value Dropped While Borrowed + +### Error Pattern +```rust +let r: &str = &String::from("hello"); // ERROR: temporary dropped +println!("{}", r); +``` + +### Fix Options + +**Option 1: Bind to variable first** +```rust +let s = String::from("hello"); +let r: &str = &s; +println!("{}", r); +``` + +**Option 2: Use let binding with reference** +```rust +let r: &str = { + let s = String::from("hello"); + // s.as_str() // ERROR: still temporary + Box::leak(s.into_boxed_str()) // extreme: leak for 'static +}; +``` + +--- + +## Pattern: Loop Ownership Issues + +### Error Pattern +```rust +let strings = vec![String::from("a"), String::from("b")]; +for s in strings { + println!("{}", s); +} +// ERROR: strings moved into loop +println!("{:?}", strings); +``` + +### Fix Options + +**Option 1: Iterate by reference** +```rust +let strings = vec![String::from("a"), String::from("b")]; +for s in &strings { + println!("{}", s); +} +println!("{:?}", strings); // OK +``` + +**Option 2: Use iter()** +```rust +for s in strings.iter() { + println!("{}", s); +} +``` + +**Option 3: Clone if needed** +```rust +for s in strings.clone() { + // consumes cloned vec +} +println!("{:?}", strings); // original still available +``` diff --git a/nls/.agents/skills/m01-ownership/patterns/lifetime-patterns.md b/nls/.agents/skills/m01-ownership/patterns/lifetime-patterns.md new file mode 100644 index 00000000..19f76a86 --- /dev/null +++ b/nls/.agents/skills/m01-ownership/patterns/lifetime-patterns.md @@ -0,0 +1,229 @@ +# Lifetime Patterns + +## Basic Lifetime Annotation + +### When Required +```rust +// ERROR: missing lifetime specifier +fn longest(x: &str, y: &str) -> &str { + if x.len() > y.len() { x } else { y } +} + +// FIX: explicit lifetime +fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { + if x.len() > y.len() { x } else { y } +} +``` + +### Lifetime Elision Rules +1. Each input reference gets its own lifetime +2. If one input lifetime, output uses same +3. If `&self` or `&mut self`, output uses self's lifetime + +```rust +// These are equivalent (elision applies): +fn first_word(s: &str) -> &str { ... } +fn first_word<'a>(s: &'a str) -> &'a str { ... } + +// Method with self (elision applies): +impl MyStruct { + fn get_ref(&self) -> &str { ... } + // Equivalent to: + fn get_ref<'a>(&'a self) -> &'a str { ... } +} +``` + +--- + +## Struct Lifetimes + +### Struct Holding References +```rust +// Struct must declare lifetime for references +struct Excerpt<'a> { + part: &'a str, +} + +impl<'a> Excerpt<'a> { + fn level(&self) -> i32 { 3 } + + // Return reference tied to self's lifetime + fn get_part(&self) -> &str { + self.part + } +} +``` + +### Multiple Lifetimes in Struct +```rust +struct Multi<'a, 'b> { + x: &'a str, + y: &'b str, +} + +// Use when references may have different lifetimes +fn make_multi<'a, 'b>(x: &'a str, y: &'b str) -> Multi<'a, 'b> { + Multi { x, y } +} +``` + +--- + +## 'static Lifetime + +### When to Use +```rust +// String literals are 'static +let s: &'static str = "hello"; + +// Owned data can be leaked to 'static +let leaked: &'static str = Box::leak(String::from("hello").into_boxed_str()); + +// Thread spawn requires 'static or move +std::thread::spawn(move || { + // closure owns data, satisfies 'static +}); +``` + +### Avoid Overusing 'static +```rust +// BAD: requires 'static unnecessarily +fn process(s: &'static str) { ... } + +// GOOD: use generic lifetime +fn process<'a>(s: &'a str) { ... } +// or +fn process(s: &str) { ... } // lifetime elision +``` + +--- + +## Higher-Ranked Trait Bounds (HRTB) + +### for<'a> Syntax +```rust +// Function that works with any lifetime +fn apply_to_ref(f: F) +where + F: for<'a> Fn(&'a str) -> &'a str, +{ + let s = String::from("hello"); + let result = f(&s); + println!("{}", result); +} +``` + +### Common Use: Closure Bounds +```rust +// Closure that borrows any lifetime +fn filter_refs(items: &[&str], pred: F) -> Vec<&str> +where + F: for<'a> Fn(&'a str) -> bool, +{ + items.iter().copied().filter(|s| pred(s)).collect() +} +``` + +--- + +## Lifetime Bounds + +### 'a: 'b (Outlives) +```rust +// 'a must live at least as long as 'b +fn coerce<'a, 'b>(x: &'a str) -> &'b str +where + 'a: 'b, +{ + x +} +``` + +### T: 'a (Type Outlives Lifetime) +```rust +// T must live at least as long as 'a +struct Wrapper<'a, T: 'a> { + value: &'a T, +} + +// Common pattern with trait objects +fn use_trait<'a, T: MyTrait + 'a>(t: &'a T) { ... } +``` + +--- + +## Common Lifetime Mistakes + +### Mistake 1: Returning Reference to Local +```rust +// WRONG +fn dangle() -> &String { + let s = String::from("hello"); + &s // s dropped, reference invalid +} + +// RIGHT +fn no_dangle() -> String { + String::from("hello") +} +``` + +### Mistake 2: Conflicting Lifetimes +```rust +// WRONG: might return reference to y which has shorter lifetime +fn wrong<'a, 'b>(x: &'a str, y: &'b str) -> &'a str { + y // ERROR: 'b might not live as long as 'a +} + +// RIGHT: use same lifetime or add bound +fn right<'a>(x: &'a str, y: &'a str) -> &'a str { + y // OK: both have lifetime 'a +} +``` + +### Mistake 3: Struct Outlives Reference +```rust +// WRONG: s might outlive the string it references +let r; +{ + let s = String::from("hello"); + r = Excerpt { part: &s }; // ERROR +} +println!("{}", r.part); // s already dropped + +// RIGHT: ensure source outlives struct +let s = String::from("hello"); +let r = Excerpt { part: &s }; +println!("{}", r.part); // OK: s still in scope +``` + +--- + +## Subtyping and Variance + +### Covariance +```rust +// &'a T is covariant in 'a +// Can use &'long where &'short expected +fn example<'short, 'long: 'short>(long_ref: &'long str) { + let short_ref: &'short str = long_ref; // OK: covariance +} +``` + +### Invariance +```rust +// &'a mut T is invariant in 'a +fn example<'a, 'b>(x: &'a mut &'b str, y: &'b str) { + *x = y; // ERROR if 'a and 'b are different +} +``` + +### Practical Impact +```rust +// This works due to covariance +fn accept_any<'a>(s: &'a str) { ... } + +let s = String::from("hello"); +let long_lived: &str = &s; +accept_any(long_lived); // 'long coerces to 'short +``` diff --git a/nls/.agents/skills/m06-error-handling/SKILL.md b/nls/.agents/skills/m06-error-handling/SKILL.md new file mode 100644 index 00000000..38254884 --- /dev/null +++ b/nls/.agents/skills/m06-error-handling/SKILL.md @@ -0,0 +1,166 @@ +--- +name: m06-error-handling +description: "CRITICAL: Use for error handling. Triggers: Result, Option, Error, ?, unwrap, expect, panic, anyhow, thiserror, when to panic vs return Result, custom error, error propagation, 错误处理, Result 用法, 什么时候用 panic" +user-invocable: false +--- + +# Error Handling + +> **Layer 1: Language Mechanics** + +## Core Question + +**Is this failure expected or a bug?** + +Before choosing error handling strategy: +- Can this fail in normal operation? +- Who should handle this failure? +- What context does the caller need? + +--- + +## Error → Design Question + +| Pattern | Don't Just Say | Ask Instead | +|---------|----------------|-------------| +| unwrap panics | "Use ?" | Is None/Err actually possible here? | +| Type mismatch on ? | "Use anyhow" | Are error types designed correctly? | +| Lost error context | "Add .context()" | What does the caller need to know? | +| Too many error variants | "Use Box" | Is error granularity right? | + +--- + +## Thinking Prompt + +Before handling an error: + +1. **What kind of failure is this?** + - Expected → Result + - Absence normal → Option + - Bug/invariant → panic! + - Unrecoverable → panic! + +2. **Who handles this?** + - Caller → propagate with ? + - Current function → match/if-let + - User → friendly error message + - Programmer → panic with message + +3. **What context is needed?** + - Type of error → thiserror variants + - Call chain → anyhow::Context + - Debug info → anyhow or tracing + +--- + +## Trace Up ↑ + +When error strategy is unclear: + +``` +"Should I return Result or Option?" + ↑ Ask: Is absence/failure normal or exceptional? + ↑ Check: m09-domain (what does domain say?) + ↑ Check: domain-* (error handling requirements) +``` + +| Situation | Trace To | Question | +|-----------|----------|----------| +| Too many unwraps | m09-domain | Is the data model right? | +| Error context design | m13-domain-error | What recovery is needed? | +| Library vs app errors | m11-ecosystem | Who are the consumers? | + +--- + +## Trace Down ↓ + +From design to implementation: + +``` +"Expected failure, library code" + ↓ Use: thiserror for typed errors + +"Expected failure, application code" + ↓ Use: anyhow for ergonomic errors + +"Absence is normal (find, get, lookup)" + ↓ Use: Option + +"Bug or invariant violation" + ↓ Use: panic!, assert!, unreachable! + +"Need to propagate with context" + ↓ Use: .context("what was happening") +``` + +--- + +## Quick Reference + +| Pattern | When | Example | +|---------|------|---------| +| `Result` | Recoverable error | `fn read() -> Result` | +| `Option` | Absence is normal | `fn find() -> Option<&Item>` | +| `?` | Propagate error | `let data = file.read()?;` | +| `unwrap()` | Dev/test only | `config.get("key").unwrap()` | +| `expect()` | Invariant holds | `env.get("HOME").expect("HOME set")` | +| `panic!` | Unrecoverable | `panic!("critical failure")` | + +## Library vs Application + +| Context | Error Crate | Why | +|---------|-------------|-----| +| Library | `thiserror` | Typed errors for consumers | +| Application | `anyhow` | Ergonomic error handling | +| Mixed | Both | thiserror at boundaries, anyhow internally | + +## Decision Flowchart + +``` +Is failure expected? +├─ Yes → Is absence the only "failure"? +│ ├─ Yes → Option +│ └─ No → Result +│ ├─ Library → thiserror +│ └─ Application → anyhow +└─ No → Is it a bug? + ├─ Yes → panic!, assert! + └─ No → Consider if really unrecoverable + +Use ? → Need context? +├─ Yes → .context("message") +└─ No → Plain ? +``` + +--- + +## Common Errors + +| Error | Cause | Fix | +|-------|-------|-----| +| `unwrap()` panic | Unhandled None/Err | Use `?` or match | +| Type mismatch | Different error types | Use `anyhow` or `From` | +| Lost context | `?` without context | Add `.context()` | +| `cannot use ?` | Missing Result return | Return `Result<(), E>` | + +--- + +## Anti-Patterns + +| Anti-Pattern | Why Bad | Better | +|--------------|---------|--------| +| `.unwrap()` everywhere | Panics in production | `.expect("reason")` or `?` | +| Ignore errors silently | Bugs hidden | Handle or propagate | +| `panic!` for expected errors | Bad UX, no recovery | Result | +| Box everywhere | Lost type info | thiserror | + +--- + +## Related Skills + +| When | See | +|------|-----| +| Domain error strategy | m13-domain-error | +| Crate boundaries | m11-ecosystem | +| Type-safe errors | m05-type-driven | +| Mental models | m14-mental-model | diff --git a/nls/.agents/skills/m06-error-handling/examples/library-vs-app.md b/nls/.agents/skills/m06-error-handling/examples/library-vs-app.md new file mode 100644 index 00000000..7a7ef622 --- /dev/null +++ b/nls/.agents/skills/m06-error-handling/examples/library-vs-app.md @@ -0,0 +1,332 @@ +# Error Handling: Library vs Application + +## Library Error Design + +### Principles +1. **Define specific error types** - Don't use `anyhow` in libraries +2. **Implement std::error::Error** - For compatibility +3. **Provide error variants** - Let users match on errors +4. **Include source errors** - Enable error chains +5. **Be `Send + Sync`** - For async compatibility + +### Example: Library Error Type +```rust +// lib.rs +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum DatabaseError { + #[error("connection failed: {host}:{port}")] + ConnectionFailed { + host: String, + port: u16, + #[source] + source: std::io::Error, + }, + + #[error("query failed: {query}")] + QueryFailed { + query: String, + #[source] + source: SqlError, + }, + + #[error("record not found: {table}.{id}")] + NotFound { table: String, id: String }, + + #[error("constraint violation: {0}")] + ConstraintViolation(String), +} + +// Public Result alias +pub type Result = std::result::Result; + +// Library functions +pub fn connect(host: &str, port: u16) -> Result { + // ... +} + +pub fn query(conn: &Connection, sql: &str) -> Result { + // ... +} +``` + +### Library Usage of Errors +```rust +impl Database { + pub fn get_user(&self, id: &str) -> Result { + let rows = self.query(&format!("SELECT * FROM users WHERE id = '{}'", id))?; + + rows.first() + .cloned() + .ok_or_else(|| DatabaseError::NotFound { + table: "users".to_string(), + id: id.to_string(), + }) + } +} +``` + +--- + +## Application Error Design + +### Principles +1. **Use anyhow for convenience** - Or custom unified error +2. **Add context liberally** - Help debugging +3. **Log at boundaries** - Don't log in libraries +4. **Convert to user-friendly messages** - For display + +### Example: Application Error Handling +```rust +// main.rs +use anyhow::{Context, Result}; +use tracing::{error, info}; + +async fn run_server() -> Result<()> { + let config = load_config() + .context("failed to load configuration")?; + + let db = Database::connect(&config.db_url) + .await + .context("failed to connect to database")?; + + let server = Server::new(config.port) + .context("failed to create server")?; + + info!("Server starting on port {}", config.port); + + server.run(db).await + .context("server error")?; + + Ok(()) +} + +#[tokio::main] +async fn main() { + tracing_subscriber::init(); + + if let Err(e) = run_server().await { + error!("Application error: {:#}", e); + std::process::exit(1); + } +} +``` + +### Converting Library Errors +```rust +use mylib::DatabaseError; + +async fn get_user_handler(id: &str) -> Result { + match db.get_user(id).await { + Ok(user) => Ok(Response::json(user)), + + Err(DatabaseError::NotFound { .. }) => { + Ok(Response::not_found("User not found")) + } + + Err(DatabaseError::ConnectionFailed { .. }) => { + error!("Database connection failed"); + Ok(Response::internal_error("Service unavailable")) + } + + Err(e) => { + error!("Database error: {}", e); + Err(e.into()) // Convert to anyhow::Error + } + } +} +``` + +--- + +## Error Handling Layers + +``` +┌─────────────────────────────────────┐ +│ Application Layer │ +│ - Use anyhow or unified error │ +│ - Add context at boundaries │ +│ - Log errors │ +│ - Convert to user messages │ +└─────────────────────────────────────┘ + │ + │ calls + ▼ +┌─────────────────────────────────────┐ +│ Service Layer │ +│ - Map between error types │ +│ - Add business context │ +│ - Handle recoverable errors │ +└─────────────────────────────────────┘ + │ + │ calls + ▼ +┌─────────────────────────────────────┐ +│ Library Layer │ +│ - Define specific error types │ +│ - Use thiserror │ +│ - Include source errors │ +│ - No logging │ +└─────────────────────────────────────┘ +``` + +--- + +## Practical Examples + +### HTTP API Error Response +```rust +use axum::{response::IntoResponse, http::StatusCode}; +use serde::Serialize; + +#[derive(Serialize)] +struct ErrorResponse { + error: String, + code: String, +} + +enum AppError { + NotFound(String), + BadRequest(String), + Internal(anyhow::Error), +} + +impl IntoResponse for AppError { + fn into_response(self) -> axum::response::Response { + let (status, error, code) = match self { + AppError::NotFound(msg) => { + (StatusCode::NOT_FOUND, msg, "NOT_FOUND") + } + AppError::BadRequest(msg) => { + (StatusCode::BAD_REQUEST, msg, "BAD_REQUEST") + } + AppError::Internal(e) => { + tracing::error!("Internal error: {:#}", e); + ( + StatusCode::INTERNAL_SERVER_ERROR, + "Internal server error".to_string(), + "INTERNAL_ERROR", + ) + } + }; + + let body = ErrorResponse { + error, + code: code.to_string(), + }; + + (status, axum::Json(body)).into_response() + } +} +``` + +### CLI Error Handling +```rust +use anyhow::{Context, Result}; +use clap::Parser; + +#[derive(Parser)] +struct Args { + #[arg(short, long)] + config: String, +} + +fn main() { + if let Err(e) = run() { + eprintln!("Error: {:#}", e); + std::process::exit(1); + } +} + +fn run() -> Result<()> { + let args = Args::parse(); + + let config = std::fs::read_to_string(&args.config) + .context(format!("Failed to read config file: {}", args.config))?; + + let parsed: Config = toml::from_str(&config) + .context("Failed to parse config file")?; + + process(parsed)?; + + println!("Done!"); + Ok(()) +} +``` + +--- + +## Testing Error Handling + +### Testing Error Cases +```rust +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_not_found_error() { + let result = db.get_user("nonexistent"); + + assert!(matches!( + result, + Err(DatabaseError::NotFound { table, id }) + if table == "users" && id == "nonexistent" + )); + } + + #[test] + fn test_error_message() { + let err = DatabaseError::NotFound { + table: "users".to_string(), + id: "123".to_string(), + }; + + assert_eq!(err.to_string(), "record not found: users.123"); + } + + #[test] + fn test_error_chain() { + let io_err = std::io::Error::new( + std::io::ErrorKind::ConnectionRefused, + "connection refused" + ); + + let err = DatabaseError::ConnectionFailed { + host: "localhost".to_string(), + port: 5432, + source: io_err, + }; + + // Check source is preserved + assert!(err.source().is_some()); + } +} +``` + +### Testing with anyhow +```rust +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_with_context() -> anyhow::Result<()> { + let result = process("valid input")?; + assert_eq!(result, expected); + Ok(()) + } + + #[test] + fn test_error_context() { + let err = process("invalid") + .context("processing failed") + .unwrap_err(); + + // Check error chain contains expected text + let chain = format!("{:#}", err); + assert!(chain.contains("processing failed")); + } +} +``` diff --git a/nls/.agents/skills/m06-error-handling/patterns/error-patterns.md b/nls/.agents/skills/m06-error-handling/patterns/error-patterns.md new file mode 100644 index 00000000..d4d70c8c --- /dev/null +++ b/nls/.agents/skills/m06-error-handling/patterns/error-patterns.md @@ -0,0 +1,404 @@ +# Error Handling Patterns + +## The ? Operator + +### Basic Usage +```rust +fn read_config() -> Result { + let content = std::fs::read_to_string("config.toml")?; + let config: Config = toml::from_str(&content)?; // needs From impl + Ok(config) +} +``` + +### With Different Error Types +```rust +use std::error::Error; + +// Box for quick prototyping +fn process() -> Result<(), Box> { + let file = std::fs::read_to_string("data.txt")?; + let num: i32 = file.trim().parse()?; // different error type + Ok(()) +} +``` + +### Custom Conversion with From +```rust +#[derive(Debug)] +enum MyError { + Io(std::io::Error), + Parse(std::num::ParseIntError), +} + +impl From for MyError { + fn from(err: std::io::Error) -> Self { + MyError::Io(err) + } +} + +impl From for MyError { + fn from(err: std::num::ParseIntError) -> Self { + MyError::Parse(err) + } +} + +fn process() -> Result { + let content = std::fs::read_to_string("num.txt")?; // auto-converts + let num: i32 = content.trim().parse()?; // auto-converts + Ok(num) +} +``` + +--- + +## Error Type Design + +### Simple Enum Error +```rust +#[derive(Debug, Clone, PartialEq)] +pub enum ConfigError { + NotFound, + InvalidFormat, + MissingField(String), +} + +impl std::fmt::Display for ConfigError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ConfigError::NotFound => write!(f, "configuration file not found"), + ConfigError::InvalidFormat => write!(f, "invalid configuration format"), + ConfigError::MissingField(field) => write!(f, "missing field: {}", field), + } + } +} + +impl std::error::Error for ConfigError {} +``` + +### Error with Source (Wrapping) +```rust +#[derive(Debug)] +pub struct AppError { + kind: AppErrorKind, + source: Option>, +} + +#[derive(Debug, Clone, Copy)] +pub enum AppErrorKind { + Config, + Database, + Network, +} + +impl std::fmt::Display for AppError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self.kind { + AppErrorKind::Config => write!(f, "configuration error"), + AppErrorKind::Database => write!(f, "database error"), + AppErrorKind::Network => write!(f, "network error"), + } + } +} + +impl std::error::Error for AppError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + self.source.as_ref().map(|e| e.as_ref() as _) + } +} +``` + +--- + +## Using thiserror + +### Basic Usage +```rust +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum DataError { + #[error("file not found: {path}")] + NotFound { path: String }, + + #[error("invalid data format")] + InvalidFormat, + + #[error("IO error")] + Io(#[from] std::io::Error), + + #[error("parse error: {0}")] + Parse(#[from] std::num::ParseIntError), +} + +// Usage +fn load_data(path: &str) -> Result { + let content = std::fs::read_to_string(path) + .map_err(|_| DataError::NotFound { path: path.to_string() })?; + let num: i32 = content.trim().parse()?; // auto-converts with #[from] + Ok(Data { value: num }) +} +``` + +### Transparent Wrapper +```rust +use thiserror::Error; + +#[derive(Error, Debug)] +#[error(transparent)] +pub struct MyError(#[from] InnerError); + +// Useful for newtype error wrappers +``` + +--- + +## Using anyhow + +### For Applications +```rust +use anyhow::{Context, Result, bail, ensure}; + +fn process_file(path: &str) -> Result { + let content = std::fs::read_to_string(path) + .context("failed to read config file")?; + + ensure!(!content.is_empty(), "config file is empty"); + + let data: Data = serde_json::from_str(&content) + .context("failed to parse JSON")?; + + if data.version < 1 { + bail!("unsupported config version: {}", data.version); + } + + Ok(data) +} + +fn main() -> Result<()> { + let data = process_file("config.json") + .context("failed to load configuration")?; + Ok(()) +} +``` + +### Error Chain +```rust +use anyhow::{Context, Result}; + +fn deep_function() -> Result<()> { + std::fs::read_to_string("missing.txt") + .context("failed to read file")?; + Ok(()) +} + +fn middle_function() -> Result<()> { + deep_function() + .context("failed in deep function")?; + Ok(()) +} + +fn top_function() -> Result<()> { + middle_function() + .context("failed in middle function")?; + Ok(()) +} + +// Error output shows full chain: +// Error: failed in middle function +// Caused by: +// 0: failed in deep function +// 1: failed to read file +// 2: No such file or directory (os error 2) +``` + +--- + +## Option Handling + +### Converting Option to Result +```rust +fn find_user(id: u32) -> Option { ... } + +// Using ok_or for static error +fn get_user(id: u32) -> Result { + find_user(id).ok_or("user not found") +} + +// Using ok_or_else for dynamic error +fn get_user(id: u32) -> Result { + find_user(id).ok_or_else(|| format!("user {} not found", id)) +} +``` + +### Chaining Options +```rust +fn get_nested_value(data: &Data) -> Option<&str> { + data.config + .as_ref()? + .nested + .as_ref()? + .value + .as_deref() +} + +// Equivalent with and_then +fn get_nested_value(data: &Data) -> Option<&str> { + data.config + .as_ref() + .and_then(|c| c.nested.as_ref()) + .and_then(|n| n.value.as_deref()) +} +``` + +--- + +## Pattern: Result Combinators + +### map and map_err +```rust +fn parse_port(s: &str) -> Result { + s.parse::() + .map_err(|e| ParseError::InvalidPort(e)) +} + +fn get_url(config: &Config) -> Result { + config.url() + .map(|u| format!("https://{}", u)) +} +``` + +### and_then (flatMap) +```rust +fn validate_and_save(input: &str) -> Result<(), Error> { + validate(input) + .and_then(|valid| save(valid)) + .and_then(|saved| notify(saved)) +} +``` + +### unwrap_or and unwrap_or_else +```rust +// Default value +let port = config.port().unwrap_or(8080); + +// Computed default +let port = config.port().unwrap_or_else(|| find_free_port()); + +// Default for Result +let data = load_data().unwrap_or_default(); +``` + +--- + +## Pattern: Early Return vs Combinators + +### Early Return Style +```rust +fn process(input: &str) -> Result { + let step1 = validate(input)?; + if !step1.is_valid { + return Err(Error::Invalid); + } + + let step2 = transform(step1)?; + let step3 = save(step2)?; + + Ok(step3) +} +``` + +### Combinator Style +```rust +fn process(input: &str) -> Result { + validate(input) + .and_then(|s| { + if s.is_valid { + Ok(s) + } else { + Err(Error::Invalid) + } + }) + .and_then(transform) + .and_then(save) +} +``` + +### When to Use Which + +| Style | Best For | +|-------|----------| +| Early return (`?`) | Most cases, clearer flow | +| Combinators | Functional pipelines, one-liners | +| Match | Complex branching on errors | + +--- + +## Panic vs Result + +### When to Panic +```rust +// 1. Unrecoverable programmer error +fn get_config() -> &'static Config { + CONFIG.get().expect("config must be initialized") +} + +// 2. In tests +#[test] +fn test_parsing() { + let result = parse("valid").unwrap(); // OK in tests + assert_eq!(result, expected); +} + +// 3. Prototype/examples +fn main() { + let data = load().unwrap(); // OK for quick examples +} +``` + +### When to Return Result +```rust +// 1. Any I/O operation +fn read_file(path: &str) -> Result + +// 2. User input validation +fn parse_port(s: &str) -> Result + +// 3. Network operations +async fn fetch(url: &str) -> Result + +// 4. Anything that can fail at runtime +fn connect(addr: &str) -> Result +``` + +--- + +## Error Context Best Practices + +### Add Context at Boundaries +```rust +fn load_user_config(user_id: u64) -> Result { + let path = format!("/home/{}/config.toml", user_id); + + std::fs::read_to_string(&path) + .context(format!("failed to read config for user {}", user_id))? + // NOT: .context("failed to read file") // too generic + + // ... +} +``` + +### Include Relevant Data +```rust +// Good: includes the problematic value +fn parse_age(s: &str) -> Result { + s.parse() + .context(format!("invalid age value: '{}'", s)) +} + +// Bad: no context about what failed +fn parse_age(s: &str) -> Result { + s.parse() + .context("parse error") +} +``` diff --git a/nls/.gitignore b/nls/.gitignore index e52e65a5..211d45e1 100644 --- a/nls/.gitignore +++ b/nls/.gitignore @@ -1,9 +1,34 @@ -.idea/ -/target +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# Dependencies node_modules -out/ -.pnpm-debug.log -*.ast +.pnp +.pnp.js +.yarn + +# Local env files +.env +**.local + +# Build Outputs +target/ +build/ dist/ +out/ +*.tsbuildinfo + +# IDE/editor +.idea/ +.history/ + +# Package manager/debug logs +.pnpm-debug.log* +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# VS Code extension package *.vsix -.history + +# Project artifacts +*.ast diff --git a/nls/.vscode/launch.json b/nls/.vscode/launch.json new file mode 100644 index 00000000..d635143a --- /dev/null +++ b/nls/.vscode/launch.json @@ -0,0 +1,23 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "type": "extensionHost", + "request": "launch", + "name": "Launch NLS Extension", + "runtimeExecutable": "${execPath}", + "args": [ + "--extensionDevelopmentPath=${workspaceFolder}" + ], + "outFiles": [ + "${workspaceFolder}/dist/**/*.js" + ], + "env": { + "SERVER_PATH": "${workspaceFolder}/target/debug/nls", + "RUST_LOG": "debug", + "RUST_BACKTRACE": "1" + }, + "preLaunchTask": "build-nls" + } + ] +} \ No newline at end of file diff --git a/nls/.vscode/tasks.json b/nls/.vscode/tasks.json new file mode 100644 index 00000000..5a4b20b1 --- /dev/null +++ b/nls/.vscode/tasks.json @@ -0,0 +1,17 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "build-nls", + "type": "shell", + "command": "cargo build && pnpm run build", + "group": "build", + "problemMatcher": [ + "$rustc" + ], + "options": { + "cwd": "${workspaceFolder}" + } + } + ] +} \ No newline at end of file diff --git a/nls/README.md b/nls/README.md index 080c684c..f07ea8bc 100644 --- a/nls/README.md +++ b/nls/README.md @@ -1,47 +1,176 @@ -# NLS - Nature Language Server - -This is the LSP (Language Server Protocol) server implementation for the [nature](https://github.com/nature-lang/nature) programming language, which includes both the server (nls) and VSCode client components. +# NLS — Nature Language Server +LSP server and VSCode client for the [Nature](https://github.com/nature-lang/nature) programming language. ## Features -The current version corresponds to nature version 0.5.x and implements: - +- Code completion (type members, imports, selective imports, auto-import, keywords) +- Hover (symbol info, doc comments, type display for ref/ptr wrappers) +- Go-to definition & find references - Semantic highlighting -- Syntax checking -- Type checking -- Package dependency management -- Multi-workspace support +- Signature help +- Inlay hints (parameter names, inferred types) +- Code actions (import sorting) +- Document & workspace symbols +- Syntax and type checking with diagnostics +- Multi-workspace and package dependency support - Platform-specific module resolution -Code completion features are planned for future releases. - +## Project Structure + +``` +nls/ +├── src/ +│ ├── main.rs # Entry point — starts the LSP server via tower-lsp +│ ├── lib.rs # Library root, declares top-level modules +│ │ +│ ├── analyzer.rs # Analyzer orchestrator (build pipeline, module loading) +│ ├── analyzer/ +│ │ ├── common.rs # Shared AST types (Expr, Stmt, Type, AstNode, …) +│ │ ├── lexer.rs # Lexer / tokeniser, semantic token definitions +│ │ ├── syntax.rs # Parser (tokens → AST) +│ │ ├── symbol.rs # Symbol table, scopes, symbol kinds +│ │ ├── typesys.rs # Type-system helpers +│ │ ├── flow.rs # Control-flow analysis +│ │ ├── generics.rs # Generics resolution +│ │ ├── global_eval.rs # Global-level constant evaluation +│ │ ├── workspace_index.rs # Cross-workspace symbol indexing +│ │ │ +│ │ ├── completion/ # Code-completion provider +│ │ │ ├── mod.rs # Types, dispatcher, shared helpers +│ │ │ ├── members.rs # Type/struct member & variable completions +│ │ │ ├── imports.rs # Import, module, workspace & auto-import completions +│ │ │ └── context.rs # Cursor-context extraction + unit tests +│ │ │ +│ │ └── semantic/ # Semantic analysis +│ │ ├── mod.rs # Semantic struct, type analysis, analyze() entry +│ │ ├── expressions.rs # Expression/statement analysis, constant folding +│ │ └── declarations.rs # Function declarations, type inference, var decls +│ │ +│ ├── server/ +│ │ ├── mod.rs # Backend struct, LSP trait impl, request routing +│ │ ├── dispatch.rs # didOpen / didChange / didSave handlers, debounce +│ │ ├── capabilities.rs # ServerCapabilities construction +│ │ ├── config.rs # Client configuration helpers +│ │ ├── completion.rs # LSP completion request handler +│ │ ├── hover.rs # Hover handler, type display +│ │ ├── navigation.rs # Go-to definition, references, cursor context +│ │ ├── semantic_tokens.rs # Semantic tokens encoding +│ │ ├── signature_help.rs # Signature help handler +│ │ ├── inlay_hints.rs # Inlay-hint handler +│ │ ├── code_actions.rs # Code actions (import sorting) +│ │ └── symbols.rs # Document / workspace symbols +│ │ +│ ├── document.rs # Thread-safe document store (Rope-backed) +│ ├── project.rs # Workspace / project state, Module struct +│ ├── package.rs # package.toml parsing +│ └── utils.rs # Position/offset conversion, identifier helpers +│ +├── tests/ # Integration tests +│ ├── analyzer_test.rs # Analyzer pipeline tests +│ ├── build_pipeline_test.rs +│ ├── global_eval_test.rs +│ └── server_test.rs # LSP server integration tests +│ +├── client/ # VSCode extension client (TypeScript) +├── syntaxes/ # TextMate grammar for syntax highlighting +└── assets/ # Extension icons / assets +``` + +### Key architectural concepts + +- **tower-lsp** — The server is built on `tower-lsp`. `Backend` (in `server/mod.rs`) implements the `LanguageServer` trait. +- **DocumentStore** — In-memory Rope store keyed by URI. Updated incrementally on `didChange`. +- **Project / Module** — Each workspace folder becomes a `Project` (stored in `DashMap`). A project contains multiple `Module`s, each with its own AST, scope, and semantic tokens. +- **Analysis pipeline** — `analyzer.rs` drives: lex → parse → global symbol registration → semantic analysis (per-module). Shared state lives in `SymbolTable` (protected by `Mutex`). +- **Lock ordering** — When both are needed, always lock `module_db` before `symbol_table` to avoid deadlocks. ## Building -This project is implemented in Rust. To build the project: +```bash +# Debug build +cd nls +cargo build -bash +# Release build cargo build --release +``` -This will generate the `nls` executable. +The binary is output to `target/{debug,release}/nls`. -## Installation +## Testing -### VSCode Extension +```bash +# Unit tests (105 tests across all modules) +cargo test --lib + +# Integration tests +cargo test --test analyzer_test +cargo test --test server_test +cargo test --test build_pipeline_test +cargo test --test global_eval_test + +# All tests +cargo test + +# Run a specific test by name +cargo test --lib completion::context::tests::detects_basic_struct_init + +# With output (for debugging) +cargo test --lib -- --nocapture +``` + +## Debugging + +### VSCode extension host + +A launch configuration is provided in `.vscode/launch.json`. It: -The LSP client can be downloaded from the VSCode extension marketplace by searching for "nature language". +1. Builds the NLS debug binary (`preLaunchTask: build-nls`) +2. Launches a new VSCode Extension Host window +3. Points `SERVER_PATH` to `target/debug/nls` +4. Enables `RUST_LOG=debug` and `RUST_BACKTRACE=1` -### Server Installation +To use it: open the `nls/` folder in VSCode, then **Run → Start Debugging** (or F5). + +### Logging + +NLS uses `env_logger`. Set the `RUST_LOG` environment variable: + +```bash +# See all NLS debug logs +RUST_LOG=debug ./target/debug/nls + +# Filter to specific modules +RUST_LOG=nls::server=debug,nls::analyzer=info ./target/debug/nls +``` + +When running via the VSCode extension host, logs appear in the **Output** panel under the "Nature Language Server" channel. + +### Manual testing against a Nature project + +```bash +# 1. Build NLS +cargo build + +# 2. Point VSCode to your local build +# Set SERVER_PATH in your shell before launching VSCode, +# or use the launch.json configuration above. +export SERVER_PATH="$(pwd)/target/debug/nls" +``` + +## Installation + +### VSCode Extension -The nls LSP server needs to be installed in `/usr/local/nature/bin`. The VSCode client will automatically detect and launch the server from this path. +Search for **"nature language"** in the VSCode extension marketplace. -Note: The nls executable is also bundled in the nature-lang release package. If you have already installed nature-lang in `/usr/local/nature`, no additional server installation is required. +### Server -### Debugging +The `nls` binary should be placed at `/usr/local/nature/bin/nls`. The VSCode client automatically detects and launches it from that path. -Launch configurations are provided for VSCode in `.vscode/launch.json` for debugging both the client and server components. +> The nls binary is also bundled in the nature-lang release package. If you already have nature-lang installed at `/usr/local/nature`, no extra installation step is needed. ## License -MIT \ No newline at end of file +MIT / Apache-2.0 \ No newline at end of file diff --git a/nls/SETTINGS.md b/nls/SETTINGS.md new file mode 100644 index 00000000..27311016 --- /dev/null +++ b/nls/SETTINGS.md @@ -0,0 +1,33 @@ +# NLS Settings + +All settings live under the `nature` namespace in your VS Code `settings.json`. + +## Inlay Hints + +| Setting | Type | Default | Description | +|---|---|---|---| +| `nature.inlayHints.typeHints` | `boolean` | `false` | Show inferred types after variable declarations (e.g. `var x = 42` shows `: i64`). | +| `nature.inlayHints.parameterHints` | `boolean` | `false` | Show parameter names at call sites (e.g. `greet("world")` shows `name:`). | + +## Analysis + +| Setting | Type | Default | Description | +|---|---|---|---| +| `nature.analysis.debounceMs` | `number` | `50` | Milliseconds to wait after an edit before re-analysing the file. Increase on slower machines to reduce CPU usage. | + +## Diagnostics + +| Setting | Type | Default | Description | +|---|---|---|---| +| `nls.trace.server` | `"off"` \| `"messages"` \| `"verbose"` | `"off"` | Traces communication between VS Code and the language server. Useful for debugging. | + +## Example + +```jsonc +// .vscode/settings.json +{ + "nature.inlayHints.typeHints": true, + "nature.inlayHints.parameterHints": true, + "nature.analysis.debounceMs": 100 +} +``` diff --git a/nls/TODO.md b/nls/TODO.md new file mode 100644 index 00000000..841c810e --- /dev/null +++ b/nls/TODO.md @@ -0,0 +1,17 @@ +# NLS TODO + +## Semantic Coloring + +- [ ] **Semantic token modifiers** — Add modifiers: `declaration`, `readonly`, `deprecated`, `static`, `defaultLibrary` +- [ ] **Unused symbol detection + dimming** — Track read counts per symbol in the symbol table; report unused imports/variables/functions/constants/types as `HINT` + `UNNECESSARY` diagnostics so the editor greys them out. Needs symbol table changes. + +## Refactoring + +- [ ] **Rename** — Rename a symbol across the entire project (with prepare-rename validation) +- [ ] **Import alias resolution** — Fix function aliasing in imports (e.g. `import 'foo.n'.{add as sum}`) so alias + symbol kinds resolve and color correctly + +## Advanced + +- [ ] **Call hierarchy** — Incoming/outgoing call trees +- [ ] **Formatting** — Format `.n` files +- [ ] **Code lens** — Show reference counts, test run buttons diff --git a/nls/package.json b/nls/package.json index a697789c..5d2e9798 100644 --- a/nls/package.json +++ b/nls/package.json @@ -30,7 +30,7 @@ "extensions": [ ".n" ], - "configuration": "./language-configuration.json", + "configuration": "./syntaxes/language-configuration.json", "icon": { "dark": "./assets/nature-lang-icon-alternative-1.svg", "light": "./assets/nature-lang-icon-alternative-1.svg" @@ -46,6 +46,13 @@ ] } ], + "grammars": [ + { + "language": "n", + "scopeName": "source.n", + "path": "./syntaxes/n.tmLanguage.json" + } + ], "configuration": { "type": "object", "title": "nls", diff --git a/nls/skills-lock.json b/nls/skills-lock.json new file mode 100644 index 00000000..3fde42bc --- /dev/null +++ b/nls/skills-lock.json @@ -0,0 +1,55 @@ +{ + "version": 1, + "skills": { + "coding-guidelines": { + "source": "ZhangHanDong/rust-skills", + "sourceType": "github", + "computedHash": "0742312a0de5eb6bd84e093c1815b1db98126130b92713ea5fc2cf902338aa3b" + }, + "m01-ownership": { + "source": "ZhangHanDong/rust-skills", + "sourceType": "github", + "computedHash": "69f57ede558e32e703e468ef5328f5bcc6610adff9d99b49984a5b3507d31b5b" + }, + "m02-resource": { + "source": "ZhangHanDong/rust-skills", + "sourceType": "github", + "computedHash": "b79e4a7702adb7c7622dfab7818dd81710352d448d285885946652e5dc73fd5b" + }, + "m03-mutability": { + "source": "ZhangHanDong/rust-skills", + "sourceType": "github", + "computedHash": "78eb8bc5e8b9bbeb5a59375b74b773c8fe824bb1644fa0fb77e99a1255a272ab" + }, + "m04-zero-cost": { + "source": "ZhangHanDong/rust-skills", + "sourceType": "github", + "computedHash": "8bc6733618b217b608fe5011f4c74b7ceb8c0765ba60702e30fa8fa4824584f2" + }, + "m05-type-driven": { + "source": "ZhangHanDong/rust-skills", + "sourceType": "github", + "computedHash": "06a52b0f35951cd01e21b2906917061f3994ece6b8c6b055dca65bbe14820283" + }, + "m06-error-handling": { + "source": "ZhangHanDong/rust-skills", + "sourceType": "github", + "computedHash": "e6639bd5913e63bd8315f7b36f5dd22a029cae2404135a75ac24c0627a93e4e5" + }, + "m11-ecosystem": { + "source": "ZhangHanDong/rust-skills", + "sourceType": "github", + "computedHash": "7ac5dd7206c339379d0aafe304a473a2e54465374be269ffc97d9e615325fc69" + }, + "rust-call-graph": { + "source": "ZhangHanDong/rust-skills", + "sourceType": "github", + "computedHash": "0999fbe783c81b87ba1dc1347758bd87b5880dca0d66c69be53bbac133a4c888" + }, + "rust-code-navigator": { + "source": "ZhangHanDong/rust-skills", + "sourceType": "github", + "computedHash": "c5c5b9521be30a3b5a3d6fa122bf9df809af3b28760e06fa317d6afab5bb3939" + } + } +} diff --git a/nls/src/analyzer.rs b/nls/src/analyzer.rs index 4d547d86..59b6acb2 100644 --- a/nls/src/analyzer.rs +++ b/nls/src/analyzer.rs @@ -8,6 +8,7 @@ pub mod semantic; pub mod symbol; pub mod syntax; pub mod typesys; +pub mod workspace_index; use std::path::Path; @@ -131,7 +132,8 @@ fn analyze_import_dep(package_config: &PackageConfig, _m: &mut Module, import: & start: import.start, end: import.end, message: format!("{} not found", package_ident), - }); + is_warning: false, + }); } }; @@ -142,7 +144,8 @@ fn analyze_import_dep(package_config: &PackageConfig, _m: &mut Module, import: & start: import.start, end: import.end, message: format!("{} not found", package_conf_path.display()), - }); + is_warning: false, + }); } match parse_package(package_conf_path.to_str().unwrap()) { @@ -158,7 +161,8 @@ fn analyze_import_dep(package_config: &PackageConfig, _m: &mut Module, import: & start: import.start, end: import.end, message: format!("import failed: {} {}", package_conf_path.display(), e.message), - }) + is_warning: false, + }) } } } @@ -178,7 +182,8 @@ fn analyze_import_std(_m: &mut Module, import: &mut ImportStmt) -> Result<(), An start: import.start, end: import.end, message: format!("{} not found", package_conf_path.display()), - }); + is_warning: false, + }); } match parse_package(package_conf_path.to_str().unwrap()) { @@ -194,7 +199,8 @@ fn analyze_import_std(_m: &mut Module, import: &mut ImportStmt) -> Result<(), An start: import.start, end: import.end, message: format!("import package failed: {} parse err {}", package_conf_path.display(), e.message), - }); + is_warning: false, + }); } } } @@ -289,7 +295,8 @@ pub fn analyze_import( start: import.start, end: import.end, message: format!("import file cannot start with . or /"), - }); + is_warning: false, + }); } import.full_path = Path::new(&m.dir).join(file).to_string_lossy().into_owned(); @@ -298,7 +305,8 @@ pub fn analyze_import( start: import.start, end: import.end, message: format!("import file suffix must .n"), - }); + is_warning: false, + }); } // check file exist @@ -307,7 +315,8 @@ pub fn analyze_import( start: import.start, end: import.end, message: format!("import file {} not found", file.clone()), - }); + is_warning: false, + }); } // 如果 import as empty, 则使用 import 的 file 的文件名称去除后缀作为 import as @@ -346,7 +355,8 @@ pub fn analyze_import( start: import.start, end: import.end, message: format!("package '{}' not found", package_ident), - }); + is_warning: false, + }); } } else { if is_std_package(&package_ident) { @@ -357,7 +367,8 @@ pub fn analyze_import( start: import.start, end: import.end, message: format!("package '{}' not found", package_ident), - }); + is_warning: false, + }); } } @@ -372,7 +383,8 @@ pub fn analyze_import( start: import.start, end: import.end, message: format!("cannot import '{}': file not found", import.full_path.clone()), - }); + is_warning: false, + }); } // check file is n file @@ -381,7 +393,8 @@ pub fn analyze_import( start: import.start, end: import.end, message: format!("import file suffix must .n"), - }); + is_warning: false, + }); } } Err(e) => { @@ -389,12 +402,15 @@ pub fn analyze_import( start: import.start, end: import.end, message: e, - }); + is_warning: false, + }); } } // calc import as, 如果不存在 import as, 则使用 ast_package 的最后一个元素作为 import as - if import.as_name.is_empty() { + // For selective imports, leave as_name empty — the symbols are imported directly, + // not via a module alias, so the module should not shadow local variables in completion. + if import.as_name.is_empty() && !import.is_selective { import.as_name = import.ast_package.as_ref().unwrap().last().unwrap().clone(); } @@ -456,7 +472,8 @@ pub fn register_global_symbol(m: &mut Module, symbol_table: &mut SymbolTable, st start: var_decl.symbol_start, end: var_decl.symbol_end, message: e, - }, + is_warning: false, + }, ); } } @@ -489,10 +506,19 @@ pub fn register_global_symbol(m: &mut Module, symbol_table: &mut SymbolTable, st start: constdef.symbol_start, end: constdef.symbol_end, message: e, - }, + is_warning: false, + }, ); } } + + // Register in global symbol table (needed for selective imports) + let _ = symbol_table.define_global_symbol( + constdef.ident.clone(), + SymbolKind::Const(constdef_mutex.clone()), + constdef.symbol_start, + m.scope_id, + ); } AstNode::Typedef(typedef_mutex) => { let mut typedef = typedef_mutex.lock().unwrap(); @@ -510,7 +536,8 @@ pub fn register_global_symbol(m: &mut Module, symbol_table: &mut SymbolTable, st start: typedef.symbol_start, end: typedef.symbol_end, message: e, - }, + is_warning: false, + }, ); } } @@ -535,7 +562,8 @@ pub fn register_global_symbol(m: &mut Module, symbol_table: &mut SymbolTable, st start: fndef.symbol_start, end: fndef.symbol_end, message: format!("ident '{}' redeclared", fndef.symbol_name), - }, + is_warning: false, + }, ); } } diff --git a/nls/src/analyzer/common.rs b/nls/src/analyzer/common.rs index 67c0bc6b..90ac3e9c 100644 --- a/nls/src/analyzer/common.rs +++ b/nls/src/analyzer/common.rs @@ -53,6 +53,16 @@ pub struct AnalyzerError { pub start: usize, pub end: usize, pub message: String, + pub is_warning: bool, // If true, rendered as hint with faded/unnecessary styling +} + +impl AnalyzerError { + pub fn new(start: usize, end: usize, message: String) -> Self { + Self { start, end, message, is_warning: false } + } + pub fn warning(start: usize, end: usize, message: String) -> Self { + Self { start, end, message, is_warning: true } + } } #[derive(Debug, Clone)] @@ -422,7 +432,6 @@ impl Type { | TypeKind::String | TypeKind::Set(..) | TypeKind::Map(..) - | TypeKind::Tuple(..) | TypeKind::Union(..) | TypeKind::TaggedUnion(..) ) @@ -1111,6 +1120,8 @@ pub struct TupleDestrExpr { pub struct ImportSelectItem { pub ident: String, pub alias: Option, + pub start: usize, + pub end: usize, } #[derive(Debug, Clone)] diff --git a/nls/src/analyzer/completion.rs b/nls/src/analyzer/completion.rs deleted file mode 100644 index a31c716d..00000000 --- a/nls/src/analyzer/completion.rs +++ /dev/null @@ -1,1059 +0,0 @@ -use crate::analyzer::common::AstFnDef; -use crate::analyzer::symbol::{NodeId, Symbol, SymbolKind, SymbolTable}; -use crate::project::Module; -use log::debug; -use std::collections::HashSet; - -#[derive(Debug, Clone)] -pub struct CompletionItem { - pub label: String, // Variable name - pub kind: CompletionItemKind, - pub detail: Option, // Type information - pub documentation: Option, - pub insert_text: String, // Insert text - pub sort_text: Option, // Sort priority - pub additional_text_edits: Vec, // Additional text edits (for auto-import) -} - -#[derive(Debug, Clone)] -pub struct TextEdit { - pub line: usize, - pub character: usize, - pub new_text: String, -} - -#[derive(Debug, Clone)] -pub enum CompletionItemKind { - Variable, - Parameter, - Function, - Constant, - Module, // Module type for imports - Struct, // Type definitions (structs, typedefs) -} - -pub struct CompletionProvider<'a> { - symbol_table: &'a mut SymbolTable, - module: &'a mut Module, - nature_root: String, - project_root: String, - package_config: Option, -} - -impl<'a> CompletionProvider<'a> { - pub fn new( - symbol_table: &'a mut SymbolTable, - module: &'a mut Module, - nature_root: String, - project_root: String, - package_config: Option, - ) -> Self { - Self { - symbol_table, - module, - nature_root, - project_root, - package_config, - } - } - - /// Main auto-completion entry function - pub fn get_completions(&self, position: usize, text: &str) -> Vec { - dbg!("get_completions", position, &self.module.ident, self.module.scope_id); - - // Check if in struct initialization context (type_name{ field: ... }) - if let Some((type_name, field_prefix)) = extract_struct_init_context(text, position) { - debug!("Detected struct initialization context: type='{}', field_prefix='{}'", type_name, field_prefix); - let current_scope_id = self.find_innermost_scope(self.module.scope_id, position); - return self.get_struct_field_completions(&type_name, &field_prefix, current_scope_id); - } - - let prefix = extract_prefix_at_position(text, position); - - // Check if in type member access context (variable.field) - if let Some((var_name, member_prefix)) = extract_module_member_context(&prefix, position) { - // First try as variable type member access - let current_scope_id = self.find_innermost_scope(self.module.scope_id, position); - if let Some(completions) = self.get_type_member_completions(&var_name, &member_prefix, current_scope_id) { - debug!("Found type member completions for variable '{}'", var_name); - return completions; - } - - // If not a variable, try as module member access - debug!("Detected module member access: {} and {}", var_name, member_prefix); - return self.get_module_member_completions(&var_name, &member_prefix); - } - - // Normal variable completion - debug!("Getting completions at position {} with prefix '{}'", position, prefix); - - // 1. Find current scope based on position - let current_scope_id = self.find_innermost_scope(self.module.scope_id, position); - debug!("Found scope_id {} by positon {}", current_scope_id, position); - - // cannot auto-import in global module scope - if current_scope_id == self.module.scope_id { - return Vec::new(); - } - - // 2. Collect all visible variable symbols - let mut completions = Vec::new(); - self.collect_variable_completions(current_scope_id, &prefix, &mut completions, position); - - // 3. Collect already-imported modules (show at top) - self.collect_imported_module_completions(&prefix, &mut completions); - - // 4. Collect available modules (for auto-import) - self.collect_module_completions(&prefix, &mut completions); - - // 5. Sort and filter - self.sort_and_filter_completions(&mut completions, &prefix); - - debug!("Found {} completions", completions.len()); - dbg!(&completions); - - completions - } - - /// Get auto-completions for module members - pub fn get_module_member_completions(&self, imported_as_name: &str, prefix: &str) -> Vec { - debug!("Getting module member completions for module '{}' with prefix '{}'", imported_as_name, prefix); - - let mut completions = Vec::new(); - - let deps = &self.module.dependencies; - - let import_stmt = deps.iter().find(|&dep| dep.as_name == imported_as_name); - if import_stmt.is_none() { - return completions; - } - - let imported_module_ident = import_stmt.unwrap().module_ident.clone(); - debug!("Imported module is '{}' find by {}", imported_module_ident, imported_as_name); - - // Find imported module scope - if let Some(&imported_scope_id) = self.symbol_table.module_scopes.get(&imported_module_ident) { - let imported_scope = self.symbol_table.find_scope(imported_scope_id); - debug!("Found imported scope {} with {} symbols", imported_scope_id, imported_scope.symbols.len()); - - // Iterate through all symbols in imported module - for &symbol_id in &imported_scope.symbols { - if let Some(symbol) = self.symbol_table.get_symbol_ref(symbol_id) { - if let SymbolKind::Fn(fndef) = &symbol.kind { - if fndef.lock().unwrap().is_test { - continue; - } - } - - // Extract the actual symbol name (without module path) - let symbol_name = extract_last_ident_part(&symbol.ident); - - debug!( - "Checking symbol: {} (extracted: {}) of kind: {:?}", - symbol.ident, - symbol_name, - match &symbol.kind { - SymbolKind::Var(_) => "Var", - SymbolKind::Const(_) => "Const", - SymbolKind::Fn(_) => "Fn", - SymbolKind::Type(_) => "Type", - } - ); - - // Check prefix match against the extracted name - if prefix.is_empty() || symbol_name.starts_with(prefix) { - let completion_item = self.create_module_completion_member(symbol); - debug!("Adding module member completion: {} (kind: {:?})", completion_item.label, completion_item.kind); - completions.push(completion_item); - } - } - } - } else { - debug!("Module '{}' not found in symbol table", imported_module_ident); - } - - // Sort and filter - self.sort_and_filter_completions(&mut completions, prefix); - - debug!("Found {} module member completions", completions.len()); - completions - } - - /// Get auto-completions for type members (for struct fields and methods) - pub fn get_type_member_completions(&self, var_name: &str, prefix: &str, current_scope_id: NodeId) -> Option> { - debug!("Getting type member completions for variable '{}' with prefix '{}'", var_name, prefix); - - // 1. Find the variable in the current scope - let var_symbol = self.find_variable_in_scope(var_name, current_scope_id)?; - - // 2. Get the variable's type - use crate::analyzer::common::TypeKind; - let (var_type_kind, typedef_symbol_id) = match &var_symbol.kind { - SymbolKind::Var(var_decl) => { - let var = var_decl.lock().unwrap(); - let type_ = &var.type_; - - debug!("Variable '{}' has type: {:?}, symbol_id: {}", var_name, type_.kind, type_.symbol_id); - - (type_.kind.clone(), type_.symbol_id) - } - _ => { - debug!("Symbol is not a variable"); - return None; - } - }; - - let mut completions = Vec::new(); - - // 3. Handle direct struct type (inlined) - if let TypeKind::Struct(_, _, properties) = &var_type_kind { - debug!("Variable has direct struct type with {} fields", properties.len()); - for prop in properties { - if prefix.is_empty() || prop.name.starts_with(prefix) { - completions.push(CompletionItem { - label: prop.name.clone(), - kind: CompletionItemKind::Variable, - detail: Some(format!("field: {}", prop.type_)), - documentation: None, - insert_text: prop.name.clone(), - sort_text: Some(format!("{:08}", prop.start)), - additional_text_edits: Vec::new(), - }); - } - } - } - - // 4. Handle typedef reference - if matches!(var_type_kind, TypeKind::Ident) && typedef_symbol_id != 0 { - debug!("Looking up typedef with symbol_id: {}", typedef_symbol_id); - - if let Some(typedef_symbol) = self.symbol_table.get_symbol_ref(typedef_symbol_id) { - if let SymbolKind::Type(typedef) = &typedef_symbol.kind { - let typedef = typedef.lock().unwrap(); - debug!( - "Found typedef: {}, type_expr.kind: {:?}, methods: {}", - typedef.ident, - typedef.type_expr.kind, - typedef.method_table.len() - ); - - // Add struct fields from typedef - if let TypeKind::Struct(_, _, properties) = &typedef.type_expr.kind { - debug!("Typedef struct has {} fields", properties.len()); - for prop in properties { - if prefix.is_empty() || prop.name.starts_with(prefix) { - completions.push(CompletionItem { - label: prop.name.clone(), - kind: CompletionItemKind::Variable, - detail: Some(format!("field: {}", prop.type_)), - documentation: None, - insert_text: prop.name.clone(), - sort_text: Some(format!("{:08}", prop.start)), - additional_text_edits: Vec::new(), - }); - } - } - } - - // Add methods for typedef - for method in typedef.method_table.values() { - let fndef = method.lock().unwrap(); - if prefix.is_empty() || fndef.fn_name.starts_with(prefix) { - let signature = self.format_function_signature(&fndef); - let insert_text = if self.has_parameters(&fndef) { - format!("{}($0)", fndef.fn_name) - } else { - format!("{}()", fndef.fn_name) - }; - completions.push(CompletionItem { - label: fndef.fn_name.clone(), - kind: CompletionItemKind::Function, - detail: Some(format!("fn: {}", signature)), - documentation: None, - insert_text, - sort_text: Some(format!("{:08}", fndef.symbol_start)), - additional_text_edits: Vec::new(), - }); - } - } - } else { - debug!("Symbol {} is not a Type", typedef_symbol_id); - } - } - } - - // 5. Look for methods by searching for functions with impl_type matching this type - // This handles cases like: fn config.helloWorld() - if typedef_symbol_id != 0 { - self.collect_impl_methods(typedef_symbol_id, prefix, &mut completions); - } - - self.sort_and_filter_completions(&mut completions, prefix); - debug!("Found {} type member completions", completions.len()); - - if completions.is_empty() { - None - } else { - Some(completions) - } - } - - /// Get auto-completions for struct fields during initialization (type_name{ field: ... }) - pub fn get_struct_field_completions(&self, type_name: &str, prefix: &str, current_scope_id: NodeId) -> Vec { - debug!("Getting struct field completions for type '{}' with prefix '{}'", type_name, prefix); - - let mut completions = Vec::new(); - - // Find the type symbol - let type_symbol = self.find_type_in_scope(type_name, current_scope_id); - if type_symbol.is_none() { - debug!("Type '{}' not found in scope", type_name); - return completions; - } - - let type_symbol = type_symbol.unwrap(); - - // Extract struct fields from the type - if let SymbolKind::Type(typedef) = &type_symbol.kind { - let typedef = typedef.lock().unwrap(); - - use crate::analyzer::common::TypeKind; - if let TypeKind::Struct(_, _, properties) = &typedef.type_expr.kind { - debug!("Found struct type with {} fields", properties.len()); - - for prop in properties { - if prefix.is_empty() || prop.name.starts_with(prefix) { - completions.push(CompletionItem { - label: prop.name.clone(), - kind: CompletionItemKind::Variable, - detail: Some(format!("{}: {}", prop.name, prop.type_)), - documentation: None, - insert_text: format!("{}: ", prop.name), - sort_text: Some(format!("{:08}", prop.start)), - additional_text_edits: Vec::new(), - }); - } - } - } else { - debug!("Type '{}' is not a struct type", type_name); - } - } - - self.sort_and_filter_completions(&mut completions, prefix); - debug!("Found {} struct field completions", completions.len()); - - completions - } - - /// Collect methods implemented for a type (fn TypeName.method()) - fn collect_impl_methods(&self, typedef_symbol_id: NodeId, prefix: &str, completions: &mut Vec) { - // Search module scope for functions with matching impl_type - let module_scope = self.symbol_table.find_scope(self.module.scope_id); - for &symbol_id in &module_scope.symbols { - if let Some(symbol) = self.symbol_table.get_symbol_ref(symbol_id) { - if let SymbolKind::Fn(fndef) = &symbol.kind { - let fndef = fndef.lock().unwrap(); - // Check if this function's impl_type matches our typedef - if fndef.impl_type.symbol_id == typedef_symbol_id { - if prefix.is_empty() || fndef.fn_name.starts_with(prefix) { - debug!("Found impl method: {}", fndef.fn_name); - let signature = self.format_function_signature(&fndef); - let insert_text = if self.has_parameters(&fndef) { - format!("{}($0)", fndef.fn_name) - } else { - format!("{}()", fndef.fn_name) - }; - completions.push(CompletionItem { - label: fndef.fn_name.clone(), - kind: CompletionItemKind::Function, - detail: Some(format!("fn: {}", signature)), - documentation: None, - insert_text, - sort_text: Some(format!("{:08}", fndef.symbol_start)), - additional_text_edits: Vec::new(), - }); - } - } - } - } - } - } - - /// Find a variable in the current scope and parent scopes - fn find_variable_in_scope(&self, var_name: &str, current_scope_id: NodeId) -> Option<&Symbol> { - debug!("Searching for variable '{}' starting from scope {}", var_name, current_scope_id); - let mut visited_scopes = HashSet::new(); - let mut current = current_scope_id; - - while current > 0 && !visited_scopes.contains(¤t) { - visited_scopes.insert(current); - let scope = self.symbol_table.find_scope(current); - debug!("Checking scope {} with {} symbols", current, scope.symbols.len()); - - for &symbol_id in &scope.symbols { - if let Some(symbol) = self.symbol_table.get_symbol_ref(symbol_id) { - match &symbol.kind { - SymbolKind::Var(var_decl) => { - let var = var_decl.lock().unwrap(); - debug!("Found var symbol: '{}' (looking for '{}')", var.ident, var_name); - if var.ident == var_name { - drop(var); - debug!("Variable '{}' found in scope {}", var_name, current); - return Some(symbol); - } - } - _ => {} - } - } - } - - current = scope.parent; - if current == 0 { - break; - } - } - - debug!("Variable '{}' not found in any scope", var_name); - None - } - - /// Find a type in the current scope and parent scopes - fn find_type_in_scope(&self, type_name: &str, current_scope_id: NodeId) -> Option<&Symbol> { - debug!("Searching for type '{}' starting from scope {}", type_name, current_scope_id); - let mut visited_scopes = HashSet::new(); - let mut current = current_scope_id; - - while current > 0 && !visited_scopes.contains(¤t) { - visited_scopes.insert(current); - let scope = self.symbol_table.find_scope(current); - debug!("Checking scope {} with {} symbols", current, scope.symbols.len()); - - for &symbol_id in &scope.symbols { - if let Some(symbol) = self.symbol_table.get_symbol_ref(symbol_id) { - match &symbol.kind { - SymbolKind::Type(typedef) => { - let type_def = typedef.lock().unwrap(); - let symbol_name = extract_last_ident_part(&type_def.ident); - debug!( - "Found type symbol: '{}' (extracted: '{}', looking for '{}')", - type_def.ident, symbol_name, type_name - ); - if symbol_name == type_name { - drop(type_def); - debug!("Type '{}' found in scope {}", type_name, current); - return Some(symbol); - } - } - _ => {} - } - } - } - - current = scope.parent; - if current == 0 { - break; - } - } - - debug!("Type '{}' not found in any scope", type_name); - None - } - - /// Check if function has parameters (excluding self) - fn has_parameters(&self, fndef: &AstFnDef) -> bool { - fndef.params.iter().any(|param| { - let param_locked = param.lock().unwrap(); - param_locked.ident != "self" - }) - } - - /// Format a function signature for display - fn format_function_signature(&self, fndef: &AstFnDef) -> String { - let mut params_str = String::new(); - let mut first = true; - - for param in fndef.params.iter() { - let param_locked = param.lock().unwrap(); - - // Skip 'self' parameter - if param_locked.ident == "self" { - continue; - } - - if !first { - params_str.push_str(", "); - } - first = false; - - // Use the ident field from Type which contains the original type name - let type_str = if !param_locked.type_.ident.is_empty() { - param_locked.type_.ident.clone() - } else { - param_locked.type_.to_string() - }; - - params_str.push_str(&format!("{} {}", type_str, param_locked.ident)); - } - - if fndef.rest_param && !params_str.is_empty() { - params_str.push_str(", ..."); - } - - let return_type = fndef.return_type.to_string(); - format!("fn({}): {}", params_str, return_type) - } - - /// Find innermost scope containing the position starting from module scope - fn find_innermost_scope(&self, scope_id: NodeId, position: usize) -> NodeId { - let scope = self.symbol_table.find_scope(scope_id); - debug!("[find_innermost_scope] scope_id {}, start {}, end {}", scope_id, scope.range.0, scope.range.1); - - // Check if current scope contains this position (range.1 == 0 means file-level scope) - if position >= scope.range.0 && (position < scope.range.1 || scope.range.1 == 0) { - // Check child scopes, find innermost scope - for &child_id in &scope.children { - let child_scope = self.symbol_table.find_scope(child_id); - - debug!( - "[find_innermost_scope] child scope_id {}, start {}, end {}", - scope_id, child_scope.range.0, child_scope.range.1 - ); - if position >= child_scope.range.0 && position < child_scope.range.1 { - return self.find_innermost_scope(child_id, position); - } - } - - return scope_id; - } - - scope_id // If not in range, return current scope - } - - /// Collect variable completion items - fn collect_variable_completions(&self, current_scope_id: NodeId, prefix: &str, completions: &mut Vec, position: usize) { - let mut visited_scopes = HashSet::new(); - let mut current = current_scope_id; - - // Traverse upward from current scope - while current > 0 && !visited_scopes.contains(¤t) { - visited_scopes.insert(current); - - let scope = self.symbol_table.find_scope(current); - debug!("Searching scope {} with {} symbols", current, scope.symbols.len()); - - // Iterate through all symbols in current scope - for &symbol_id in &scope.symbols { - if let Some(symbol) = self.symbol_table.get_symbol_ref(symbol_id) { - dbg!("Found symbol will check", symbol.ident.clone(), prefix, symbol.ident.starts_with(prefix)); - - // Only handle variables and constants - match &symbol.kind { - SymbolKind::Var(_) | SymbolKind::Const(_) => { - if (prefix.is_empty() || symbol.ident.starts_with(prefix)) && symbol.pos < position { - let completion_item = self.create_completion_item(symbol); - debug!("Adding completion: {}", completion_item.label); - completions.push(completion_item); - } - } - _ => {} - } - } - } - current = scope.parent; - - // If reached root scope, stop traversal - if current == 0 { - break; - } - } - } - - /// Collect already-imported modules as completions (show at top) - fn collect_imported_module_completions(&self, prefix: &str, completions: &mut Vec) { - debug!("Collecting imported module completions with prefix '{}'", prefix); - - for import_stmt in &self.module.dependencies { - let module_name = &import_stmt.as_name; - - // Check prefix match - if !prefix.is_empty() && !module_name.starts_with(prefix) { - continue; - } - - debug!("Found imported module: {}", module_name); - - // Determine import statement display - let import_display = if let Some(file) = &import_stmt.file { - format!("import '{}'", file) - } else if let Some(ref package) = import_stmt.ast_package { - format!("import {}", package.join(".")) - } else { - format!("import {}", module_name) - }; - - completions.push(CompletionItem { - label: module_name.to_string(), - kind: CompletionItemKind::Module, - detail: Some(format!("imported: {}", import_display)), - documentation: Some(format!("Already imported module: {}", module_name)), - insert_text: module_name.to_string(), - sort_text: Some(format!("aaa_{}", module_name)), // Sort to top with "aaa" prefix - additional_text_edits: Vec::new(), - }); - } - } - - /// Collect available module completions (for auto-import) - fn collect_module_completions(&self, prefix: &str, completions: &mut Vec) { - debug!("Collecting module completions with prefix '{}'", prefix); - - // Check which modules are already imported - let already_imported: HashSet = self.module.dependencies.iter().map(|dep| dep.as_name.clone()).collect(); - - // 1. Scan .n files in current directory (file-based imports) - let module_dir = &self.module.dir; - debug!("Scanning module directory: {}", module_dir); - - if let Ok(entries) = std::fs::read_dir(module_dir) { - for entry in entries { - if let Ok(entry) = entry { - let path = entry.path(); - if path.is_file() && path.extension().and_then(|s| s.to_str()) == Some("n") { - if let Some(file_stem) = path.file_stem().and_then(|s| s.to_str()) { - // Skip current file - if path == std::path::Path::new(&self.module.path) { - continue; - } - - // Skip already imported modules - if already_imported.contains(file_stem) { - continue; - } - - // Check prefix match - if !prefix.is_empty() && !file_stem.starts_with(prefix) { - continue; - } - - debug!("Found module file: {}", file_stem); - - // Calculate import statement to insert at beginning of file - let module_name = path.file_name().unwrap().to_str().unwrap(); - let import_statement = format!("import '{}'\n", module_name); - - completions.push(CompletionItem { - label: file_stem.to_string(), - kind: CompletionItemKind::Module, - detail: Some(format!("{}", import_statement)), - documentation: Some(format!("Import module from {}", path.display())), - insert_text: file_stem.to_string(), - sort_text: Some(format!("zzz_{}", file_stem)), // Sort to back - additional_text_edits: vec![TextEdit { - line: 0, - character: 0, - new_text: import_statement, - }], - }); - } - } - } - } - } - - // 2. Scan subdirectories for package-based imports (if package.toml exists) - if let Some(ref pkg_config) = self.package_config { - self.scan_subdirectories_for_modules(&self.project_root, &pkg_config.package_data.name, "", prefix, &already_imported, completions); - } - - // 3. Scan standard library packages - let std_dir = std::path::Path::new(&self.nature_root).join("std"); - debug!("Scanning std directory: {}", std_dir.display()); - - if let Ok(entries) = std::fs::read_dir(&std_dir) { - for entry in entries { - if let Ok(entry) = entry { - let path = entry.path(); - if path.is_dir() { - if let Some(module_name) = path.file_name().and_then(|s| s.to_str()) { - // Exclude special directories - if [".", "..", "builtin"].contains(&module_name) { - continue; - } - - // Skip already imported packages - if already_imported.contains(module_name) { - continue; - } - - // Check prefix match - if !prefix.is_empty() && !module_name.starts_with(prefix) { - continue; - } - - debug!("Found std package: {}", module_name); - - // Standard library uses import package_name syntax - let import_statement = format!("import {}\n", module_name); - - completions.push(CompletionItem { - label: module_name.to_string(), - kind: CompletionItemKind::Module, - detail: Some(format!("std: {}", import_statement)), - documentation: Some(format!("Import standard library package: {}", module_name)), - insert_text: module_name.to_string(), - sort_text: Some(format!("zzz_{}", module_name)), // Sort to back - additional_text_edits: vec![TextEdit { - line: 0, - character: 0, - new_text: import_statement, - }], - }); - } - } - } - } - } - } - - /// Recursively scan subdirectories for package-based module imports - fn scan_subdirectories_for_modules( - &self, - base_dir: &str, - package_name: &str, - current_path: &str, - prefix: &str, - already_imported: &HashSet, - completions: &mut Vec, - ) { - let scan_dir = if current_path.is_empty() { - std::path::PathBuf::from(base_dir) - } else { - std::path::PathBuf::from(base_dir).join(current_path) - }; - - if let Ok(entries) = std::fs::read_dir(&scan_dir) { - for entry in entries { - if let Ok(entry) = entry { - let path = entry.path(); - - // Check subdirectories - if path.is_dir() { - if let Some(dir_name) = path.file_name().and_then(|s| s.to_str()) { - let new_path = if current_path.is_empty() { - dir_name.to_string() - } else { - format!("{}/{}", current_path, dir_name) - }; - - // Recursively scan subdirectory - self.scan_subdirectories_for_modules(base_dir, package_name, &new_path, prefix, already_imported, completions); - } - } - // Check .n files in subdirectories - else if path.is_file() && path.extension().and_then(|s| s.to_str()) == Some("n") { - if let Some(file_stem) = path.file_stem().and_then(|s| s.to_str()) { - // Skip if this is in the root directory (already handled by file-based imports) - if current_path.is_empty() { - continue; - } - - // Build import path: package_name.folder.module_name - let module_import_path = if current_path.is_empty() { - format!("{}.{}", package_name, file_stem) - } else { - format!("{}.{}.{}", package_name, current_path.replace("/", "."), file_stem) - }; - - // Skip already imported modules - if already_imported.contains(file_stem) { - continue; - } - - // Check prefix match (match against the module name, not full path) - if !prefix.is_empty() && !file_stem.starts_with(prefix) { - continue; - } - - debug!("Found subdirectory module: {} at {}", file_stem, module_import_path); - - let import_statement = format!("import {}\n", module_import_path); - - completions.push(CompletionItem { - label: file_stem.to_string(), - kind: CompletionItemKind::Module, - detail: Some(format!("pkg: {}", import_statement)), - documentation: Some(format!("Import module from package: {}", module_import_path)), - insert_text: file_stem.to_string(), - sort_text: Some(format!("zzz_{}", file_stem)), - additional_text_edits: vec![TextEdit { - line: 0, - character: 0, - new_text: import_statement, - }], - }); - } - } - } - } - } - } - - /// Create completion item - fn create_completion_item(&self, symbol: &Symbol) -> CompletionItem { - let (kind, detail) = match &symbol.kind { - SymbolKind::Var(var_decl) => { - let detail = { - let var = var_decl.lock().unwrap(); - format!("var: {}", var.type_) - }; - (CompletionItemKind::Variable, Some(detail)) - } - SymbolKind::Const(const_def) => { - let detail = { - let const_val = const_def.lock().unwrap(); - format!("const: {}", const_val.type_) - }; - (CompletionItemKind::Constant, Some(detail)) - } - _ => (CompletionItemKind::Variable, None), - }; - - CompletionItem { - label: symbol.ident.clone(), - kind, - detail, - documentation: None, - insert_text: symbol.ident.clone(), - sort_text: Some(format!("{:08}", symbol.pos)), // Sort by definition position - additional_text_edits: Vec::new(), - } - } - - /// Create module member completion item - fn create_module_completion_member(&self, symbol: &Symbol) -> CompletionItem { - let (ident, kind, detail, insert_text, priority) = match &symbol.kind { - SymbolKind::Var(var) => { - let var = var.lock().unwrap(); - let detail = format!("var: {}", var.type_); - let display_ident = extract_last_ident_part(&var.ident.clone()); - (display_ident.clone(), CompletionItemKind::Variable, Some(detail), display_ident, 2) - } - SymbolKind::Const(constdef) => { - let constdef = constdef.lock().unwrap(); - let detail = format!("const: {}", constdef.type_); - let display_ident = extract_last_ident_part(&constdef.ident.clone()); - (display_ident.clone(), CompletionItemKind::Constant, Some(detail), display_ident, 3) - } - SymbolKind::Fn(fndef) => { - let fndef = fndef.lock().unwrap(); - let signature = self.format_function_signature(&fndef); - let insert_text = if self.has_parameters(&fndef) { - format!("{}($0)", fndef.fn_name) - } else { - format!("{}()", fndef.fn_name) - }; - (fndef.fn_name.clone(), CompletionItemKind::Function, Some(signature), insert_text, 0) - } - SymbolKind::Type(typedef) => { - let typedef = typedef.lock().unwrap(); - let detail = format!("type definition"); - let display_ident = extract_last_ident_part(&typedef.ident); - (display_ident.clone(), CompletionItemKind::Struct, Some(detail), display_ident, 1) - } - }; - - CompletionItem { - label: ident.clone(), - kind, - detail, - documentation: None, - insert_text, - sort_text: Some(format!("{}_{}", priority, ident)), - additional_text_edits: Vec::new(), - } - } - - /// Sort and filter completion items - fn sort_and_filter_completions(&self, completions: &mut Vec, prefix: &str) { - // Deduplicate - based on label - completions.sort_by(|a, b| a.label.cmp(&b.label)); - completions.dedup_by(|a, b| a.label == b.label); - - // Sort by: 1) kind priority, 2) prefix match, 3) alphabetically - completions.sort_by(|a, b| { - // Priority order: Function > Struct > Variable > Constant > Module - let a_priority = match a.kind { - CompletionItemKind::Function => 0, - CompletionItemKind::Struct => 1, - CompletionItemKind::Variable | CompletionItemKind::Parameter => 2, - CompletionItemKind::Constant => 3, - CompletionItemKind::Module => 4, - }; - let b_priority = match b.kind { - CompletionItemKind::Function => 0, - CompletionItemKind::Struct => 1, - CompletionItemKind::Variable | CompletionItemKind::Parameter => 2, - CompletionItemKind::Constant => 3, - CompletionItemKind::Module => 4, - }; - - // First sort by kind priority - match a_priority.cmp(&b_priority) { - std::cmp::Ordering::Equal => { - // Then by prefix match - let a_exact = a.label.starts_with(prefix); - let b_exact = b.label.starts_with(prefix); - - match (a_exact, b_exact) { - (true, false) => std::cmp::Ordering::Less, - (false, true) => std::cmp::Ordering::Greater, - _ => { - // Finally sort alphabetically - a.label.cmp(&b.label) - } - } - } - other => other, - } - }); - - // Limit number of results - completions.truncate(50); - } -} - -/// Extract prefix at cursor position from text -pub fn extract_prefix_at_position(text: &str, position: usize) -> String { - if position == 0 { - return String::new(); - } - - let chars: Vec = text.chars().collect(); - if position > chars.len() { - return String::new(); - } - - let mut start = position; - - // Search backward for start of identifier - while start > 0 { - let ch = chars[start - 1]; - if ch.is_alphanumeric() || ch == '_' || ch == '.' { - start -= 1; - } else { - break; - } - } - - // Extract prefix - chars[start..position].iter().collect() -} - -/// Extract last identifier part from something like "io.main.writer" to get "writer" -fn extract_last_ident_part(ident: &str) -> String { - if let Some(dot_pos) = ident.rfind('.') { - ident[dot_pos + 1..].to_string() - } else { - ident.to_string() - } -} - -/// Detect if in module member access context, returns (module_name, member_prefix) -pub fn extract_module_member_context(prefix: &str, _position: usize) -> Option<(String, String)> { - if let Some(dot_pos) = prefix.rfind('.') { - let module_name = prefix[..dot_pos].to_string(); - let member_prefix = prefix[dot_pos + 1..].to_string(); - - if !module_name.is_empty() { - return Some((module_name, member_prefix)); - } - } - - None -} - -/// Detect if cursor is inside a struct initialization, returns (type_name, field_prefix) -/// Handles cases like: `config{ [cursor] }` or `config{ value: 42, [cursor] }` -pub fn extract_struct_init_context(text: &str, position: usize) -> Option<(String, String)> { - let chars: Vec = text.chars().collect(); - if position > chars.len() { - return None; - } - - // Look backward to find the opening brace and type name - let mut i = position; - - // First, extract any prefix at the cursor position - let mut field_prefix_end = position; - while field_prefix_end > 0 { - let ch = chars[field_prefix_end - 1]; - if ch.is_alphanumeric() || ch == '_' { - field_prefix_end -= 1; - } else { - break; - } - } - let field_prefix: String = chars[field_prefix_end..position].iter().collect(); - - // Look for opening brace - while i > 0 { - i -= 1; - let ch = chars[i]; - - if ch == '{' { - // Found opening brace, now look for the type name before it - let mut type_end = i; - - // Skip whitespace between type name and brace - while type_end > 0 && chars[type_end - 1].is_whitespace() { - type_end -= 1; - } - - // Extract type name - let mut type_start = type_end; - while type_start > 0 { - let ch = chars[type_start - 1]; - if ch.is_alphanumeric() || ch == '_' || ch == '.' { - type_start -= 1; - } else { - break; - } - } - - if type_start < type_end { - let type_name: String = chars[type_start..type_end].iter().collect(); - - // Avoid treating test blocks as struct initializations: `test name { ... }` - let mut kw_end = type_start; - while kw_end > 0 && chars[kw_end - 1].is_whitespace() { - kw_end -= 1; - } - let mut kw_start = kw_end; - while kw_start > 0 { - let ch = chars[kw_start - 1]; - if ch.is_alphanumeric() || ch == '_' { - kw_start -= 1; - } else { - break; - } - } - if kw_start < kw_end { - let maybe_kw: String = chars[kw_start..kw_end].iter().collect(); - if maybe_kw == "test" { - return None; - } - } - - debug!("Detected struct init context: type='{}', field_prefix='{}'", type_name, field_prefix); - return Some((type_name, field_prefix)); - } - - return None; - } else if ch == '}' || ch == ';' { - // We've left the struct initialization context - return None; - } - } - - None -} diff --git a/nls/src/analyzer/completion/context.rs b/nls/src/analyzer/completion/context.rs new file mode 100644 index 00000000..ce7c0ee2 --- /dev/null +++ b/nls/src/analyzer/completion/context.rs @@ -0,0 +1,353 @@ +use log::debug; + +/// Extract prefix at cursor position from text +pub fn extract_prefix_at_position(text: &str, position: usize) -> String { + if position == 0 { + return String::new(); + } + + let chars: Vec = text.chars().collect(); + if position > chars.len() { + return String::new(); + } + + let mut start = position; + + // Search backward for start of identifier + while start > 0 { + let ch = chars[start - 1]; + if ch.is_alphanumeric() || ch == '_' || ch == '.' { + start -= 1; + } else { + break; + } + } + + // Extract prefix + chars[start..position].iter().collect() +} + +/// Extract last identifier part from something like "io.main.writer" to get "writer" +pub(crate) fn extract_last_ident_part(ident: &str) -> String { + if let Some(dot_pos) = ident.rfind('.') { + ident[dot_pos + 1..].to_string() + } else { + ident.to_string() + } +} + +/// Public version of extract_last_ident_part for use by other modules. +pub fn extract_last_ident_part_pub(ident: &str) -> String { + extract_last_ident_part(ident) +} + +/// Detect if in module member access context, returns (module_name, member_prefix) +pub fn extract_module_member_context(prefix: &str, _position: usize) -> Option<(String, String)> { + if let Some(dot_pos) = prefix.rfind('.') { + let module_name = prefix[..dot_pos].to_string(); + let member_prefix = prefix[dot_pos + 1..].to_string(); + + if !module_name.is_empty() { + return Some((module_name, member_prefix)); + } + } + + None +} + +/// Detect if cursor is inside a selective import `{...}` block. +/// Returns (module_path, item_prefix) where module_path is the import path before `.{` +/// Example: `import forest.app.create.{cre|` -> ("forest.app.create", "cre") +pub fn extract_selective_import_context(text: &str, position: usize) -> Option<(String, String)> { + let chars: Vec = text.chars().collect(); + if position > chars.len() { + return None; + } + + // Extract any prefix at cursor + let mut prefix_start = position; + while prefix_start > 0 { + let ch = chars[prefix_start - 1]; + if ch.is_alphanumeric() || ch == '_' { + prefix_start -= 1; + } else { + break; + } + } + let item_prefix: String = chars[prefix_start..position].iter().collect(); + + // Look backward from prefix_start (or position) to find `{` + let mut i = prefix_start; + while i > 0 { + i -= 1; + let ch = chars[i]; + + if ch == '{' { + // Found `{` — now check if it's preceded by `.` and then an import path + // Before `{` there should be a `.` + let mut dot_pos = i; + // skip whitespace between `.` and `{` + while dot_pos > 0 && chars[dot_pos - 1].is_whitespace() { + dot_pos -= 1; + } + if dot_pos == 0 || chars[dot_pos - 1] != '.' { + return None; + } + let dot_idx = dot_pos - 1; + + // Before the `.` should be the module path (ident chars and dots) + let path_end = dot_idx; + let mut path_start = path_end; + while path_start > 0 { + let ch = chars[path_start - 1]; + if ch.is_alphanumeric() || ch == '_' || ch == '.' { + path_start -= 1; + } else { + break; + } + } + + if path_start >= path_end { + return None; + } + + let module_path: String = chars[path_start..path_end].iter().collect(); + + // Check that `import` keyword precedes the module path + let mut kw_end = path_start; + while kw_end > 0 && chars[kw_end - 1].is_whitespace() { + kw_end -= 1; + } + let mut kw_start = kw_end; + while kw_start > 0 && chars[kw_start - 1].is_alphabetic() { + kw_start -= 1; + } + let keyword: String = chars[kw_start..kw_end].iter().collect(); + if keyword != "import" { + return None; + } + + debug!("Detected selective import context: module='{}', prefix='{}'", module_path, item_prefix); + return Some((module_path, item_prefix)); + } else if ch == '}' || ch == ';' { + // `}` means we're past/outside the braces already + return None; + } + // Skip over commas, spaces, other ident chars (already-typed items) + } + + None +} + +/// Detect if cursor is inside a struct initialization, returns (type_name, field_prefix) +/// Handles cases like: `config{ [cursor] }` or `config{ value: 42, [cursor] }` +pub fn extract_struct_init_context(text: &str, position: usize) -> Option<(String, String)> { + let chars: Vec = text.chars().collect(); + if position > chars.len() { + return None; + } + + // Look backward to find the opening brace and type name + let mut i = position; + + // First, extract any prefix at the cursor position + let mut field_prefix_end = position; + while field_prefix_end > 0 { + let ch = chars[field_prefix_end - 1]; + if ch.is_alphanumeric() || ch == '_' { + field_prefix_end -= 1; + } else { + break; + } + } + let field_prefix: String = chars[field_prefix_end..position].iter().collect(); + + // Look for opening brace + while i > 0 { + i -= 1; + let ch = chars[i]; + + if ch == '{' { + // Found opening brace, now look for the type name before it + let mut type_end = i; + + // Skip whitespace between type name and brace + while type_end > 0 && chars[type_end - 1].is_whitespace() { + type_end -= 1; + } + + // Extract type name + let mut type_start = type_end; + while type_start > 0 { + let ch = chars[type_start - 1]; + if ch.is_alphanumeric() || ch == '_' || ch == '.' { + type_start -= 1; + } else { + break; + } + } + + if type_start < type_end { + let type_name: String = chars[type_start..type_end].iter().collect(); + + // Reject block keywords used as the "type name" + // (e.g. `if cond {`, `for x in list {`, `else {`, `match val {`) + let block_keywords = ["if", "else", "for", "match", "while", "catch"]; + if block_keywords.contains(&type_name.as_str()) { + return None; + } + + // Check the character immediately before the type name + // (after skipping whitespace). A `:` means this is a return-type + // annotation (`fn foo(): string {`), not a struct init. + // A `)` means `fn foo() {` — also not a struct init. + let mut before = type_start; + while before > 0 && chars[before - 1].is_whitespace() { + before -= 1; + } + if before > 0 { + let prev_char = chars[before - 1]; + if prev_char == ':' || prev_char == ')' { + return None; + } + } + + // Avoid treating test/fn blocks as struct initializations + let mut kw_end = type_start; + while kw_end > 0 && chars[kw_end - 1].is_whitespace() { + kw_end -= 1; + } + let mut kw_start = kw_end; + while kw_start > 0 { + let ch = chars[kw_start - 1]; + if ch.is_alphanumeric() || ch == '_' { + kw_start -= 1; + } else { + break; + } + } + if kw_start < kw_end { + let maybe_kw: String = chars[kw_start..kw_end].iter().collect(); + if maybe_kw == "test" || maybe_kw == "fn" { + return None; + } + } + + debug!("Detected struct init context: type='{}', field_prefix='{}'", type_name, field_prefix); + return Some((type_name, field_prefix)); + } + + return None; + } else if ch == '}' || ch == ';' { + // We've left the struct initialization context + return None; + } + } + + None +} + +#[cfg(test)] +mod tests { + use super::extract_struct_init_context; + + #[test] + fn detects_basic_struct_init() { + let text = "MyStruct{ na"; + let result = extract_struct_init_context(text, text.len()); + assert_eq!(result, Some(("MyStruct".into(), "na".into()))); + } + + #[test] + fn detects_struct_init_with_space() { + let text = "MyStruct { name"; + let result = extract_struct_init_context(text, text.len()); + assert_eq!(result, Some(("MyStruct".into(), "name".into()))); + } + + #[test] + fn empty_prefix() { + let text = "MyStruct{ "; + let result = extract_struct_init_context(text, text.len()); + assert_eq!(result, Some(("MyStruct".into(), "".into()))); + } + + #[test] + fn rejects_fn_body_brace() { + // fn MyCustomStruct.toString(): string { ... } + let text = "fn MyCustomStruct.toString(): string { "; + let result = extract_struct_init_context(text, text.len()); + assert_eq!(result, None); + } + + #[test] + fn rejects_fn_no_return_type() { + let text = "fn foo() { "; + let result = extract_struct_init_context(text, text.len()); + assert_eq!(result, None); + } + + #[test] + fn rejects_if_block() { + // "if" is a block keyword, but the extracted "type name" is + // "condition" (the word before {). The if-keyword check works + // when `if` directly precedes `{`, like `if {`. + let text = "if { "; + let result = extract_struct_init_context(text, text.len()); + assert_eq!(result, None); + } + + #[test] + fn if_with_condition_falls_through() { + // `if condition {` extracts "condition" as struct name. + // The completion system's fallthrough handles this gracefully. + let text = "if condition { "; + let result = extract_struct_init_context(text, text.len()); + assert_eq!(result, Some(("condition".into(), "".into()))); + } + + #[test] + fn rejects_else_block() { + let text = "else { "; + let result = extract_struct_init_context(text, text.len()); + assert_eq!(result, None); + } + + #[test] + fn rejects_for_block() { + // `for {` is rejected by the block keyword check. + let text = "for { "; + let result = extract_struct_init_context(text, text.len()); + assert_eq!(result, None); + } + + #[test] + fn for_with_expr_falls_through() { + // `for x in list {` extracts "list" as struct name. + // The completion fallthrough handles this. + let text = "for x in list { "; + let result = extract_struct_init_context(text, text.len()); + assert_eq!(result, Some(("list".into(), "".into()))); + } + + #[test] + fn rejects_test_block() { + let text = "test my_test { "; + let result = extract_struct_init_context(text, text.len()); + assert_eq!(result, None); + } + + #[test] + fn stops_at_closing_brace() { + let text = "MyStruct{ name: 1 }; OtherStruct{ "; + let result = extract_struct_init_context(text, text.len()); + assert_eq!(result, Some(("OtherStruct".into(), "".into()))); + } + + #[test] + fn stops_at_semicolon() { + let text = "var x = 1; MyStruct{ "; + let result = extract_struct_init_context(text, text.len()); + assert_eq!(result, Some(("MyStruct".into(), "".into()))); + } +} diff --git a/nls/src/analyzer/completion/imports.rs b/nls/src/analyzer/completion/imports.rs new file mode 100644 index 00000000..ace48643 --- /dev/null +++ b/nls/src/analyzer/completion/imports.rs @@ -0,0 +1,873 @@ +use crate::analyzer::common::ImportStmt; +use crate::analyzer::symbol::SymbolKind; +use crate::analyzer::workspace_index::IndexedSymbolKind; +use log::debug; +use std::collections::HashSet; + +use super::context::extract_last_ident_part; +use super::{CompletionItem, CompletionItemKind, CompletionProvider, TextEdit}; + +impl<'a> CompletionProvider<'a> { + /// Get completions for selective import context: `import module.path.{|}` + /// Lists all exportable symbols from the target module. + pub fn get_selective_import_completions(&self, module_path: &str, prefix: &str) -> Vec { + debug!("Getting selective import completions for module path '{}' with prefix '{}'", module_path, prefix); + + let mut completions = Vec::new(); + + // Find the import statement whose module path matches + // The module_path from text is like "forest.app.create", the module_ident + // in dependencies is derived from full path. Match via ast_package. + let import_stmt = self.module.dependencies.iter().find(|dep| { + if let Some(ref pkg) = dep.ast_package { + let pkg_path = pkg.join("."); + // The module_path may or may not include a trailing dot + pkg_path == module_path || pkg_path.starts_with(module_path) + } else { + false + } + }); + + let module_ident = if let Some(stmt) = import_stmt { + stmt.module_ident.clone() + } else { + // Try to find in dependencies where module_ident or as_name contains the path + // Fall back: try matching the last segment + let last_seg = module_path.rsplit('.').next().unwrap_or(module_path); + if let Some(stmt) = self.module.dependencies.iter().find(|dep| { + dep.module_ident.ends_with(last_seg) || dep.as_name == last_seg + }) { + stmt.module_ident.clone() + } else { + debug!("Module path '{}' not found in dependencies", module_path); + return completions; + } + }; + + debug!("Resolved module_ident: '{}'", module_ident); + + // Find the module's scope and list its symbols + if let Some(&scope_id) = self.symbol_table.module_scopes.get(&module_ident) { + let scope = self.symbol_table.find_scope(scope_id); + debug!("Found module scope {} with {} symbols", scope_id, scope.symbols.len()); + + // Collect already-imported item names to mark them + let already_imported: HashSet = if let Some(stmt) = import_stmt { + if let Some(ref items) = stmt.select_items { + items.iter().map(|item| item.ident.clone()).collect() + } else { + HashSet::new() + } + } else { + HashSet::new() + }; + + for &symbol_id in &scope.symbols { + if let Some(symbol) = self.symbol_table.get_symbol_ref(symbol_id) { + let symbol_name = extract_last_ident_part(&symbol.ident); + + // Skip test functions and impl methods (struct methods can't be exported) + if let SymbolKind::Fn(fndef) = &symbol.kind { + let fndef_locked = fndef.lock().unwrap(); + if fndef_locked.is_test || fndef_locked.impl_type.kind.is_exist() { + continue; + } + } + + // Skip if already imported + if already_imported.contains(&symbol_name) { + continue; + } + + // Check prefix match + if !prefix.is_empty() && !symbol_name.starts_with(prefix) { + continue; + } + + let (kind, detail) = match &symbol.kind { + SymbolKind::Fn(fndef) => { + let fndef = fndef.lock().unwrap(); + let sig = self.format_function_signature(&fndef); + (CompletionItemKind::Function, format!("fn: {}", sig)) + } + SymbolKind::Type(typedef) => { + let typedef = typedef.lock().unwrap(); + (CompletionItemKind::Struct, format!("type: {}", typedef.ident)) + } + SymbolKind::Var(var) => { + let var = var.lock().unwrap(); + (CompletionItemKind::Variable, format!("var: {}", var.type_)) + } + SymbolKind::Const(c) => { + let c = c.lock().unwrap(); + (CompletionItemKind::Constant, format!("const: {}", c.type_)) + } + }; + + completions.push(CompletionItem { + label: symbol_name.clone(), + kind, + detail: Some(detail), + documentation: None, + insert_text: symbol_name, + sort_text: Some(format!("{:08}", symbol.pos)), + additional_text_edits: Vec::new(), + }); + } + } + } else { + debug!("Module '{}' scope not found in symbol_table.module_scopes", module_ident); + } + + self.sort_and_filter_completions(&mut completions, prefix); + debug!("Found {} selective import completions", completions.len()); + completions + } + + pub fn get_module_member_completions(&self, imported_as_name: &str, prefix: &str) -> Vec { + debug!("Getting module member completions for module '{}' with prefix '{}'", imported_as_name, prefix); + + let mut completions = Vec::new(); + + let deps = &self.module.dependencies; + + let import_stmt = deps.iter().find(|&dep| dep.as_name == imported_as_name); + if import_stmt.is_none() { + // Module not imported — try to offer workspace-indexed symbols + // from a matching module with an auto-import text edit, so the + // user can type `fmt.` and see `sprintf`, `printf`, etc. even + // before adding `import fmt`. + return self.get_unimported_module_member_completions(imported_as_name, prefix); + } + + let imported_module_ident = import_stmt.unwrap().module_ident.clone(); + debug!("Imported module is '{}' find by {}", imported_module_ident, imported_as_name); + + // Find imported module scope + if let Some(&imported_scope_id) = self.symbol_table.module_scopes.get(&imported_module_ident) { + let imported_scope = self.symbol_table.find_scope(imported_scope_id); + debug!("Found imported scope {} with {} symbols", imported_scope_id, imported_scope.symbols.len()); + + // Iterate through all symbols in imported module + for &symbol_id in &imported_scope.symbols { + if let Some(symbol) = self.symbol_table.get_symbol_ref(symbol_id) { + if let SymbolKind::Fn(fndef) = &symbol.kind { + if fndef.lock().unwrap().is_test { + continue; + } + } + + // Extract the actual symbol name (without module path) + let symbol_name = extract_last_ident_part(&symbol.ident); + + debug!( + "Checking symbol: {} (extracted: {}) of kind: {:?}", + symbol.ident, + symbol_name, + match &symbol.kind { + SymbolKind::Var(_) => "Var", + SymbolKind::Const(_) => "Const", + SymbolKind::Fn(_) => "Fn", + SymbolKind::Type(_) => "Type", + } + ); + + // Check prefix match against the extracted name + if prefix.is_empty() || symbol_name.starts_with(prefix) { + let completion_item = self.create_module_completion_member(symbol); + debug!("Adding module member completion: {} (kind: {:?})", completion_item.label, completion_item.kind); + completions.push(completion_item); + } + } + } + } else { + debug!("Module '{}' not found in symbol table", imported_module_ident); + } + + // Sort and filter + self.sort_and_filter_completions(&mut completions, prefix); + + debug!("Found {} module member completions", completions.len()); + completions + } + + /// Provide dot-completions for a module that hasn't been imported yet. + /// + /// When the user types `fmt.` but doesn't have `import fmt`, we look up + /// the module in the workspace index and offer its symbols with an + /// auto-import text edit that inserts `import fmt\n` at the top of the file. + fn get_unimported_module_member_completions(&self, module_name: &str, prefix: &str) -> Vec { + debug!("Trying unimported module member completions for '{}'", module_name); + + let workspace_index = match &self.workspace_index { + Some(idx) => idx, + None => return Vec::new(), + }; + + // Find the module directory. We check: + // 1. std library: $NATURE_ROOT/std// + // 2. local file: /.n + // 3. package sub-dir: // + let std_module_dir = { + let mut p = std::path::PathBuf::from(&self.nature_root); + p.push("std"); + p.push(module_name); + p + }; + let local_file = { + let mut p = std::path::PathBuf::from(&self.module.dir); + p.push(format!("{}.n", module_name)); + p + }; + + // Determine the import statement and which file paths belong to + // this module. + let (import_statement, match_paths): (String, Vec) = if std_module_dir.is_dir() { + // Standard library package — collect all .n files under std// + let mut paths = Vec::new(); + if let Ok(entries) = std::fs::read_dir(&std_module_dir) { + for entry in entries.flatten() { + let p = entry.path(); + if p.is_file() && p.extension().map_or(false, |e| e == "n") { + if let Some(s) = p.to_str() { + paths.push(s.to_string()); + } + } + } + } + (format!("import {}\n", module_name), paths) + } else if local_file.is_file() { + let path_str = local_file.to_str().unwrap_or("").to_string(); + let file_name = format!("{}.n", module_name); + (format!("import '{}'\n", file_name), vec![path_str]) + } else { + return Vec::new(); + }; + + let match_set: HashSet<&str> = match_paths.iter().map(|s| s.as_str()).collect(); + + // Gather all indexed symbols whose file_path belongs to this module. + let mut completions = Vec::new(); + + for (name, indexed_list) in &workspace_index.symbols { + for indexed in indexed_list { + if !match_set.contains(indexed.file_path.as_str()) { + continue; + } + if !prefix.is_empty() && !name.starts_with(prefix) { + continue; + } + + let kind = match indexed.kind { + IndexedSymbolKind::Function => CompletionItemKind::Function, + IndexedSymbolKind::Type => CompletionItemKind::Struct, + IndexedSymbolKind::Variable => CompletionItemKind::Variable, + IndexedSymbolKind::Constant => CompletionItemKind::Constant, + }; + + completions.push(CompletionItem { + label: name.clone(), + kind, + detail: Some(format!("{} (auto-import)", module_name)), + documentation: None, + insert_text: name.clone(), + sort_text: Some(format!("aaa_{}", name)), + additional_text_edits: vec![TextEdit { + line: 0, + character: 0, + new_text: import_statement.clone(), + }], + }); + } + } + + self.sort_and_filter_completions(&mut completions, prefix); + debug!("Found {} unimported module member completions for '{}'", completions.len(), module_name); + completions + } + + /// Collect symbols from selective imports as direct completions. + /// When the user has `import forest.app.configuration.{configuration}`, the symbol + /// `configuration` should appear as a completion in function bodies. + pub(crate) fn collect_selective_import_symbol_completions(&self, prefix: &str, completions: &mut Vec) { + let existing_labels: HashSet = completions.iter().map(|c| c.label.clone()).collect(); + + for import in &self.module.dependencies { + if !import.is_selective { + continue; + } + let Some(ref items) = import.select_items else { continue }; + + // Find the imported module's scope to look up symbols + let imported_scope_id = self.symbol_table.module_scopes.get(&import.module_ident); + + for item in items { + let local_name = item.alias.as_ref().unwrap_or(&item.ident); + + // Check prefix match + if !prefix.is_empty() && !local_name.starts_with(prefix) { + continue; + } + + // Skip if already provided by another completion source + if existing_labels.contains(local_name) { + continue; + } + + // Try to find the symbol in the imported module's scope + let global_ident = crate::utils::format_global_ident(import.module_ident.clone(), item.ident.clone()); + let symbol = self.symbol_table.find_global_symbol(&global_ident) + .or_else(|| { + // Fallback: search in module scope + if let Some(&scope_id) = imported_scope_id { + self.symbol_table.find_symbol(&global_ident, scope_id) + .or_else(|| self.symbol_table.find_symbol(&item.ident, scope_id)) + } else { + None + } + }); + + let (kind, detail, insert_text) = if let Some(symbol) = symbol { + match &symbol.kind { + SymbolKind::Fn(fndef_mutex) => { + let fndef = fndef_mutex.lock().unwrap(); + let sig = self.format_function_signature(&fndef); + let ins = if self.has_parameters(&fndef) { + format!("{}($0)", local_name) + } else { + format!("{}()", local_name) + }; + (CompletionItemKind::Function, format!("fn: {}", sig), ins) + } + SymbolKind::Type(typedef_mutex) => { + let typedef = typedef_mutex.lock().unwrap(); + (CompletionItemKind::Struct, format!("type: {}", typedef.ident), local_name.clone()) + } + SymbolKind::Var(var_mutex) => { + let var = var_mutex.lock().unwrap(); + (CompletionItemKind::Variable, format!("var: {}", var.type_), local_name.clone()) + } + SymbolKind::Const(const_mutex) => { + let const_def = const_mutex.lock().unwrap(); + (CompletionItemKind::Constant, format!("const: {}", const_def.type_), local_name.clone()) + } + } + } else { + // Symbol not found in symbol table; still offer it as a basic completion + (CompletionItemKind::Variable, format!("from {}", import.module_ident), local_name.clone()) + }; + + let module_display = if let Some(ref pkg) = import.ast_package { + pkg.join(".") + } else { + import.module_ident.clone() + }; + + completions.push(CompletionItem { + label: local_name.clone(), + kind, + detail: Some(detail), + documentation: Some(format!("imported from {}", module_display)), + insert_text, + sort_text: Some(format!("aab_{}", local_name)), // High priority (after already-imported modules) + additional_text_edits: Vec::new(), + }); + } + } + } + + /// Collect already-imported modules as completions (show at top) + pub(crate) fn collect_imported_module_completions(&self, prefix: &str, completions: &mut Vec) { + debug!("Collecting imported module completions with prefix '{}'", prefix); + + for import_stmt in &self.module.dependencies { + let module_name = &import_stmt.as_name; + + // Skip selective imports (they don't have a module alias) + if module_name.is_empty() || import_stmt.is_selective { + continue; + } + + // Check prefix match + if !prefix.is_empty() && !module_name.starts_with(prefix) { + continue; + } + + debug!("Found imported module: {}", module_name); + + // Determine import statement display + let import_display = if let Some(file) = &import_stmt.file { + format!("import '{}'", file) + } else if let Some(ref package) = import_stmt.ast_package { + format!("import {}", package.join(".")) + } else { + format!("import {}", module_name) + }; + + completions.push(CompletionItem { + label: module_name.to_string(), + kind: CompletionItemKind::Module, + detail: Some(format!("imported: {}", import_display)), + documentation: Some(format!("Already imported module: {}", module_name)), + insert_text: module_name.to_string(), + sort_text: Some(format!("aaa_{}", module_name)), // Sort to top with "aaa" prefix + additional_text_edits: Vec::new(), + }); + } + } + + /// Collect available module completions (for auto-import) + pub(crate) fn collect_module_completions(&self, prefix: &str, completions: &mut Vec) { + debug!("Collecting module completions with prefix '{}'", prefix); + + // Check which modules are already imported + let already_imported: HashSet = self.module.dependencies.iter().map(|dep| dep.as_name.clone()).collect(); + + // 1. Scan .n files in current directory (file-based imports) + let module_dir = &self.module.dir; + debug!("Scanning module directory: {}", module_dir); + + if let Ok(entries) = std::fs::read_dir(module_dir) { + for entry in entries { + if let Ok(entry) = entry { + let path = entry.path(); + if path.is_file() && path.extension().and_then(|s| s.to_str()) == Some("n") { + if let Some(file_stem) = path.file_stem().and_then(|s| s.to_str()) { + // Skip current file + if path == std::path::Path::new(&self.module.path) { + continue; + } + + // Skip already imported modules + if already_imported.contains(file_stem) { + continue; + } + + // Check prefix match + if !prefix.is_empty() && !file_stem.starts_with(prefix) { + continue; + } + + debug!("Found module file: {}", file_stem); + + // Calculate import statement to insert at beginning of file + let module_name = path.file_name().unwrap().to_str().unwrap(); + let import_statement = format!("import '{}'\n", module_name); + + completions.push(CompletionItem { + label: file_stem.to_string(), + kind: CompletionItemKind::Module, + detail: Some(format!("{}", import_statement)), + documentation: Some(format!("Import module from {}", path.display())), + insert_text: file_stem.to_string(), + sort_text: Some(format!("zzz_{}", file_stem)), // Sort to back + additional_text_edits: vec![TextEdit { + line: 0, + character: 0, + new_text: import_statement, + }], + }); + } + } + } + } + } + + // 2. Scan subdirectories for package-based imports (if package.toml exists) + if let Some(ref pkg_config) = self.package_config { + self.scan_subdirectories_for_modules(&self.project_root, &pkg_config.package_data.name, "", prefix, &already_imported, completions); + } + + // 3. Scan standard library packages + let std_dir = std::path::Path::new(&self.nature_root).join("std"); + debug!("Scanning std directory: {}", std_dir.display()); + + if let Ok(entries) = std::fs::read_dir(&std_dir) { + for entry in entries { + if let Ok(entry) = entry { + let path = entry.path(); + if path.is_dir() { + if let Some(module_name) = path.file_name().and_then(|s| s.to_str()) { + // Exclude special directories + if [".", "..", "builtin"].contains(&module_name) { + continue; + } + + // Skip already imported packages + if already_imported.contains(module_name) { + continue; + } + + // Check prefix match + if !prefix.is_empty() && !module_name.starts_with(prefix) { + continue; + } + + debug!("Found std package: {}", module_name); + + // Standard library uses import package_name syntax + let import_statement = format!("import {}\n", module_name); + + completions.push(CompletionItem { + label: module_name.to_string(), + kind: CompletionItemKind::Module, + detail: Some(format!("std: {}", import_statement)), + documentation: Some(format!("Import standard library package: {}", module_name)), + insert_text: module_name.to_string(), + sort_text: Some(format!("zzz_{}", module_name)), // Sort to back + additional_text_edits: vec![TextEdit { + line: 0, + character: 0, + new_text: import_statement, + }], + }); + } + } + } + } + } + } + + /// Recursively scan subdirectories for package-based module imports + fn scan_subdirectories_for_modules( + &self, + base_dir: &str, + package_name: &str, + current_path: &str, + prefix: &str, + already_imported: &HashSet, + completions: &mut Vec, + ) { + let scan_dir = if current_path.is_empty() { + std::path::PathBuf::from(base_dir) + } else { + std::path::PathBuf::from(base_dir).join(current_path) + }; + + if let Ok(entries) = std::fs::read_dir(&scan_dir) { + for entry in entries { + if let Ok(entry) = entry { + let path = entry.path(); + + // Check subdirectories + if path.is_dir() { + if let Some(dir_name) = path.file_name().and_then(|s| s.to_str()) { + let new_path = if current_path.is_empty() { + dir_name.to_string() + } else { + format!("{}/{}", current_path, dir_name) + }; + + // Recursively scan subdirectory + self.scan_subdirectories_for_modules(base_dir, package_name, &new_path, prefix, already_imported, completions); + } + } + // Check .n files in subdirectories + else if path.is_file() && path.extension().and_then(|s| s.to_str()) == Some("n") { + if let Some(file_stem) = path.file_stem().and_then(|s| s.to_str()) { + // Skip if this is in the root directory (already handled by file-based imports) + if current_path.is_empty() { + continue; + } + + // Build import path: package_name.folder.module_name + let module_import_path = if current_path.is_empty() { + format!("{}.{}", package_name, file_stem) + } else { + format!("{}.{}.{}", package_name, current_path.replace("/", "."), file_stem) + }; + + // Skip already imported modules + if already_imported.contains(file_stem) { + continue; + } + + // Check prefix match (match against the module name, not full path) + if !prefix.is_empty() && !file_stem.starts_with(prefix) { + continue; + } + + debug!("Found subdirectory module: {} at {}", file_stem, module_import_path); + + let import_statement = format!("import {}\n", module_import_path); + + completions.push(CompletionItem { + label: file_stem.to_string(), + kind: CompletionItemKind::Module, + detail: Some(format!("pkg: {}", import_statement)), + documentation: Some(format!("Import module from package: {}", module_import_path)), + insert_text: file_stem.to_string(), + sort_text: Some(format!("zzz_{}", file_stem)), + additional_text_edits: vec![TextEdit { + line: 0, + character: 0, + new_text: import_statement, + }], + }); + } + } + } + } + } + } + + /// Collect cross-file symbol completions from the workspace index. + /// This enables typing e.g. "MyControll" and getting "MyController" from another file, + /// with a selective auto-import so the symbol can be used directly. + pub(crate) fn collect_workspace_symbol_completions(&self, prefix: &str, completions: &mut Vec) { + let workspace_index = match &self.workspace_index { + Some(idx) => idx, + None => return, + }; + + if prefix.is_empty() || prefix.len() < 2 { + return; // Need at least 2 chars to avoid flooding with results + } + + debug!("Collecting workspace symbol completions with prefix '{}'", prefix); + + // Build a set of non-selectively imported module as_names + let non_selective_imported: HashSet = self.module.dependencies + .iter() + .filter(|dep| !dep.is_selective) + .map(|dep| dep.as_name.clone()) + .collect(); + + // Build a map of selectively imported modules: full_path -> &ImportStmt + let selective_imports_by_path: std::collections::HashMap<&str, &ImportStmt> = self.module.dependencies + .iter() + .filter(|dep| dep.is_selective) + .map(|dep| (dep.full_path.as_str(), dep as &ImportStmt)) + .collect(); + + // Check which symbols are already selectively imported (by their visible name) + let selectively_imported: HashSet = self.module.dependencies + .iter() + .filter(|dep| dep.is_selective) + .flat_map(|dep| { + dep.select_items.iter() + .flat_map(|items| items.iter()) + .map(|item| item.alias.clone().unwrap_or_else(|| item.ident.clone())) + }) + .collect(); + + // Collect symbol names already provided by local/module completions to avoid duplication + let existing_labels: HashSet = completions.iter().map(|c| c.label.clone()).collect(); + + let package_name = self.package_config.as_ref().map(|c| c.package_data.name.as_str()); + + let matching_symbols = workspace_index.find_symbols_by_prefix(prefix); + + // First pass: count how many distinct files provide each symbol name + let mut name_sources: std::collections::HashMap> = std::collections::HashMap::new(); + for (i, sym) in matching_symbols.iter().enumerate() { + name_sources.entry(sym.name.clone()).or_default().push(i); + } + + // Build the builtin directory path so we can skip symbols that are + // always in scope (e.g. println, print, …). + let builtin_dir = { + let mut p = std::path::PathBuf::from(&self.nature_root); + p.push("std"); + p.push("builtin"); + p + }; + + for indexed_symbol in &matching_symbols { + // Skip if this symbol is from the current file + if indexed_symbol.file_path == self.module.path { + continue; + } + + // Skip builtin symbols — they are already provided by + // collect_module_scope_fn_completions via the global scope. + if std::path::Path::new(&indexed_symbol.file_path).starts_with(&builtin_dir) { + continue; + } + + // Skip if already selectively imported (already usable directly) + if selectively_imported.contains(&indexed_symbol.name) { + continue; + } + + // Compute how to import the module containing this symbol + let import_info = match workspace_index.compute_import_info( + &indexed_symbol.file_path, + &self.module.dir, + &self.module.path, + &self.project_root, + &self.nature_root, + package_name, + ) { + Some(info) => info, + None => continue, + }; + + // Determine import state: + // 1. Non-selective import exists for this module -> qualified access + // 2. Selective import exists for same file -> extend the {..} list + // 3. Not imported at all -> new selective import line + let has_non_selective = non_selective_imported.contains(&import_info.module_as_name); + let existing_selective = selective_imports_by_path.get(indexed_symbol.file_path.as_str()); + + let kind = match indexed_symbol.kind { + IndexedSymbolKind::Type => CompletionItemKind::Struct, + IndexedSymbolKind::Function => CompletionItemKind::Function, + IndexedSymbolKind::Variable => CompletionItemKind::Variable, + IndexedSymbolKind::Constant => CompletionItemKind::Constant, + }; + + let kind_label = match indexed_symbol.kind { + IndexedSymbolKind::Type => "type", + IndexedSymbolKind::Function => "fn", + IndexedSymbolKind::Variable => "var", + IndexedSymbolKind::Constant => "const", + }; + + let file_stem = std::path::Path::new(&indexed_symbol.file_path) + .file_stem() + .and_then(|s| s.to_str()) + .unwrap_or("") + .to_string(); + + // Check if multiple files provide a symbol with this name + let has_duplicates = name_sources.get(&indexed_symbol.name) + .map(|sources| sources.len() > 1) + .unwrap_or(false); + + // Also check if a local symbol already exists with this name + let conflicts_with_existing = existing_labels.contains(&indexed_symbol.name); + let needs_disambiguation = has_duplicates || conflicts_with_existing; + + // Add () for functions, {} for structs (snippet syntax for direct use) + let direct_insert = match indexed_symbol.kind { + IndexedSymbolKind::Function => format!("{}($0)", indexed_symbol.name), + IndexedSymbolKind::Type => format!("{}{{$0}}", indexed_symbol.name), + _ => indexed_symbol.name.clone(), + }; + + if has_non_selective { + // Case 1: Module already imported non-selectively -> use qualified access + let label = if needs_disambiguation { + format!("{} ({})", indexed_symbol.name, import_info.module_as_name) + } else { + indexed_symbol.name.clone() + }; + + let qualified_insert = match indexed_symbol.kind { + IndexedSymbolKind::Function => format!("{}.{}($0)", import_info.module_as_name, indexed_symbol.name), + IndexedSymbolKind::Type => format!("{}.{}{{$0}}", import_info.module_as_name, indexed_symbol.name), + _ => format!("{}.{}", import_info.module_as_name, indexed_symbol.name), + }; + + completions.push(CompletionItem { + label, + kind, + detail: Some(format!("{} (from {})", kind_label, import_info.module_as_name)), + documentation: Some(format!("from {} ({})", file_stem, indexed_symbol.file_path)), + insert_text: qualified_insert, + sort_text: Some(format!("zzy_{}", indexed_symbol.name)), + additional_text_edits: Vec::new(), + }); + } else if let Some(sel_import) = existing_selective { + // Case 2: Module already has a selective import -> extend the {..} list + // Always disambiguate with source path so user knows where this comes from + let label = if needs_disambiguation { + format!("{} ({})", indexed_symbol.name, file_stem) + } else { + indexed_symbol.name.clone() + }; + + let additional_text_edits = if let Some(edit) = self.find_selective_import_insert_point(sel_import, &indexed_symbol.name) { + vec![edit] + } else { + // Fallback: add a new selective import line + let selective_import = format!("{}.{{{}}}\n", import_info.import_base, indexed_symbol.name); + vec![TextEdit { + line: 0, + character: 0, + new_text: selective_import, + }] + }; + + completions.push(CompletionItem { + label, + kind, + detail: Some(format!("{} (add to import {})", kind_label, import_info.import_base)), + documentation: Some(format!("from {} ({})", file_stem, indexed_symbol.file_path)), + insert_text: direct_insert, + sort_text: Some(format!("zzy_{}", indexed_symbol.name)), + additional_text_edits, + }); + } else { + // Case 3: Module not imported at all -> new selective import line + let selective_import = format!("{}.{{{}}}\n", import_info.import_base, indexed_symbol.name); + + let label = if needs_disambiguation { + format!("{} ({})", indexed_symbol.name, file_stem) + } else { + indexed_symbol.name.clone() + }; + + completions.push(CompletionItem { + label, + kind, + detail: Some(format!("{} (auto import: {})", kind_label, selective_import.trim())), + documentation: Some(format!("from {} ({})", file_stem, indexed_symbol.file_path)), + insert_text: direct_insert, + sort_text: Some(format!("zzy_{}", indexed_symbol.name)), + additional_text_edits: vec![TextEdit { + line: 0, + character: 0, + new_text: selective_import, + }], + }); + } + } + + debug!("Added workspace symbol completions, total now: {}", completions.len()); + } + + /// Find the insert point to add a new symbol to an existing selective import. + /// Returns a TextEdit that inserts ", symbolName" before the closing '}' of the import. + fn find_selective_import_insert_point( + &self, + import_stmt: &ImportStmt, + symbol_name: &str, + ) -> Option { + // start/end are char offsets (from the lexer which works on Vec). + // Use the module's rope to convert char offsets to line/col positions. + let rope = &self.module.rope; + let end = import_stmt.end; + let start = import_stmt.start; + + if end == 0 || end > rope.len_chars() { + return None; + } + + // Search backward from end to find '}' character + let mut brace_char_idx = None; + let mut pos = end; + while pos > start { + pos -= 1; + let ch = rope.char(pos); + if ch == '}' { + brace_char_idx = Some(pos); + break; + } + } + + let brace_char_idx = brace_char_idx?; + + // Convert char offset to line/character using the rope + let line = rope.char_to_line(brace_char_idx); + let line_start_char = rope.line_to_char(line); + let character = brace_char_idx - line_start_char; + + Some(TextEdit { + line, + character, + new_text: format!(", {}", symbol_name), + }) + } +} diff --git a/nls/src/analyzer/completion/members.rs b/nls/src/analyzer/completion/members.rs new file mode 100644 index 00000000..697ddcc6 --- /dev/null +++ b/nls/src/analyzer/completion/members.rs @@ -0,0 +1,621 @@ +use crate::analyzer::common::TypeKind; +use crate::analyzer::symbol::{NodeId, Symbol, SymbolKind}; +use log::debug; +use std::collections::HashSet; + +use super::context::extract_last_ident_part; +use super::{CompletionItem, CompletionItemKind, CompletionProvider}; + +impl<'a> CompletionProvider<'a> { + /// Get auto-completions for type members (for struct fields and methods) + pub fn get_type_member_completions(&self, var_name: &str, prefix: &str, current_scope_id: NodeId) -> Option> { + debug!("Getting type member completions for variable '{}' with prefix '{}'", var_name, prefix); + + // 1. Find the variable in the current scope (try position-based first, then broad search) + let var_symbol = self.find_variable_in_scope(var_name, current_scope_id) + .or_else(|| { + debug!("Variable '{}' not found via scope chain, trying broad search in module", var_name); + self.find_variable_in_all_scopes(var_name, self.module.scope_id) + })?; + + // 2. Get the variable's type + let (var_type_kind, mut typedef_symbol_id, type_ident) = match &var_symbol.kind { + SymbolKind::Var(var_decl) => { + let var = var_decl.lock().unwrap(); + let type_ = &var.type_; + + debug!("Variable '{}' has type: {:?}, symbol_id: {}, ident: '{}'", var_name, type_.kind, type_.symbol_id, type_.ident); + + // Extract the inner type ident for Ref/Ptr-wrapped types + let inner_ident = match &type_.kind { + TypeKind::Ref(inner) | TypeKind::Ptr(inner) => { + if inner.symbol_id != 0 { + // Use inner symbol_id if available + debug!("Ref/Ptr inner type: {:?}, symbol_id: {}, ident: '{}'", inner.kind, inner.symbol_id, inner.ident); + (type_.kind.clone(), inner.symbol_id, inner.ident.clone()) + } else { + (type_.kind.clone(), type_.symbol_id, inner.ident.clone()) + } + } + _ => (type_.kind.clone(), type_.symbol_id, type_.ident.clone()), + }; + + inner_ident + } + _ => { + debug!("Symbol is not a variable"); + return None; + } + }; + + // 2b. Fallback: if typedef_symbol_id is still 0 and we have a type name, try to resolve it + if typedef_symbol_id == 0 && !type_ident.is_empty() { + debug!("typedef_symbol_id is 0, trying to resolve type by name: '{}'", type_ident); + typedef_symbol_id = self.resolve_type_name_to_symbol_id(&type_ident); + if typedef_symbol_id != 0 { + debug!("Resolved type '{}' to symbol_id: {}", type_ident, typedef_symbol_id); + } + } + + let mut completions = Vec::new(); + let mut has_struct_fields = false; + + // 3. Handle direct struct type (inlined) + if let TypeKind::Struct(_, _, properties) = &var_type_kind { + debug!("Variable has direct struct type with {} fields", properties.len()); + has_struct_fields = true; + for prop in properties { + if prefix.is_empty() || prop.name.starts_with(prefix) { + completions.push(CompletionItem { + label: prop.name.clone(), + kind: CompletionItemKind::Variable, + detail: Some(format!("field: {}", prop.type_)), + documentation: None, + insert_text: prop.name.clone(), + sort_text: Some(format!("{:08}", prop.start)), + additional_text_edits: Vec::new(), + }); + } + } + } + + // 4. Handle typedef reference (after reduction, kind may be Struct/Fn/etc, not Ident) + if typedef_symbol_id != 0 { + debug!("Looking up typedef with symbol_id: {}", typedef_symbol_id); + + if let Some(typedef_symbol) = self.symbol_table.get_symbol_ref(typedef_symbol_id) { + if let SymbolKind::Type(typedef) = &typedef_symbol.kind { + let typedef = typedef.lock().unwrap(); + debug!( + "Found typedef: {}, type_expr.kind: {:?}, methods: {}", + typedef.ident, + typedef.type_expr.kind, + typedef.method_table.len() + ); + + // Add struct fields from typedef (only if not already added from inline type) + if !has_struct_fields { + if let TypeKind::Struct(_, _, properties) = &typedef.type_expr.kind { + debug!("Typedef struct has {} fields", properties.len()); + for prop in properties { + if prefix.is_empty() || prop.name.starts_with(prefix) { + completions.push(CompletionItem { + label: prop.name.clone(), + kind: CompletionItemKind::Variable, + detail: Some(format!("field: {}", prop.type_)), + documentation: None, + insert_text: prop.name.clone(), + sort_text: Some(format!("{:08}", prop.start)), + additional_text_edits: Vec::new(), + }); + } + } + } + } + + // Add methods for typedef + for method in typedef.method_table.values() { + let fndef = method.lock().unwrap(); + if prefix.is_empty() || fndef.fn_name.starts_with(prefix) { + let signature = self.format_function_signature(&fndef); + let insert_text = if self.has_parameters(&fndef) { + format!("{}($0)", fndef.fn_name) + } else { + format!("{}()", fndef.fn_name) + }; + completions.push(CompletionItem { + label: fndef.fn_name.clone(), + kind: CompletionItemKind::Function, + detail: Some(format!("fn: {}", signature)), + documentation: None, + insert_text, + sort_text: Some(format!("{:08}", fndef.symbol_start)), + additional_text_edits: Vec::new(), + }); + } + } + } else { + debug!("Symbol {} is not a Type", typedef_symbol_id); + } + } + } + + // 5. Look for methods by searching for functions with impl_type matching this type + // This handles cases like: fn config.helloWorld() + if typedef_symbol_id != 0 { + self.collect_impl_methods(typedef_symbol_id, prefix, &mut completions); + } + + self.sort_and_filter_completions(&mut completions, prefix); + debug!("Found {} type member completions", completions.len()); + + if completions.is_empty() { + None + } else { + Some(completions) + } + } + + /// Get auto-completions for struct fields during initialization (type_name{ field: ... }) + pub fn get_struct_field_completions(&self, type_name: &str, prefix: &str, current_scope_id: NodeId) -> Vec { + debug!("Getting struct field completions for type '{}' with prefix '{}'", type_name, prefix); + + let mut completions = Vec::new(); + + // Find the type symbol + let type_symbol = self.find_type_in_scope(type_name, current_scope_id); + if type_symbol.is_none() { + debug!("Type '{}' not found in scope", type_name); + return completions; + } + + let type_symbol = type_symbol.unwrap(); + + // Extract struct fields from the type + if let SymbolKind::Type(typedef) = &type_symbol.kind { + let typedef = typedef.lock().unwrap(); + + if let TypeKind::Struct(_, _, properties) = &typedef.type_expr.kind { + debug!("Found struct type with {} fields", properties.len()); + + for prop in properties { + if prefix.is_empty() || prop.name.starts_with(prefix) { + completions.push(CompletionItem { + label: prop.name.clone(), + kind: CompletionItemKind::Variable, + detail: Some(format!("{}: {}", prop.name, prop.type_)), + documentation: None, + insert_text: format!("{}: ", prop.name), + sort_text: Some(format!("{:08}", prop.start)), + additional_text_edits: Vec::new(), + }); + } + } + } else { + debug!("Type '{}' is not a struct type", type_name); + } + } + + self.sort_and_filter_completions(&mut completions, prefix); + debug!("Found {} struct field completions", completions.len()); + + completions + } + + /// Collect methods implemented for a type (fn TypeName.method()) + pub(crate) fn collect_impl_methods(&self, typedef_symbol_id: NodeId, prefix: &str, completions: &mut Vec) { + // Search global scope for functions with matching impl_type + // Global symbols are stored in symbol_map (not symbols Vec), so we must check both. + let global_scope = self.symbol_table.find_scope(self.symbol_table.global_scope_id); + + // Collect from symbols Vec + for &symbol_id in &global_scope.symbols { + self.check_impl_method(symbol_id, typedef_symbol_id, prefix, completions); + } + + // Collect from symbol_map (where define_global_symbol stores them) + for (_, &symbol_id) in &global_scope.symbol_map { + self.check_impl_method(symbol_id, typedef_symbol_id, prefix, completions); + } + + // Also check module scope (impl methods are registered there too) + let module_scope = self.symbol_table.find_scope(self.module.scope_id); + for &symbol_id in &module_scope.symbols { + self.check_impl_method(symbol_id, typedef_symbol_id, prefix, completions); + } + for (_, &symbol_id) in &module_scope.symbol_map { + self.check_impl_method(symbol_id, typedef_symbol_id, prefix, completions); + } + } + + /// Check if a symbol is an impl method for the given typedef and add it to completions + fn check_impl_method(&self, symbol_id: NodeId, typedef_symbol_id: NodeId, prefix: &str, completions: &mut Vec) { + let Some(symbol) = self.symbol_table.get_symbol_ref(symbol_id) else { return }; + let SymbolKind::Fn(fndef_mutex) = &symbol.kind else { return }; + let fndef = fndef_mutex.lock().unwrap(); + + // Check if this function's impl_type matches our typedef + if fndef.impl_type.symbol_id != typedef_symbol_id { + return; + } + + // Skip if already in completions (avoid duplicates from symbols + symbol_map overlap) + if completions.iter().any(|c| c.label == fndef.fn_name) { + return; + } + + if prefix.is_empty() || fndef.fn_name.starts_with(prefix) { + debug!("Found impl method: {}", fndef.fn_name); + let signature = self.format_function_signature(&fndef); + let insert_text = if self.has_parameters(&fndef) { + format!("{}($0)", fndef.fn_name) + } else { + format!("{}()", fndef.fn_name) + }; + completions.push(CompletionItem { + label: fndef.fn_name.clone(), + kind: CompletionItemKind::Function, + detail: Some(format!("fn: {}", signature)), + documentation: None, + insert_text, + sort_text: Some(format!("{:08}", fndef.symbol_start)), + additional_text_edits: Vec::new(), + }); + } + } + + /// Find a variable in the current scope and parent scopes + fn find_variable_in_scope(&self, var_name: &str, current_scope_id: NodeId) -> Option<&Symbol> { + debug!("Searching for variable '{}' starting from scope {}", var_name, current_scope_id); + let mut visited_scopes = HashSet::new(); + let mut current = current_scope_id; + + while current > 0 && !visited_scopes.contains(¤t) { + visited_scopes.insert(current); + let scope = self.symbol_table.find_scope(current); + debug!("Checking scope {} with {} symbols", current, scope.symbols.len()); + + // Check symbols Vec + for &symbol_id in &scope.symbols { + if let Some(symbol) = self.symbol_table.get_symbol_ref(symbol_id) { + match &symbol.kind { + SymbolKind::Var(var_decl) => { + let var = var_decl.lock().unwrap(); + debug!("Found var symbol: '{}' (looking for '{}')", var.ident, var_name); + if var.ident == var_name { + drop(var); + debug!("Variable '{}' found in scope {}", var_name, current); + return Some(symbol); + } + } + _ => {} + } + } + } + + // Also check symbol_map (global symbols from imported modules are only stored here) + for (&ref _key, &symbol_id) in &scope.symbol_map { + if let Some(symbol) = self.symbol_table.get_symbol_ref(symbol_id) { + match &symbol.kind { + SymbolKind::Var(var_decl) => { + let var = var_decl.lock().unwrap(); + let symbol_name = extract_last_ident_part(&var.ident); + if symbol_name == var_name { + drop(var); + return Some(symbol); + } + } + _ => {} + } + } + } + + current = scope.parent; + if current == 0 { + break; + } + } + + debug!("Variable '{}' not found in any scope", var_name); + None + } + + /// Broad search: find a variable by name across ALL child scopes of a given scope. + /// Used as fallback when position-based scope resolution fails (e.g., during typing when + /// the parse tree is incomplete and scope ranges are wrong). + fn find_variable_in_all_scopes(&self, var_name: &str, start_scope_id: NodeId) -> Option<&Symbol> { + let mut worklist = vec![start_scope_id]; + let mut visited = HashSet::new(); + + while let Some(scope_id) = worklist.pop() { + if !visited.insert(scope_id) { + continue; + } + let scope = self.symbol_table.find_scope(scope_id); + + // Check symbols in this scope + for &symbol_id in &scope.symbols { + if let Some(symbol) = self.symbol_table.get_symbol_ref(symbol_id) { + if let SymbolKind::Var(var_decl) = &symbol.kind { + let var = var_decl.lock().unwrap(); + if var.ident == var_name { + drop(var); + debug!("Variable '{}' found via broad search in scope {}", var_name, scope_id); + return Some(symbol); + } + } + } + } + + // Check symbol_map + for (_, &symbol_id) in &scope.symbol_map { + if let Some(symbol) = self.symbol_table.get_symbol_ref(symbol_id) { + if let SymbolKind::Var(var_decl) = &symbol.kind { + let var = var_decl.lock().unwrap(); + let symbol_name = extract_last_ident_part(&var.ident); + if symbol_name == var_name { + drop(var); + debug!("Variable '{}' found via broad search in symbol_map of scope {}", var_name, scope_id); + return Some(symbol); + } + } + } + } + + // Add children to worklist + worklist.extend(&scope.children); + } + + None + } + + /// Find a type in the current scope and parent scopes + fn find_type_in_scope(&self, type_name: &str, current_scope_id: NodeId) -> Option<&Symbol> { + debug!("Searching for type '{}' starting from scope {}", type_name, current_scope_id); + let mut visited_scopes = HashSet::new(); + let mut current = current_scope_id; + + while current > 0 && !visited_scopes.contains(¤t) { + visited_scopes.insert(current); + let scope = self.symbol_table.find_scope(current); + debug!("Checking scope {} with {} symbols", current, scope.symbols.len()); + + // Check symbols Vec + for &symbol_id in &scope.symbols { + if let Some(symbol) = self.symbol_table.get_symbol_ref(symbol_id) { + match &symbol.kind { + SymbolKind::Type(typedef) => { + let type_def = typedef.lock().unwrap(); + let symbol_name = extract_last_ident_part(&type_def.ident); + debug!( + "Found type symbol: '{}' (extracted: '{}', looking for '{}')", + type_def.ident, symbol_name, type_name + ); + if symbol_name == type_name { + drop(type_def); + debug!("Type '{}' found in scope {}", type_name, current); + return Some(symbol); + } + } + _ => {} + } + } + } + + // Also check symbol_map (global symbols from imported modules are only stored here) + for (&ref _key, &symbol_id) in &scope.symbol_map { + if let Some(symbol) = self.symbol_table.get_symbol_ref(symbol_id) { + match &symbol.kind { + SymbolKind::Type(typedef) => { + let type_def = typedef.lock().unwrap(); + let symbol_name = extract_last_ident_part(&type_def.ident); + if symbol_name == type_name { + drop(type_def); + debug!("Type '{}' found in symbol_map of scope {}", type_name, current); + return Some(symbol); + } + } + _ => {} + } + } + } + + current = scope.parent; + if current == 0 { + break; + } + } + + debug!("Type '{}' not found in any scope", type_name); + None + } + + /// Resolve a type name (possibly unqualified or partially qualified) to a symbol_id. + /// Tries multiple lookup strategies: direct, module-qualified, and global search. + fn resolve_type_name_to_symbol_id(&self, type_name: &str) -> NodeId { + // 1. Try exact match in the module scope's symbol_map + let module_scope = self.symbol_table.find_scope(self.module.scope_id); + if let Some(&symbol_id) = module_scope.symbol_map.get(type_name) { + return symbol_id; + } + + // 2. Try fully-qualified name: module_ident.type_name + if let Some(symbol_id) = self.symbol_table.find_module_symbol_id(&self.module.ident, type_name) { + return symbol_id; + } + + // 3. Try as-is in global scope (might already be fully qualified) + if let Some(symbol_id) = self.symbol_table.find_symbol_id(type_name, self.symbol_table.global_scope_id) { + return symbol_id; + } + + // 4. Extract last part and try module-qualified (handles "module.type" stored as "type") + let short_name = extract_last_ident_part(type_name); + if short_name != type_name { + if let Some(symbol_id) = self.symbol_table.find_module_symbol_id(&self.module.ident, &short_name) { + return symbol_id; + } + } + + // 5. Search through imports + for import in &self.module.dependencies { + if let Some(symbol_id) = self.symbol_table.find_module_symbol_id(&import.module_ident, &short_name) { + return symbol_id; + } + } + + 0 + } + + /// Collect variable completion items + pub(crate) fn collect_variable_completions(&self, current_scope_id: NodeId, prefix: &str, completions: &mut Vec, position: usize) { + let mut visited_scopes = HashSet::new(); + let mut current = current_scope_id; + + // Traverse upward from current scope + while current > 0 && !visited_scopes.contains(¤t) { + visited_scopes.insert(current); + + let scope = self.symbol_table.find_scope(current); + debug!("Searching scope {} with {} symbols", current, scope.symbols.len()); + + // Iterate through all symbols in current scope + for &symbol_id in &scope.symbols { + if let Some(symbol) = self.symbol_table.get_symbol_ref(symbol_id) { + debug!("Found symbol: ident='{}', prefix='{}', matches={}", symbol.ident, prefix, symbol.ident.starts_with(prefix)); + + // Only handle variables and constants + match &symbol.kind { + SymbolKind::Var(_) | SymbolKind::Const(_) => { + if (prefix.is_empty() || symbol.ident.starts_with(prefix)) && symbol.pos < position { + let completion_item = self.create_completion_item(symbol); + debug!("Adding completion: {}", completion_item.label); + completions.push(completion_item); + } + } + _ => {} + } + } + } + current = scope.parent; + + // If reached root scope, stop traversal + if current == 0 { + break; + } + } + } + + /// Collect functions and types defined at the module scope level. + /// These are not in scope.symbols (only in global_scope.symbol_map), + /// so collect_variable_completions misses them. + /// + /// Also scans the **global scope** where builtin functions (println, + /// print, panic, assert, …) are registered. Builtins use + /// `module_ident = ""` which maps to the global scope. + pub(crate) fn collect_module_scope_fn_completions(&self, prefix: &str, completions: &mut Vec) { + let module_scope_id = self.module.scope_id; + let global_scope_id = self.symbol_table.global_scope_id; + + let existing_labels: HashSet = completions.iter().map(|c| c.label.clone()).collect(); + + // Closure that inspects one symbol and maybe pushes a completion. + let mut check_symbol = |symbol_id: NodeId, is_builtin: bool| { + let Some(symbol) = self.symbol_table.get_symbol_ref(symbol_id) else { return }; + let symbol_name = extract_last_ident_part(&symbol.ident); + + if !prefix.is_empty() && !symbol_name.starts_with(prefix) { + return; + } + if existing_labels.contains(&symbol_name) { + return; + } + + match &symbol.kind { + SymbolKind::Fn(fndef_mutex) => { + let fndef = fndef_mutex.lock().unwrap(); + // Skip test functions and impl methods + if fndef.is_test || fndef.impl_type.kind.is_exist() { + return; + } + let signature = self.format_function_signature(&fndef); + let insert_text = if self.has_parameters(&fndef) { + format!("{}($0)", fndef.fn_name) + } else { + format!("{}()", fndef.fn_name) + }; + let detail = if is_builtin { + format!("fn: {} (builtin)", signature) + } else { + format!("fn: {}", signature) + }; + completions.push(CompletionItem { + label: fndef.fn_name.clone(), + kind: CompletionItemKind::Function, + detail: Some(detail), + documentation: None, + insert_text, + sort_text: Some(format!("{:08}", fndef.symbol_start)), + additional_text_edits: Vec::new(), + }); + } + SymbolKind::Type(typedef_mutex) => { + // Skip builtin types when prefix is empty — too noisy + if is_builtin && prefix.is_empty() { + return; + } + let typedef = typedef_mutex.lock().unwrap(); + completions.push(CompletionItem { + label: symbol_name.clone(), + kind: CompletionItemKind::Struct, + detail: Some(format!("type: {}", typedef.ident)), + documentation: None, + insert_text: symbol_name, + sort_text: Some(format!("{:08}", typedef.symbol_start)), + additional_text_edits: Vec::new(), + }); + } + _ => {} + } + }; + + // 1. Current module scope + let module_scope = self.symbol_table.find_scope(module_scope_id); + for &symbol_id in &module_scope.symbols { + check_symbol(symbol_id, false); + } + let module_symbol_map: Vec = module_scope.symbol_map.values().copied().collect(); + for symbol_id in module_symbol_map { + check_symbol(symbol_id, false); + } + + // 2. Global scope (builtins — println, print, panic, assert, …) + // Only include builtins when the user has typed a prefix; with an + // empty prefix we limit noise to local-scope items only. + debug!( + "global_scope_id={}, module_scope_id={}, prefix='{}', will_scan_global={}", + global_scope_id, + module_scope_id, + prefix, + global_scope_id != module_scope_id && !prefix.is_empty() + ); + if global_scope_id != module_scope_id && !prefix.is_empty() { + let global_scope = self.symbol_table.find_scope(global_scope_id); + debug!( + "global scope: {} symbols, {} symbol_map entries", + global_scope.symbols.len(), + global_scope.symbol_map.len() + ); + for &symbol_id in &global_scope.symbols { + if let Some(sym) = self.symbol_table.get_symbol_ref(symbol_id) { + debug!(" global symbol: id={}, ident='{}', kind={:?}", symbol_id, sym.ident, std::mem::discriminant(&sym.kind)); + } + check_symbol(symbol_id, true); + } + let global_symbol_map: Vec = global_scope.symbol_map.values().copied().collect(); + for symbol_id in global_symbol_map { + check_symbol(symbol_id, true); + } + } + } +} diff --git a/nls/src/analyzer/completion/mod.rs b/nls/src/analyzer/completion/mod.rs new file mode 100644 index 00000000..61be14aa --- /dev/null +++ b/nls/src/analyzer/completion/mod.rs @@ -0,0 +1,416 @@ +mod context; +mod imports; +mod members; + +pub use context::{ + extract_last_ident_part_pub, extract_module_member_context, extract_prefix_at_position, + extract_selective_import_context, extract_struct_init_context, +}; + +use crate::analyzer::common::AstFnDef; +use crate::analyzer::symbol::{NodeId, Symbol, SymbolKind, SymbolTable}; +use crate::analyzer::workspace_index::WorkspaceIndex; +use crate::project::Module; +use log::debug; + +use context::extract_last_ident_part; + +#[derive(Debug, Clone)] +pub struct CompletionItem { + pub label: String, // Variable name + pub kind: CompletionItemKind, + pub detail: Option, // Type information + pub documentation: Option, + pub insert_text: String, // Insert text + pub sort_text: Option, // Sort priority + pub additional_text_edits: Vec, // Additional text edits (for auto-import) +} + +#[derive(Debug, Clone)] +pub struct TextEdit { + pub line: usize, + pub character: usize, + pub new_text: String, +} + +#[derive(Debug, Clone)] +pub enum CompletionItemKind { + Variable, + Parameter, + Function, + Constant, + Module, // Module type for imports + Struct, // Type definitions (structs, typedefs) + Keyword, // Language keywords +} + +pub struct CompletionProvider<'a> { + pub(crate) symbol_table: &'a mut SymbolTable, + pub(crate) module: &'a mut Module, + pub(crate) nature_root: String, + pub(crate) project_root: String, + pub(crate) package_config: Option, + pub(crate) workspace_index: Option<&'a WorkspaceIndex>, +} + +impl<'a> CompletionProvider<'a> { + pub fn new( + symbol_table: &'a mut SymbolTable, + module: &'a mut Module, + nature_root: String, + project_root: String, + package_config: Option, + ) -> Self { + Self { + symbol_table, + module, + nature_root, + project_root, + package_config, + workspace_index: None, + } + } + + pub fn with_workspace_index(mut self, index: &'a WorkspaceIndex) -> Self { + self.workspace_index = Some(index); + self + } + + /// Main auto-completion entry function + pub fn get_completions(&self, position: usize, text: &str) -> Vec { + debug!("get_completions at position={}, module='{}', scope={}", position, &self.module.ident, self.module.scope_id); + + // Check if in selective import context (import module.{cursor}) + if let Some((module_path, item_prefix)) = extract_selective_import_context(text, position) { + debug!("Detected selective import context: module='{}', prefix='{}'", module_path, item_prefix); + return self.get_selective_import_completions(&module_path, &item_prefix); + } + + // Check if in struct initialization context (type_name{ field: ... }) + if let Some((type_name, field_prefix)) = extract_struct_init_context(text, position) { + debug!("Detected struct initialization context: type='{}', field_prefix='{}'", type_name, field_prefix); + let current_scope_id = self.find_innermost_scope(self.module.scope_id, position); + let struct_completions = self.get_struct_field_completions(&type_name, &field_prefix, current_scope_id); + if !struct_completions.is_empty() { + return struct_completions; + } + // Fall through to try other completion types when the detected + // "struct init" context doesn't actually resolve to any fields. + } + + let prefix = extract_prefix_at_position(text, position); + + // Check if in type member access context (variable.field) + if let Some((var_name, member_prefix)) = extract_module_member_context(&prefix, position) { + // First try as variable type member access + let current_scope_id = self.find_innermost_scope(self.module.scope_id, position); + if let Some(completions) = self.get_type_member_completions(&var_name, &member_prefix, current_scope_id) { + debug!("Found type member completions for variable '{}'", var_name); + return completions; + } + + // If not a variable, try as module member access + debug!("Detected module member access: {} and {}", var_name, member_prefix); + return self.get_module_member_completions(&var_name, &member_prefix); + } + + // Normal variable completion + debug!("Getting completions at position {} with prefix '{}'", position, prefix); + + // 1. Find current scope based on position + let current_scope_id = self.find_innermost_scope(self.module.scope_id, position); + debug!("Found scope_id {} by positon {}, module.scope_id={}", current_scope_id, position, self.module.scope_id); + + // cannot auto-import in global module scope + if current_scope_id == self.module.scope_id { + debug!("cursor is at module scope, returning empty"); + return Vec::new(); + } + + let has_prefix = !prefix.is_empty(); + debug!("prefix='{}', has_prefix={}", prefix, has_prefix); + + // 2. Collect all visible variable symbols + let mut completions = Vec::new(); + self.collect_variable_completions(current_scope_id, &prefix, &mut completions, position); + debug!("after collect_variable_completions: {} items", completions.len()); + + // 2b. Collect functions/types defined at the current module scope + self.collect_module_scope_fn_completions(&prefix, &mut completions); + debug!("after collect_module_scope_fn_completions: {} items, labels: {:?}", + completions.len(), + completions.iter().map(|c| c.label.clone()).collect::>() + ); + + // 2c. Collect symbols from selective imports (import module.{symbol1, symbol2}) + self.collect_selective_import_symbol_completions(&prefix, &mut completions); + + // 3. Collect already-imported modules — always shown (they're in + // scope and useful even on an empty-prefix Cmd+Enter trigger). + self.collect_imported_module_completions(&prefix, &mut completions); + + // 4. Collect available modules (for auto-import) — always shown, + // matching gopls which lists std packages on empty prefix. + self.collect_module_completions(&prefix, &mut completions); + + // The remaining sources (workspace symbols, keywords) are only + // useful when the user has started typing. + if has_prefix { + // 5. Collect cross-file symbol completions (workspace index) + self.collect_workspace_symbol_completions(&prefix, &mut completions); + + // 6. Collect keyword completions + self.collect_keyword_completions(&prefix, &mut completions); + } + + // 7. Sort and filter + self.sort_and_filter_completions(&mut completions, &prefix); + + debug!("Found {} completions", completions.len()); + + completions + } + + /// Check if function has parameters (excluding self) + pub(crate) fn has_parameters(&self, fndef: &AstFnDef) -> bool { + fndef.params.iter().any(|param| { + let param_locked = param.lock().unwrap(); + param_locked.ident != "self" + }) + } + + /// Format a function signature for display + pub(crate) fn format_function_signature(&self, fndef: &AstFnDef) -> String { + let mut params_str = String::new(); + let mut first = true; + + for param in fndef.params.iter() { + let param_locked = param.lock().unwrap(); + + // Skip 'self' parameter + if param_locked.ident == "self" { + continue; + } + + if !first { + params_str.push_str(", "); + } + first = false; + + // Use the ident field from Type which contains the original type name + let type_str = if !param_locked.type_.ident.is_empty() { + param_locked.type_.ident.clone() + } else { + param_locked.type_.to_string() + }; + + params_str.push_str(&format!("{} {}", type_str, param_locked.ident)); + } + + if fndef.rest_param && !params_str.is_empty() { + params_str.push_str(", ..."); + } + + let return_type = fndef.return_type.to_string(); + format!("fn({}): {}", params_str, return_type) + } + + /// Find innermost scope containing the position starting from module scope + pub(crate) fn find_innermost_scope(&self, scope_id: NodeId, position: usize) -> NodeId { + let scope = self.symbol_table.find_scope(scope_id); + debug!("[find_innermost_scope] scope_id {}, start {}, end {}", scope_id, scope.range.0, scope.range.1); + + // Check if current scope contains this position (range.1 == 0 means file-level scope) + if position >= scope.range.0 && (position < scope.range.1 || scope.range.1 == 0) { + // Check child scopes, find innermost scope + for &child_id in &scope.children { + let child_scope = self.symbol_table.find_scope(child_id); + + debug!( + "[find_innermost_scope] child scope_id {}, start {}, end {}", + scope_id, child_scope.range.0, child_scope.range.1 + ); + if position >= child_scope.range.0 && position < child_scope.range.1 { + return self.find_innermost_scope(child_id, position); + } + } + + return scope_id; + } + + scope_id // If not in range, return current scope + } + + /// Create completion item + pub(crate) fn create_completion_item(&self, symbol: &Symbol) -> CompletionItem { + let (kind, detail) = match &symbol.kind { + SymbolKind::Var(var_decl) => { + let detail = { + let var = var_decl.lock().unwrap(); + format!("var: {}", var.type_) + }; + (CompletionItemKind::Variable, Some(detail)) + } + SymbolKind::Const(const_def) => { + let detail = { + let const_val = const_def.lock().unwrap(); + format!("const: {}", const_val.type_) + }; + (CompletionItemKind::Constant, Some(detail)) + } + _ => (CompletionItemKind::Variable, None), + }; + + CompletionItem { + label: symbol.ident.clone(), + kind, + detail, + documentation: None, + insert_text: symbol.ident.clone(), + sort_text: Some(format!("{:08}", symbol.pos)), // Sort by definition position + additional_text_edits: Vec::new(), + } + } + + /// Create module member completion item + pub(crate) fn create_module_completion_member(&self, symbol: &Symbol) -> CompletionItem { + let (ident, kind, detail, insert_text, priority) = match &symbol.kind { + SymbolKind::Var(var) => { + let var = var.lock().unwrap(); + let detail = format!("var: {}", var.type_); + let display_ident = extract_last_ident_part(&var.ident.clone()); + (display_ident.clone(), CompletionItemKind::Variable, Some(detail), display_ident, 2) + } + SymbolKind::Const(constdef) => { + let constdef = constdef.lock().unwrap(); + let detail = format!("const: {}", constdef.type_); + let display_ident = extract_last_ident_part(&constdef.ident.clone()); + (display_ident.clone(), CompletionItemKind::Constant, Some(detail), display_ident, 3) + } + SymbolKind::Fn(fndef) => { + let fndef = fndef.lock().unwrap(); + let signature = self.format_function_signature(&fndef); + let insert_text = if self.has_parameters(&fndef) { + format!("{}($0)", fndef.fn_name) + } else { + format!("{}()", fndef.fn_name) + }; + (fndef.fn_name.clone(), CompletionItemKind::Function, Some(signature), insert_text, 0) + } + SymbolKind::Type(typedef) => { + let typedef = typedef.lock().unwrap(); + let detail = format!("type definition"); + let display_ident = extract_last_ident_part(&typedef.ident); + (display_ident.clone(), CompletionItemKind::Struct, Some(detail), display_ident, 1) + } + }; + + CompletionItem { + label: ident.clone(), + kind, + detail, + documentation: None, + insert_text, + sort_text: Some(format!("{}_{}", priority, ident)), + additional_text_edits: Vec::new(), + } + } + + /// Collect keyword completions matching the current prefix. + pub(crate) fn collect_keyword_completions(&self, prefix: &str, completions: &mut Vec) { + if prefix.is_empty() { + return; + } + + const KEYWORDS: &[(&str, &str, &str)] = &[ + ("fn", "fn name($0) {\n\t\n}", "Function definition"), + ("if", "if $0 {\n\t\n}", "If statement"), + ("else", "else {\n\t$0\n}", "Else clause"), + ("for", "for $0 {\n\t\n}", "For loop"), + ("var", "var $0", "Variable declaration"), + ("let", "let $0", "Let binding (error unwrap)"), + ("return", "return $0", "Return statement"), + ("import", "import $0", "Import module"), + ("type", "type $0 = ", "Type definition"), + ("match", "match $0 {\n\t\n}", "Match expression"), + ("continue", "continue", "Continue loop"), + ("break", "break", "Break loop"), + ("as", "as $0", "Type cast"), + ("is", "is $0", "Type test"), + ("in", "in $0", "In operator"), + ("true", "true", "Boolean true"), + ("false", "false", "Boolean false"), + ("null", "null", "Null value"), + ("throw", "throw $0", "Throw error"), + ("try", "try {\n\t$0\n} catch err {\n\t\n}", "Try-catch block"), + ("catch", "catch $0 {\n\t\n}", "Catch clause"), + ("go", "go $0", "Spawn coroutine"), + ("select", "select {\n\t$0\n}", "Select statement"), + ]; + + let lower_prefix = prefix.to_lowercase(); + for &(kw, snippet, detail) in KEYWORDS { + if kw.starts_with(&lower_prefix) && kw != lower_prefix { + completions.push(CompletionItem { + label: kw.to_string(), + kind: CompletionItemKind::Keyword, + detail: Some(detail.to_string()), + documentation: None, + insert_text: snippet.to_string(), + sort_text: Some(format!("90_{}", kw)), // low priority so symbols come first + additional_text_edits: Vec::new(), + }); + } + } + } + + /// Sort and filter completion items + pub(crate) fn sort_and_filter_completions(&self, completions: &mut Vec, prefix: &str) { + // Deduplicate - based on label + completions.sort_by(|a, b| a.label.cmp(&b.label)); + completions.dedup_by(|a, b| a.label == b.label); + + // Sort by: 1) kind priority, 2) prefix match, 3) alphabetically + completions.sort_by(|a, b| { + // Priority order: Function > Struct > Variable > Constant > Module + let a_priority = match a.kind { + CompletionItemKind::Function => 0, + CompletionItemKind::Struct => 1, + CompletionItemKind::Variable | CompletionItemKind::Parameter => 2, + CompletionItemKind::Constant => 3, + CompletionItemKind::Module => 4, + CompletionItemKind::Keyword => 5, + }; + let b_priority = match b.kind { + CompletionItemKind::Function => 0, + CompletionItemKind::Struct => 1, + CompletionItemKind::Variable | CompletionItemKind::Parameter => 2, + CompletionItemKind::Constant => 3, + CompletionItemKind::Module => 4, + CompletionItemKind::Keyword => 5, + }; + + // First sort by kind priority + match a_priority.cmp(&b_priority) { + std::cmp::Ordering::Equal => { + // Then by prefix match + let a_exact = a.label.starts_with(prefix); + let b_exact = b.label.starts_with(prefix); + + match (a_exact, b_exact) { + (true, false) => std::cmp::Ordering::Less, + (false, true) => std::cmp::Ordering::Greater, + _ => { + // Finally sort alphabetically + a.label.cmp(&b.label) + } + } + } + other => other, + } + }); + + // Limit number of results + completions.truncate(50); + } +} diff --git a/nls/src/analyzer/flow.rs b/nls/src/analyzer/flow.rs index 89d5cb70..7004f755 100644 --- a/nls/src/analyzer/flow.rs +++ b/nls/src/analyzer/flow.rs @@ -34,7 +34,8 @@ impl<'a> Flow<'a> { start: fndef.symbol_end, end: fndef.symbol_end, message: "missing return".to_string(), - }); + is_warning: false, + }); } } @@ -117,7 +118,8 @@ impl<'a> Flow<'a> { end: expr.end, start: expr.end, message: "missing ret".to_string(), - }); + is_warning: false, + }); } // has ret 属于当前 match, 离开当前 match 后,ret 作废 diff --git a/nls/src/analyzer/generics.rs b/nls/src/analyzer/generics.rs index d1f4e400..0e79e68d 100644 --- a/nls/src/analyzer/generics.rs +++ b/nls/src/analyzer/generics.rs @@ -37,7 +37,7 @@ impl<'a> Generics<'a> { } fn push_error(&mut self, start: usize, end: usize, message: String) { - errors_push(self.module, AnalyzerError { start, end, message }); + errors_push(self.module, AnalyzerError { start, end, message, is_warning: false }); } fn find_generics_param<'b>(&self, fndef: &'b AstFnDef, ident: &str) -> Option<&'b GenericsParam> { diff --git a/nls/src/analyzer/global_eval.rs b/nls/src/analyzer/global_eval.rs index 4fa5663f..b2a6e466 100644 --- a/nls/src/analyzer/global_eval.rs +++ b/nls/src/analyzer/global_eval.rs @@ -79,7 +79,8 @@ impl<'a> GlobalEval<'a> { start: symbol_start, end: symbol_end, message: "cannot assign to void".to_string(), - }); + is_warning: false, + }); } let right_type = typesys.infer_right_expr(right_expr, var_type.clone())?; @@ -88,7 +89,8 @@ impl<'a> GlobalEval<'a> { start: right_expr.start, end: right_expr.end, message: "cannot assign void to global var".to_string(), - }); + is_warning: false, + }); } if var_type.kind.is_unknown() { @@ -97,7 +99,8 @@ impl<'a> GlobalEval<'a> { start: right_expr.start, end: right_expr.end, message: format!("global var {} type infer failed, right expr cannot confirm", var_ident), - }); + is_warning: false, + }); } var_type = right_type; } @@ -108,7 +111,8 @@ impl<'a> GlobalEval<'a> { start: right_expr.start, end: right_expr.end, message: "global type not confirmed".to_string(), - }); + is_warning: false, + }); } { @@ -418,6 +422,7 @@ impl<'a> GlobalEval<'a> { start: expr.start, end: expr.end, message, + is_warning: false, } } } diff --git a/nls/src/analyzer/lexer.rs b/nls/src/analyzer/lexer.rs index e9a5c834..866feba3 100644 --- a/nls/src/analyzer/lexer.rs +++ b/nls/src/analyzer/lexer.rs @@ -642,7 +642,8 @@ impl Lexer { start: self.offset, end: self.guard, message: String::from("Unterminated comment"), - }); + is_warning: false, + }); return; // 直接返回,避免 advance 溢出 } @@ -700,7 +701,8 @@ impl Lexer { start: self.offset, end: self.guard, message: String::from("floating-point numbers cannot end with '.'"), - }); + is_warning: false, + }); return false; } @@ -718,7 +720,8 @@ impl Lexer { start: self.offset, end: self.guard, message: String::from("floating-point number contains multiple '.'"), - }); + is_warning: false, + }); return false; } @@ -1019,7 +1022,8 @@ impl Lexer { start: self.offset, end: self.guard, message: String::from("Unexpected character"), - }); + is_warning: false, + }); TokenType::Unknown } } @@ -1042,7 +1046,8 @@ impl Lexer { start: self.offset, end: self.guard, message: String::from("string not terminated"), - }); + is_warning: false, + }); return result; } @@ -1066,7 +1071,8 @@ impl Lexer { start: self.offset, end: self.guard, message: String::from("string not terminated"), - }); + is_warning: false, + }); return result; // 返回已经解析的字符串 } } @@ -1094,7 +1100,8 @@ impl Lexer { start: self.offset, end: self.guard + 1, message: String::from("incomplete hex escape sequence"), - }); + is_warning: false, + }); guard_char } else { let hex_chars: String = self.source[self.guard + 1..self.guard + 3].iter().collect(); @@ -1110,7 +1117,8 @@ impl Lexer { start: self.offset, end: self.guard + 3, message: format!("invalid hex escape sequence \\x{}", hex_chars), - }); + is_warning: false, + }); guard_char } } @@ -1119,7 +1127,8 @@ impl Lexer { start: self.offset, end: self.guard + 3, message: format!("invalid hex escape sequence \\x{}", hex_chars), - }); + is_warning: false, + }); guard_char } } @@ -1129,7 +1138,8 @@ impl Lexer { start: self.offset, end: self.guard + 1, message: format!("unknown escape char '{}'", guard_char), - }); + is_warning: false, + }); guard_char } }; @@ -1144,7 +1154,8 @@ impl Lexer { start: self.offset, end: self.guard, message: String::from("string not terminated"), - }); + is_warning: false, + }); return result; } } diff --git a/nls/src/analyzer/semantic/declarations.rs b/nls/src/analyzer/semantic/declarations.rs new file mode 100644 index 00000000..f0eab16e --- /dev/null +++ b/nls/src/analyzer/semantic/declarations.rs @@ -0,0 +1,475 @@ +use log::debug; + +use crate::utils::errors_push; + +use super::super::common::*; +use super::super::symbol::{ScopeKind, SymbolKind}; +use super::Semantic; +use std::sync::{Arc, Mutex}; + +impl<'a> Semantic<'a> { + pub fn analyze_global_fn(&mut self, fndef_mutex: Arc>) { + { + let mut fndef = fndef_mutex.lock().unwrap(); + + fndef.is_local = false; + fndef.module_index = self.module.index; + if fndef.generics_params.is_some() { + fndef.is_generics = true; + } + + if fndef.is_tpl && fndef.body.stmts.len() == 0 { + debug_assert!(fndef.body.stmts.len() == 0); + } + + self.analyze_type(&mut fndef.return_type); + // Resolve impl_type so its ident becomes module-qualified (e.g. "module.Dog") + // and symbol_id is set. This is required for symbol_typedef_add_method to + // locate the typedef in the global scope. + if fndef.impl_type.kind.is_exist() { + self.analyze_type(&mut fndef.impl_type); + } + + // 如果 impl type 是 type alias, 则从符号表中获取当前的 type alias 的全称进行更新 + // fn vec.len() -> fn vec_len(vec self) + // impl 是 type alias 时,只能是 fn person_t.len() 而不能是 fn pkg.person_t.len() + if fndef.impl_type.kind.is_exist() { + if !fndef.is_static && fndef.self_kind != SelfKind::Null { + // 重构 params 的位置, 新增 self param + let mut new_params = Vec::new(); + let param_type = fndef.impl_type.clone(); + let self_vardecl = VarDeclExpr { + ident: String::from("self"), + type_: param_type, + be_capture: false, + heap_ident: None, + symbol_start: fndef.symbol_start, + symbol_end: fndef.symbol_end, + symbol_id: 0, + is_private: false, + }; + + new_params.push(Arc::new(Mutex::new(self_vardecl))); + new_params.extend(fndef.params.iter().cloned()); + fndef.params = new_params; + + // builtin type 没有注册在符号表,不能添加 method + if !Type::is_impl_builtin_type(&fndef.impl_type.kind) { + self.symbol_typedef_add_method(fndef.impl_type.ident.clone(), fndef.symbol_name.clone(), fndef_mutex.clone()) + .unwrap_or_else(|e| { + errors_push( + self.module, + AnalyzerError { + start: fndef.symbol_start, + end: fndef.symbol_end, + message: e, + is_warning: false, + }, + ); + }); + } + } + } + + self.enter_scope(ScopeKind::GlobalFn(fndef_mutex.clone()), fndef.symbol_start, fndef.symbol_end); + + // 函数形参处理 + for param_mutex in &fndef.params { + let mut param = param_mutex.lock().unwrap(); + self.analyze_type(&mut param.type_); + + // 将参数添加到符号表中 + match self.symbol_table.define_symbol_in_scope( + param.ident.clone(), + SymbolKind::Var(param_mutex.clone()), + param.symbol_start, + self.current_scope_id, + ) { + Ok(symbol_id) => { + param.symbol_id = symbol_id; + } + Err(e) => { + errors_push( + self.module, + AnalyzerError { + start: param.symbol_start, + end: param.symbol_end, + message: e, + is_warning: false, + }, + ); + } + } + } + } + + { + let mut body = { + let mut fndef = fndef_mutex.lock().unwrap(); + std::mem::take(&mut fndef.body) + }; + + if body.stmts.len() > 0 { + self.analyze_body(&mut body); + } + + // 将当前的 fn 添加到 global fn 的 local_children 中 + { + let mut fndef = fndef_mutex.lock().unwrap(); + + // 归还 body + fndef.body = body; + fndef.local_children = self.current_local_fn_list.clone(); + } + } + + // 清空 self.current_local_fn_list, 进行重新计算 + self.current_local_fn_list.clear(); + + self.exit_scope(); + } + + /** + * local fn in global fn + */ + pub fn analyze_local_fndef(&mut self, fndef_mutex: &Arc>) { + self.module.all_fndefs.push(fndef_mutex.clone()); + + let mut fndef = fndef_mutex.lock().unwrap(); + + // find global fn in symbol table + let Some(global_fn_mutex) = self.symbol_table.find_global_fn(self.current_scope_id) else { + return; + }; + fndef.global_parent = Some(global_fn_mutex.clone()); + fndef.is_local = true; + + self.current_local_fn_list.push(fndef_mutex.clone()); + + // local fn 作为闭包函数, 不能进行类型扩展和泛型参数 + if fndef.impl_type.kind.is_exist() || fndef.generics_params.is_some() { + errors_push( + self.module, + AnalyzerError { + start: fndef.symbol_start, + end: fndef.symbol_end, + message: "closure fn cannot be generics or impl type alias".to_string(), + is_warning: false, + }, + ); + } + + // 闭包不能包含 macro ident + if fndef.linkid.is_some() { + errors_push( + self.module, + AnalyzerError { + start: fndef.symbol_start, + end: fndef.symbol_end, + message: "closure fn cannot have #linkid label".to_string(), + is_warning: false, + }, + ); + } + + if fndef.is_tpl { + errors_push( + self.module, + AnalyzerError { + start: fndef.symbol_start, + end: fndef.symbol_end, + message: "closure fn cannot be template".to_string(), + is_warning: false, + }, + ); + } + + self.analyze_type(&mut fndef.return_type); + + self.enter_scope(ScopeKind::LocalFn(fndef_mutex.clone()), fndef.symbol_start, fndef.symbol_end); + + // 形参处理 + for param_mutex in &fndef.params { + let mut param = param_mutex.lock().unwrap(); + self.analyze_type(&mut param.type_); + + // 将参数添加到符号表中 + match self.symbol_table.define_symbol_in_scope( + param.ident.clone(), + SymbolKind::Var(param_mutex.clone()), + param.symbol_start, + self.current_scope_id, + ) { + Ok(symbol_id) => { + param.symbol_id = symbol_id; + } + Err(e) => { + errors_push( + self.module, + AnalyzerError { + start: param.symbol_start, + end: param.symbol_end, + message: e, + is_warning: false, + }, + ); + } + } + } + + // handle body + self.analyze_body(&mut fndef.body); + + let mut free_var_count = 0; + let scope = self.symbol_table.find_scope(self.current_scope_id); + for (_, free_ident) in scope.frees.iter() { + if matches!(free_ident.kind, SymbolKind::Var(..)) { + free_var_count += 1; + } + } + + self.exit_scope(); + + // 当前函数需要编译成闭包, 所有的 call fn 改造成 call fn_var + if free_var_count > 0 { + fndef.is_closure = true; + } + + // 将 fndef lambda 添加到 symbol table 中 + match self.symbol_table.define_symbol_in_scope( + fndef.symbol_name.clone(), + SymbolKind::Fn(fndef_mutex.clone()), + fndef.symbol_start, + self.current_scope_id, + ) { + Ok(symbol_id) => { + fndef.symbol_id = symbol_id; + } + Err(e) => { + errors_push( + self.module, + AnalyzerError { + start: fndef.symbol_start, + end: fndef.symbol_end, + message: e, + is_warning: false, + }, + ); + } + } + } + + /// Infer variable type from right-hand expression when type is unknown. + /// Handles function calls by looking up the called function's return type. + pub(crate) fn infer_var_type_from_expr(&mut self, var_decl_mutex: &Arc>, expr: &Box) { + let needs_inference = { + let var_decl = var_decl_mutex.lock().unwrap(); + var_decl.type_.kind.is_unknown() + }; + + if !needs_inference { + return; + } + + // Try to infer type from the expression + if let Some(mut inferred_type) = self.infer_type_from_expr(expr) { + // Resolve unresolved type idents (e.g., return type "testing" that hasn't been analyzed yet) + self.resolve_inferred_type(&mut inferred_type); + + debug!("Inferred type for variable: {:?}, symbol_id: {}", inferred_type.kind, inferred_type.symbol_id); + let mut var_decl = var_decl_mutex.lock().unwrap(); + var_decl.type_ = inferred_type; + } + } + + /// Resolve an unresolved type ident in an inferred type. + /// Handles direct Ident types and types wrapped in Ref/Ptr. + fn resolve_inferred_type(&mut self, t: &mut Type) { + use crate::analyzer::common::TypeKind; + + // Handle unreduced ref/ptr stored as TypeKind::Ident with args. + // Convert them to proper TypeKind::Ref/Ptr before resolving. + if t.kind == TypeKind::Ident + && !t.args.is_empty() + && (t.ident == "ref" || t.ident == "ptr") + { + let mut inner = t.args.remove(0); + self.resolve_inferred_type(&mut inner); + if t.ident == "ref" { + t.kind = TypeKind::Ref(Box::new(inner.clone())); + } else { + t.kind = TypeKind::Ptr(Box::new(inner.clone())); + } + // Propagate symbol_id from inner for completion/hover lookup + if t.symbol_id == 0 && inner.symbol_id != 0 { + t.symbol_id = inner.symbol_id; + t.ident = inner.ident.clone(); + if t.ident_kind == TypeIdentKind::Unknown { + t.ident_kind = inner.ident_kind.clone(); + } + } + return; + } + + match &mut t.kind { + TypeKind::Ident => { + if t.symbol_id == 0 && !t.ident.is_empty() { + if let Some(symbol_id) = self.resolve_typedef(&mut t.ident) { + t.symbol_id = symbol_id; + if t.ident_kind == TypeIdentKind::Unknown { + t.ident_kind = TypeIdentKind::Def; + } + } else { + // Fallback: try direct global lookup (ident may already be + // fully qualified from a cross-module return type). + if let Some(symbol_id) = self.symbol_table.find_symbol_id(&t.ident, self.symbol_table.global_scope_id) { + t.symbol_id = symbol_id; + if t.ident_kind == TypeIdentKind::Unknown { + t.ident_kind = TypeIdentKind::Def; + } + } else { + // Last resort: suffix match against all global symbols. + let scope = self.symbol_table.find_scope(self.symbol_table.global_scope_id); + for (name, &sym_id) in &scope.symbol_map { + if name.rsplit('.').next() == Some(&t.ident) { + if let Some(sym) = self.symbol_table.get_symbol_ref(sym_id) { + if matches!(sym.kind, SymbolKind::Type(_)) { + t.symbol_id = sym_id; + t.ident = name.clone(); + if t.ident_kind == TypeIdentKind::Unknown { + t.ident_kind = TypeIdentKind::Def; + } + break; + } + } + } + } + } + } + } + } + TypeKind::Ref(inner) | TypeKind::Ptr(inner) => { + self.resolve_inferred_type(inner); + // Propagate the resolved symbol_id to the outer type for completion lookup + if t.symbol_id == 0 && inner.symbol_id != 0 { + t.symbol_id = inner.symbol_id; + t.ident = inner.ident.clone(); + if t.ident_kind == TypeIdentKind::Unknown { + t.ident_kind = inner.ident_kind.clone(); + } + } + } + _ => {} + } + } + + /// Try to infer a Type from an expression node. + fn infer_type_from_expr(&self, expr: &Box) -> Option { + match &expr.node { + AstNode::Call(call) => { + // Look up the function being called to get its return type + self.infer_type_from_call(call) + } + AstNode::New(type_, _, _) | AstNode::StructNew(_, type_, _) => { + // new Type{} or Type{} — the type is the type being constructed + Some(type_.clone()) + } + AstNode::Ident(_, symbol_id) => { + // Variable reference — look up the variable's type + if *symbol_id != 0 { + if let Some(symbol) = self.symbol_table.get_symbol_ref(*symbol_id) { + match &symbol.kind { + SymbolKind::Var(var) => { + let var = var.lock().unwrap(); + if !var.type_.kind.is_unknown() { + return Some(var.type_.clone()); + } + } + SymbolKind::Const(c) => { + let c = c.lock().unwrap(); + if !c.type_.kind.is_unknown() { + return Some(c.type_.clone()); + } + } + _ => {} + } + } + } + None + } + _ => None, + } + } + + /// Infer type from a function call by looking up the function's return type. + fn infer_type_from_call(&self, call: &AstCall) -> Option { + // The called function is typically an Ident or a SelectExpr (module.fn_name) + match &call.left.node { + AstNode::Ident(_, symbol_id) => { + if *symbol_id != 0 { + if let Some(symbol) = self.symbol_table.get_symbol_ref(*symbol_id) { + if let SymbolKind::Fn(fndef) = &symbol.kind { + let fndef = fndef.lock().unwrap(); + if !fndef.return_type.kind.is_unknown() { + return Some(fndef.return_type.clone()); + } + } + } + } + None + } + AstNode::SelectExpr(left, key, _) => { + // module.fn_name() — look up the function in the module's scope + if let AstNode::Ident(module_name, _) = &left.node { + // Find the module import + if let Some(import) = self.imports.iter().find(|i| i.as_name == *module_name) { + let global_ident = crate::utils::format_global_ident(import.module_ident.clone(), key.clone()); + if let Some(symbol_id) = self.symbol_table.find_symbol_id(&global_ident, self.symbol_table.global_scope_id) { + if let Some(symbol) = self.symbol_table.get_symbol_ref(symbol_id) { + if let SymbolKind::Fn(fndef) = &symbol.kind { + let fndef = fndef.lock().unwrap(); + if !fndef.return_type.kind.is_unknown() { + return Some(fndef.return_type.clone()); + } + } + } + } + } + } + None + } + // StructSelect: instance.method() — look up the method's return type + AstNode::StructSelect(_, _, _) => None, + _ => None, + } + } + + pub fn analyze_var_decl(&mut self, var_decl_mutex: &Arc>) { + let mut var_decl = var_decl_mutex.lock().unwrap(); + + self.analyze_type(&mut var_decl.type_); + + // 添加到符号表,返回值 sysmbol_id 添加到 var_decl 中, 已经包含了 redeclare check + match self.symbol_table.define_symbol_in_scope( + var_decl.ident.clone(), + SymbolKind::Var(var_decl_mutex.clone()), + var_decl.symbol_start, + self.current_scope_id, + ) { + Ok(symbol_id) => { + var_decl.symbol_id = symbol_id; + } + Err(e) => { + errors_push( + self.module, + AnalyzerError { + start: var_decl.symbol_start, + end: var_decl.symbol_end, + message: e, + is_warning: false, + }, + ); + } + } + } +} diff --git a/nls/src/analyzer/semantic.rs b/nls/src/analyzer/semantic/expressions.rs similarity index 52% rename from nls/src/analyzer/semantic.rs rename to nls/src/analyzer/semantic/expressions.rs index 77b787a9..d3509cb8 100644 --- a/nls/src/analyzer/semantic.rs +++ b/nls/src/analyzer/semantic/expressions.rs @@ -1,191 +1,17 @@ use log::debug; +use tower_lsp::lsp_types::SemanticTokenType; -use crate::project::Module; -use crate::utils::{errors_push, format_global_ident, format_impl_ident}; +use crate::utils::{errors_push, format_global_ident}; -use super::common::*; -use super::symbol::{NodeId, ScopeKind, SymbolKind, SymbolTable}; +use super::super::common::*; +use super::super::lexer::semantic_token_type_index; +use super::super::symbol::{NodeId, ScopeKind, SymbolKind}; +use super::Semantic; use std::sync::{Arc, Mutex}; -#[derive(Debug)] -pub struct Semantic<'a> { - symbol_table: &'a mut SymbolTable, - errors: Vec, - module: &'a mut Module, - stmts: Vec>, - imports: Vec, - current_local_fn_list: Vec>>, - current_scope_id: NodeId, -} - impl<'a> Semantic<'a> { - pub fn new(m: &'a mut Module, symbol_table: &'a mut SymbolTable) -> Self { - Self { - symbol_table, - errors: Vec::new(), - stmts: m.stmts.clone(), - imports: m.dependencies.clone(), - current_scope_id: m.scope_id, // m.scope_id 是 global scope id - module: m, - current_local_fn_list: Vec::new(), - } - } - - fn enter_scope(&mut self, kind: ScopeKind, start: usize, end: usize) { - let scope_id = self.symbol_table.create_scope(kind, self.current_scope_id, start, end); - self.current_scope_id = scope_id; - } - - fn exit_scope(&mut self) { - self.current_scope_id = self.symbol_table.exit_scope(self.current_scope_id); - } - - fn analyze_special_type_rewrite(&mut self, t: &mut Type) -> bool { - debug_assert!(t.import_as.is_empty()); - - // void ptr rewrite - if t.ident == "anyptr" { - t.kind = TypeKind::Anyptr; - t.ident = "".to_string(); - t.ident_kind = TypeIdentKind::Unknown; - - if t.args.len() > 0 { - errors_push( - self.module, - AnalyzerError { - start: t.start, - end: t.end, - message: format!("anyptr cannot contains arg"), - }, - ); - t.err = true; - } - - return true; - } - - // raw ptr rewrite - if t.ident == "ptr".to_string() { - // extract first args to type_ - if t.args.len() > 0 { - let mut first_arg_type = t.args[0].clone(); - self.analyze_type(&mut first_arg_type); - t.kind = TypeKind::Ptr(Box::new(first_arg_type)); - } else { - errors_push( - self.module, - AnalyzerError { - start: t.start, - end: t.end, - message: format!("ptr must contains one arg"), - }, - ); - } - - t.ident = "".to_string(); - t.ident_kind = TypeIdentKind::Unknown; - return true; - } - - // ref rewrite - if t.ident == "ref".to_string() { - if t.args.len() > 0 { - let mut first_arg_type = t.args[0].clone(); - self.analyze_type(&mut first_arg_type); - t.kind = TypeKind::Ref(Box::new(first_arg_type)); - } else { - errors_push( - self.module, - AnalyzerError { - start: t.start, - end: t.end, - message: format!("ref must contains one arg"), - }, - ); - } - - t.ident = "".to_string(); - t.ident_kind = TypeIdentKind::Unknown; - return true; - } - - // all_t rewrite - if t.ident == "all_t".to_string() { - t.kind = TypeKind::Anyptr; // 底层类型 - t.ident = "all_t".to_string(); - t.ident_kind = TypeIdentKind::Builtin; - if t.args.len() > 0 { - errors_push( - self.module, - AnalyzerError { - start: t.start, - end: t.end, - message: format!("all_type cannot contains arg"), - }, - ); - } - return true; - } - - // fn_t rewrite - if t.ident == "fn_t".to_string() { - t.kind = TypeKind::Anyptr; - t.ident = "fn_t".to_string(); - t.ident_kind = TypeIdentKind::Builtin; - if t.args.len() > 0 { - errors_push( - self.module, - AnalyzerError { - start: t.start, - end: t.end, - message: format!("fn_t cannot contains arg"), - }, - ); - } - return true; - } - - if t.ident == "integer_t".to_string() { - t.kind = TypeKind::Int; - t.ident = "integer_t".to_string(); - t.ident_kind = TypeIdentKind::Builtin; - - if t.args.len() > 0 { - errors_push( - self.module, - AnalyzerError { - start: t.start, - end: t.end, - message: format!("fn_t cannot contains arg"), - }, - ); - } - return true; - } - - if t.ident == "floater_t".to_string() { - t.kind = TypeKind::Int; - t.ident = "floater_t".to_string(); - t.ident_kind = TypeIdentKind::Builtin; - - if t.args.len() > 0 { - errors_push( - self.module, - AnalyzerError { - start: t.start, - end: t.end, - message: format!("fn_t cannot contains arg"), - }, - ); - } - return true; - } - - return false; - } - // 常量折叠 - 在编译时计算常量表达式的值 - fn constant_folding(&mut self, expr: &mut Box) { + pub(crate) fn constant_folding(&mut self, expr: &mut Box) { match &mut expr.node { AstNode::Binary(op, left, right) => { // 递归处理左右操作数 @@ -318,7 +144,7 @@ impl<'a> Semantic<'a> { } } - fn constant_propagation(&mut self, expr: &mut Box) { + pub(crate) fn constant_propagation(&mut self, expr: &mut Box) { // 检查表达式是否为标识符 let AstNode::Ident(_, symbol_id) = &expr.node else { return; @@ -344,6 +170,7 @@ impl<'a> Semantic<'a> { start: expr.start, end: expr.end, message: "const initialization cycle detected".to_string(), + is_warning: false, }, ); return; @@ -366,6 +193,7 @@ impl<'a> Semantic<'a> { start: expr.start, end: expr.end, message: "const cannot be initialized with non-literal value".to_string(), + is_warning: false, }, ); return; @@ -375,654 +203,6 @@ impl<'a> Semantic<'a> { expr.node = AstNode::Literal(literal_kind.clone(), literal_value.clone()); } - fn analyze_type(&mut self, t: &mut Type) { - if Type::is_ident(t) || t.ident_kind == TypeIdentKind::Interface { - // 处理导入的全局模式别名,例如 package.foo_t - if !t.import_as.is_empty() { - // 只要存在 import as, 就必须能够在 imports 中找到对应的 import - let import_stmt = self.imports.iter().find(|i| i.as_name == t.import_as); - if import_stmt.is_none() { - errors_push( - self.module, - AnalyzerError { - start: t.start, - end: t.end, - message: format!("import '{}' undeclared", t.import_as), - }, - ); - t.err = true; - return; - } - - let import_stmt = import_stmt.unwrap(); - - // 从 symbol table 中查找相关的 global symbol id - if let Some(symbol_id) = self.symbol_table.find_module_symbol_id(&import_stmt.module_ident, &t.ident) { - t.import_as = "".to_string(); - t.ident = format_global_ident(import_stmt.module_ident.clone(), t.ident.clone()); - t.symbol_id = symbol_id; - } else { - errors_push( - self.module, - AnalyzerError { - start: t.start, - end: t.end, - message: format!("type '{}' undeclared in {} module", t.ident, import_stmt.module_ident), - }, - ); - t.err = true; - return; - } - } else { - // no import as, maybe local ident or parent indet - if let Some(symbol_id) = self.resolve_typedef(&mut t.ident) { - t.symbol_id = symbol_id; - } else { - // maybe check is special type ident - if self.analyze_special_type_rewrite(t) { - return; - } - - errors_push( - self.module, - AnalyzerError { - start: t.start, - end: t.end, - message: format!("type '{}' undeclared", t.ident), - }, - ); - t.err = true; - return; - } - } - - if let Some(symbol) = self.symbol_table.get_symbol(t.symbol_id) { - let SymbolKind::Type(typedef_mutex) = &symbol.kind else { - errors_push( - self.module, - AnalyzerError { - start: t.start, - end: t.end, - message: format!("'{}' not a type", t.ident), - }, - ); - t.err = true; - return; - }; - let (is_alias, is_interface) = { - let typedef_stmt = typedef_mutex.lock().unwrap(); - (typedef_stmt.is_alias, typedef_stmt.is_interface) - }; - - // 确认具体类型 - if t.ident_kind == TypeIdentKind::Unknown { - if is_alias { - t.ident_kind = TypeIdentKind::Alias; - if t.args.len() > 0 { - errors_push( - self.module, - AnalyzerError { - start: t.start, - end: t.end, - message: format!("alias '{}' cannot contains generics type args", t.ident), - }, - ); - return; - } - } else if is_interface { - t.ident_kind = TypeIdentKind::Interface; - } else { - t.ident_kind = TypeIdentKind::Def; - } - } - } - - // analyzer args - if t.args.len() > 0 { - for arg_type in &mut t.args { - self.analyze_type(arg_type); - } - } - - return; - } - - match &mut t.kind { - TypeKind::Interface(elements) => { - for element in elements { - self.analyze_type(element); - } - } - TypeKind::Union(_, _, elements) => { - for element in elements.iter_mut() { - self.analyze_type(element); - } - } - TypeKind::Map(key_type, value_type) => { - self.analyze_type(key_type); - self.analyze_type(value_type); - } - TypeKind::Set(element_type) => { - self.analyze_type(element_type); - } - TypeKind::Vec(element_type) => { - self.analyze_type(element_type); - } - TypeKind::Chan(element_type) => { - self.analyze_type(element_type); - } - TypeKind::Arr(length_expr, length, element_type) => { - self.analyze_expr(length_expr); - if let AstNode::Literal(literal_kind, literal_value) = &mut length_expr.node { - if Type::is_integer(literal_kind) { - if let Ok(parsed_length) = literal_value.parse::() { - *length = parsed_length as u64; - } else { - errors_push( - self.module, - AnalyzerError { - start: t.start, - end: t.end, - message: "array length must be constans or integer literal".to_string(), - }, - ); - } - } else { - errors_push( - self.module, - AnalyzerError { - start: t.start, - end: t.end, - message: "array length must be constans or integer literal".to_string(), - }, - ); - } - } else { - // error push - errors_push( - self.module, - AnalyzerError { - start: t.start, - end: t.end, - message: "array length must be constans or integer literal".to_string(), - }, - ); - } - - self.analyze_type(element_type); - } - TypeKind::Tuple(elements, _align) => { - for element in elements { - self.analyze_type(element); - } - } - TypeKind::Ref(value_type) => { - self.analyze_type(value_type); - } - TypeKind::Ptr(value_type) => { - self.analyze_type(value_type); - } - TypeKind::Fn(fn_type) => { - self.analyze_type(&mut fn_type.return_type); - - for param_type in &mut fn_type.param_types { - self.analyze_type(param_type); - } - } - TypeKind::Struct(_ident, _, properties) => { - for property in properties.iter_mut() { - self.analyze_type(&mut property.type_); - - // 可选的又值 - if let Some(value) = &mut property.value { - self.analyze_expr(value); - - // value kind cannot is fndef - if let AstNode::FnDef(..) = value.node { - errors_push( - self.module, - AnalyzerError { - start: value.start, - end: value.end, - message: format!("struct field default value cannot be a fn def, use fn def ident instead"), - }, - ); - t.err = true; - } - } - } - } - TypeKind::Enum(element_type, properties) => { - // Analyze element type - self.analyze_type(element_type); - - // Analyze value expressions for each enum member - for property in properties.iter_mut() { - if let Some(value_expr) = &mut property.value_expr { - self.analyze_expr(value_expr); - } - } - } - TypeKind::TaggedUnion(_ident, elements) => { - for element in elements.iter_mut() { - self.analyze_type(&mut element.type_); - } - } - _ => { - return; - } - } - } - - /** - * 验证选择性导入的符号是否存在于目标模块中 - * 例如: import co.mutex.{mutex_t} 时验证 mutex_t 是否真实存在 - */ - fn validate_selective_imports(&mut self) { - for import in &self.imports { - if !import.is_selective { - continue; - } - - let Some(ref items) = import.select_items else { continue }; - - for item in items { - let global_ident = format_global_ident(import.module_ident.clone(), item.ident.clone()); - if self.symbol_table.find_symbol_id(&global_ident, self.symbol_table.global_scope_id).is_some() { - continue; - } - - errors_push( - self.module, - AnalyzerError { - start: import.start, - end: import.end, - message: format!("symbol '{}' not found in module '{}'", item.ident, import.module_ident), - }, - ); - } - } - } - - /** - * analyze 之前,相关 module 的 global symbol 都已经注册完成, 这里不能再重复注册了。 - */ - pub fn analyze(&mut self) { - // 验证选择性导入的符号是否存在 - self.validate_selective_imports(); - - let mut global_fn_stmt_list = Vec::>>::new(); - - let mut stmts = Vec::>::new(); - - let mut global_vardefs = Vec::new(); - - // 跳过 import - for i in 0..self.stmts.len() { - // 使用 clone 避免对 self 所有权占用 - let mut stmt = self.stmts[i].clone(); - - match &mut stmt.node { - AstNode::Import(..) => continue, - AstNode::FnDef(fndef_mutex) => { - let mut fndef = fndef_mutex.lock().unwrap(); - let symbol_name = fndef.symbol_name.clone(); - - if fndef.impl_type.kind.is_exist() { - let mut impl_type_ident = fndef.impl_type.ident.clone(); - - if Type::is_impl_builtin_type(&fndef.impl_type.kind) { - impl_type_ident = fndef.impl_type.kind.to_string(); - } - - // 非 builtin type 则进行 resolve type 查找 - if !Type::is_impl_builtin_type(&fndef.impl_type.kind) { - // resolve global ident - if let Some(symbol_id) = self.resolve_typedef(&mut fndef.impl_type.ident) { - // ident maybe change - fndef.impl_type.symbol_id = symbol_id; - impl_type_ident = fndef.impl_type.ident.clone(); - - // 自定义泛型 impl type 必须显式给出类型参数(仅检查 impl_type.args) - if let Some(symbol) = self.symbol_table.get_symbol(symbol_id) { - if let SymbolKind::Type(typedef_mutex) = &symbol.kind { - let typedef = typedef_mutex.lock().unwrap(); - if !typedef.params.is_empty() && fndef.impl_type.args.len() != typedef.params.len() { - errors_push( - self.module, - AnalyzerError { - start: fndef.symbol_start, - end: fndef.symbol_end, - message: format!("impl type '{}' must specify generics params", fndef.impl_type.ident), - }, - ); - } - } - } - } else { - errors_push( - self.module, - AnalyzerError { - start: fndef.symbol_start, - end: fndef.symbol_end, - message: format!("type '{}' undeclared", fndef.impl_type.symbol_id), - }, - ); - } - } - - fndef.symbol_name = format_impl_ident(impl_type_ident, symbol_name); - - // register to global symbol table - match self.symbol_table.define_symbol_in_scope( - fndef.symbol_name.clone(), - SymbolKind::Fn(fndef_mutex.clone()), - fndef.symbol_start, - self.module.scope_id, - ) { - Ok(symbol_id) => { - fndef.symbol_id = symbol_id; - } - Err(e) => { - errors_push( - self.module, - AnalyzerError { - start: fndef.symbol_start, - end: fndef.symbol_end, - message: e, - }, - ); - } - } - - // register to global symbol - let _ = self.symbol_table.define_global_symbol( - fndef.symbol_name.clone(), - SymbolKind::Fn(fndef_mutex.clone()), - fndef.symbol_start, - self.module.scope_id, - ); - } - - global_fn_stmt_list.push(fndef_mutex.clone()); - - if let Some(generics_params) = &mut fndef.generics_params { - for generics_param in generics_params { - for constraint in &mut generics_param.constraints { - self.analyze_type(constraint); - } - } - } - } - AstNode::VarDef(var_decl_mutex, right_expr) => { - let mut var_decl = var_decl_mutex.lock().unwrap(); - self.analyze_type(&mut var_decl.type_); - - // push to global_vardef - global_vardefs.push(AstNode::VarDef(var_decl_mutex.clone(), right_expr.clone())); - } - - AstNode::Typedef(type_alias_mutex) => { - let mut type_expr = { - let mut typedef = type_alias_mutex.lock().unwrap(); - - // 处理 params constraints, type foo = ... - if typedef.params.len() > 0 { - for param in typedef.params.iter_mut() { - // 遍历所有 constraints 类型 进行 analyze - for constraint in &mut param.constraints { - // TODO constraint 不能是自身 - self.analyze_type(constraint); - } - } - } - - if typedef.impl_interfaces.len() > 0 { - for impl_interface in &mut typedef.impl_interfaces { - debug_assert!(impl_interface.kind == TypeKind::Ident && impl_interface.ident_kind == TypeIdentKind::Interface); - self.analyze_type(impl_interface); - } - } - - // analyzer type expr, symbol table 中存储的是 type_expr 的 arc clone, 所以这里的修改会同步到 symbol table 中 - // 递归依赖处理 - typedef.type_expr.clone() - }; - - self.analyze_type(&mut type_expr); - - { - let mut typedef = type_alias_mutex.lock().unwrap(); - typedef.type_expr = type_expr; - } - } - - AstNode::ConstDef(const_mutex) => { - let mut constdef = const_mutex.lock().unwrap(); - self.analyze_expr(&mut constdef.right); - - if !matches!(constdef.right.node, AstNode::Literal(..)) { - errors_push( - self.module, - AnalyzerError { - start: constdef.symbol_start, - end: constdef.symbol_end, - message: format!("const cannot be initialized"), - }, - ); - } - } - _ => { - // 语义分析中包含许多错误 - } - } - - // 归还 stmt list - stmts.push(stmt); - } - - // 对 fn stmt list 进行 analyzer 处理。 - for fndef_mutex in &global_fn_stmt_list { - self.module.all_fndefs.push(fndef_mutex.clone()); - self.analyze_global_fn(fndef_mutex.clone()); - } - - // global vardef 的右值不在函数体里,需要独立做 analyze - for node in &mut global_vardefs { - match node { - AstNode::VarDef(_, right_expr) => { - if let AstNode::FnDef(_) = &right_expr.node { - // fn def 会自动 arc 引用传递,这里无需重复处理 - } else { - self.analyze_expr(right_expr); - } - } - _ => {} - } - } - - self.module.stmts = stmts; - self.module.global_vardefs = global_vardefs; - self.module.global_fndefs = global_fn_stmt_list; - self.module.analyzer_errors.extend(self.errors.clone()); - } - - pub fn resolve_typedef(&mut self, ident: &mut String) -> Option { - // 首先尝试在当前作用域和父级作用域中直接查找该符号, 最终会找到 m.scope_id, 这里包含当前 module 的全局符号 - if let Some(symbol_id) = self.symbol_table.lookup_symbol(ident, self.current_scope_id) { - return Some(symbol_id); - } - - // 首先尝试在当前 module 中查找该符号 - if let Some(symbol_id) = self.symbol_table.find_module_symbol_id(&self.module.ident, ident) { - let current_module_ident = format_global_ident(self.module.ident.clone(), ident.to_string()); - *ident = current_module_ident; - return Some(symbol_id); - } - - // Check selective imports: import math.{sqrt, pow, Point} - for import in &self.imports { - if !import.is_selective { - continue; - } - let Some(ref items) = import.select_items else { continue }; - for item in items { - let local_name = item.alias.as_ref().unwrap_or(&item.ident); - if local_name != ident { - continue; - } - let global_ident = format_global_ident(import.module_ident.clone(), item.ident.clone()); - if let Some(id) = self.symbol_table.find_symbol_id(&global_ident, self.symbol_table.global_scope_id) { - *ident = global_ident; - return Some(id); - } - } - } - - // import x as * 产生的全局符号 - for i in &self.imports { - if i.as_name != "*" { - continue; - }; - - if let Some(symbol_id) = self.symbol_table.find_module_symbol_id(&i.module_ident, ident) { - *ident = format_global_ident(i.module_ident.clone(), ident.to_string()); - return Some(symbol_id); - } - } - - // builtin 全局符号,不需要进行 format 链接,直接读取 global 符号表 - return self.symbol_table.find_symbol_id(ident, self.symbol_table.global_scope_id); - } - - pub fn symbol_typedef_add_method(&mut self, typedef_ident: String, method_ident: String, fndef: Arc>) -> Result<(), String> { - // get typedef from symbol table(global symbol) - let symbol = self - .symbol_table - .find_global_symbol(&typedef_ident) - .ok_or_else(|| format!("symbol {} not found", typedef_ident))?; - let SymbolKind::Type(typedef_mutex) = &symbol.kind else { - return Err(format!("symbol {} is not typedef", typedef_ident)); - }; - let mut typedef = typedef_mutex.lock().unwrap(); - typedef.method_table.insert(method_ident, fndef); - - return Ok(()); - } - - pub fn analyze_global_fn(&mut self, fndef_mutex: Arc>) { - { - let mut fndef = fndef_mutex.lock().unwrap(); - - fndef.is_local = false; - fndef.module_index = self.module.index; - if fndef.generics_params.is_some() { - fndef.is_generics = true; - } - - if fndef.is_tpl && fndef.body.stmts.len() == 0 { - debug_assert!(fndef.body.stmts.len() == 0); - } - - self.analyze_type(&mut fndef.return_type); - - // 如果 impl type 是 type alias, 则从符号表中获取当前的 type alias 的全称进行更新 - // fn vec.len() -> fn vec_len(vec self) - // impl 是 type alias 时,只能是 fn person_t.len() 而不能是 fn pkg.person_t.len() - if fndef.impl_type.kind.is_exist() { - if !fndef.is_static && fndef.self_kind != SelfKind::Null { - // 重构 params 的位置, 新增 self param - let mut new_params = Vec::new(); - let param_type = fndef.impl_type.clone(); - let self_vardecl = VarDeclExpr { - ident: String::from("self"), - type_: param_type, - be_capture: false, - heap_ident: None, - symbol_start: fndef.symbol_start, - symbol_end: fndef.symbol_end, - symbol_id: 0, - is_private: false, - }; - - new_params.push(Arc::new(Mutex::new(self_vardecl))); - new_params.extend(fndef.params.iter().cloned()); - fndef.params = new_params; - - // builtin type 没有注册在符号表,不能添加 method - if !Type::is_impl_builtin_type(&fndef.impl_type.kind) { - self.symbol_typedef_add_method(fndef.impl_type.ident.clone(), fndef.symbol_name.clone(), fndef_mutex.clone()) - .unwrap_or_else(|e| { - errors_push( - self.module, - AnalyzerError { - start: fndef.symbol_start, - end: fndef.symbol_end, - message: e, - }, - ); - }); - } - } - } - - self.enter_scope(ScopeKind::GlobalFn(fndef_mutex.clone()), fndef.symbol_start, fndef.symbol_end); - - // 函数形参处理 - for param_mutex in &fndef.params { - let mut param = param_mutex.lock().unwrap(); - self.analyze_type(&mut param.type_); - - // 将参数添加到符号表中 - match self.symbol_table.define_symbol_in_scope( - param.ident.clone(), - SymbolKind::Var(param_mutex.clone()), - param.symbol_start, - self.current_scope_id, - ) { - Ok(symbol_id) => { - param.symbol_id = symbol_id; - } - Err(e) => { - errors_push( - self.module, - AnalyzerError { - start: param.symbol_start, - end: param.symbol_end, - message: e, - }, - ); - } - } - } - } - - { - let mut body = { - let mut fndef = fndef_mutex.lock().unwrap(); - std::mem::take(&mut fndef.body) - }; - - if body.stmts.len() > 0 { - self.analyze_body(&mut body); - } - - // 将当前的 fn 添加到 global fn 的 local_children 中 - { - let mut fndef = fndef_mutex.lock().unwrap(); - - // 归还 body - fndef.body = body; - fndef.local_children = self.current_local_fn_list.clone(); - } - } - - // 清空 self.current_local_fn_list, 进行重新计算 - self.current_local_fn_list.clear(); - - self.exit_scope(); - } - pub fn analyze_body(&mut self, body: &mut AstBody) { for stmt in &mut body.stmts { self.analyze_stmt(stmt); @@ -1050,11 +230,29 @@ impl<'a> Semantic<'a> { pub fn rewrite_select_expr(&mut self, expr: &mut Box) { let AstNode::SelectExpr(left, key, _) = &mut expr.node else { unreachable!() }; + // Empty key means incomplete parse recovery (user is typing "x."), + // resolve the left side but skip key-dependent analysis and error messages + if key.is_empty() { + if let AstNode::Ident(left_ident, symbol_id) = &mut left.node { + if let Some(id) = self.symbol_table.lookup_symbol(left_ident, self.current_scope_id) { + *symbol_id = id; + } else if let Some(id) = self.symbol_table.find_module_symbol_id(&self.module.ident, left_ident) { + *symbol_id = id; + *left_ident = format_global_ident(self.module.ident.clone(), left_ident.to_string().clone()); + } + } else { + self.analyze_expr(left); + } + return; + } + if let AstNode::Ident(left_ident, symbol_id) = &mut left.node { // 尝试 find local or parent ident, 如果找到,将 symbol_id 添加到 Ident 中 // symbol 可能是 parent local, 也可能是 parent fn,此时则发生闭包函数引用, 需要将 ident 改写成 env access if let Some(id) = self.symbol_table.lookup_symbol(left_ident, self.current_scope_id) { *symbol_id = id; + // Update the left ident token type based on what it actually is + self.update_ident_token_type(left.start, left.end, id); return; } @@ -1088,17 +286,25 @@ impl<'a> Semantic<'a> { } // import package ident - let import_stmt = self.imports.iter().find(|i| i.as_name == *left_ident); - if let Some(import_stmt) = import_stmt { - // debug!("import as name {}, module_ident {}, key {key}", import_stmt.as_name, import_stmt.module_ident); - - // select left 以及找到了,但是还是改不了? infer 阶段能快速定位就好了。现在的关键是,找到了又怎么样, 又能做什么,也改写不了什么。只能是? - // 只能是添加一个 symbol_id? 但是符号本身也没有意义了?如果直接改成 ident + symbol_id 呢?还是改,只是改成了更为奇怪的存在。 - if let Some(id) = self.symbol_table.find_module_symbol_id(&import_stmt.module_ident, key) { - // debug!("find symbol id {} by module_ident {}, key {key}", id, import_stmt.module_ident); + let import_module_ident = self.imports.iter() + .find(|i| i.as_name == *left_ident) + .map(|i| i.module_ident.clone()); + if let Some(module_ident) = import_module_ident { + if let Some(id) = self.symbol_table.find_module_symbol_id(&module_ident, key) { + // Mark the import prefix as NAMESPACE and the key token by its resolved kind + let left_ns_idx = semantic_token_type_index(SemanticTokenType::NAMESPACE); + for token in self.module.sem_token_db.iter_mut() { + if token.start == left.start && token.end == left.end { + token.semantic_token_type = left_ns_idx; + break; + } + } + // Update key token type based on the resolved symbol + let expr_end = expr.end; + self.update_ident_token_by_end(expr_end, id); - // 将整个 expr 直接改写成 global ident, 这也是 analyze_select_expr 的核心目录 - expr.node = AstNode::Ident(format_global_ident(import_stmt.module_ident.clone(), key.clone()), id); + // 将整个 expr 直接改写成 global ident + expr.node = AstNode::Ident(format_global_ident(module_ident.clone(), key.clone()), id); return; } else { errors_push( @@ -1107,6 +313,7 @@ impl<'a> Semantic<'a> { start: expr.start, end: expr.end, message: format!("identifier '{}' undeclared in '{}' module", key, left_ident), + is_warning: false, }, ); return; @@ -1126,6 +333,7 @@ impl<'a> Semantic<'a> { start: expr.start, end: expr.end, message: format!("identifier '{}.{}' undeclared", left_ident, key), + is_warning: false, }, ); @@ -1202,6 +410,7 @@ impl<'a> Semantic<'a> { start: cond.start, end: cond.end, message: "default case '_' conflict in a 'match' expression".to_string(), + is_warning: false, }, ); } @@ -1213,6 +422,7 @@ impl<'a> Semantic<'a> { start: cond.start, end: cond.end, message: "default case '_' must be the last one in a 'match' expression".to_string(), + is_warning: false, }, ); } @@ -1331,6 +541,7 @@ impl<'a> Semantic<'a> { start: ut.start, end: ut.end, message: "unexpected is expr".to_string(), + is_warning: false, }, ); return; @@ -1360,6 +571,7 @@ impl<'a> Semantic<'a> { start: ut.start, end: ut.end, message: "unexpected is expr".to_string(), + is_warning: false, }, ); } @@ -1469,8 +681,12 @@ impl<'a> Semantic<'a> { start: expr.start, end: expr.end, message: format!("identifier '{}' undeclared", ident), + is_warning: false, }, ); + } else { + // Update semantic token type based on the resolved symbol kind + self.update_ident_token_type(expr.start, expr.end, *symbol_id); } // propagation @@ -1490,6 +706,7 @@ impl<'a> Semantic<'a> { start: expr.start, end: expr.end, message: "tagged union uses parentheses but passes no arguments".to_string(), + is_warning: false, }, ); return; @@ -1550,6 +767,7 @@ impl<'a> Semantic<'a> { start: item.start, end: item.end, message: "var tuple destr expr type exception".to_string(), + is_warning: false, }, ); } @@ -1574,131 +792,6 @@ impl<'a> Semantic<'a> { } } - /** - * local fn in global fn - */ - pub fn analyze_local_fndef(&mut self, fndef_mutex: &Arc>) { - self.module.all_fndefs.push(fndef_mutex.clone()); - - let mut fndef = fndef_mutex.lock().unwrap(); - - // find global fn in symbol table - let Some(global_fn_mutex) = self.symbol_table.find_global_fn(self.current_scope_id) else { - return; - }; - fndef.global_parent = Some(global_fn_mutex.clone()); - fndef.is_local = true; - - self.current_local_fn_list.push(fndef_mutex.clone()); - - // local fn 作为闭包函数, 不能进行类型扩展和泛型参数 - if fndef.impl_type.kind.is_exist() || fndef.generics_params.is_some() { - errors_push( - self.module, - AnalyzerError { - start: fndef.symbol_start, - end: fndef.symbol_end, - message: "closure fn cannot be generics or impl type alias".to_string(), - }, - ); - } - - // 闭包不能包含 macro ident - if fndef.linkid.is_some() { - errors_push( - self.module, - AnalyzerError { - start: fndef.symbol_start, - end: fndef.symbol_end, - message: "closure fn cannot have #linkid label".to_string(), - }, - ); - } - - if fndef.is_tpl { - errors_push( - self.module, - AnalyzerError { - start: fndef.symbol_start, - end: fndef.symbol_end, - message: "closure fn cannot be template".to_string(), - }, - ); - } - - self.analyze_type(&mut fndef.return_type); - - self.enter_scope(ScopeKind::LocalFn(fndef_mutex.clone()), fndef.symbol_start, fndef.symbol_end); - - // 形参处理 - for param_mutex in &fndef.params { - let mut param = param_mutex.lock().unwrap(); - self.analyze_type(&mut param.type_); - - // 将参数添加到符号表中 - match self.symbol_table.define_symbol_in_scope( - param.ident.clone(), - SymbolKind::Var(param_mutex.clone()), - param.symbol_start, - self.current_scope_id, - ) { - Ok(symbol_id) => { - param.symbol_id = symbol_id; - } - Err(e) => { - errors_push( - self.module, - AnalyzerError { - start: param.symbol_start, - end: param.symbol_end, - message: e, - }, - ); - } - } - } - - // handle body - self.analyze_body(&mut fndef.body); - - let mut free_var_count = 0; - let scope = self.symbol_table.find_scope(self.current_scope_id); - for (_, free_ident) in scope.frees.iter() { - if matches!(free_ident.kind, SymbolKind::Var(..)) { - free_var_count += 1; - } - } - - self.exit_scope(); - - // 当前函数需要编译成闭包, 所有的 call fn 改造成 call fn_var - if free_var_count > 0 { - fndef.is_closure = true; - } - - // 将 fndef lambda 添加到 symbol table 中 - match self.symbol_table.define_symbol_in_scope( - fndef.symbol_name.clone(), - SymbolKind::Fn(fndef_mutex.clone()), - fndef.symbol_start, - self.current_scope_id, - ) { - Ok(symbol_id) => { - fndef.symbol_id = symbol_id; - } - Err(e) => { - errors_push( - self.module, - AnalyzerError { - start: fndef.symbol_start, - end: fndef.symbol_end, - message: e, - }, - ); - } - } - } - pub fn extract_is_expr(&mut self, cond: &Box) -> Option> { // 支持任意表达式作为 is 表达式的源,不再限制必须是 ident if let AstNode::Is(_target_type, _union_tag, _src, _binding) = &cond.node { @@ -1719,6 +812,7 @@ impl<'a> Semantic<'a> { start: cond.start, end: cond.end, message: "condition expr cannot contains multiple is expr".to_string(), + is_warning: false, }, ); } @@ -1843,11 +937,13 @@ impl<'a> Semantic<'a> { start: constdef.symbol_start, end: constdef.symbol_end, message: e, + is_warning: false, }, ); } } } + pub fn analyze_stmt(&mut self, stmt: &mut Box) { match &mut stmt.node { AstNode::Fake(expr) | AstNode::Ret(expr) => { @@ -1859,6 +955,9 @@ impl<'a> Semantic<'a> { AstNode::VarDef(var_decl_mutex, expr) => { self.analyze_expr(expr); self.analyze_var_decl(var_decl_mutex); + + // Type inference: if var type is unknown, try to infer from right-hand expression + self.infer_var_type_from_expr(var_decl_mutex, expr); } AstNode::ConstDef(constdef_mutex) => { self.analyze_constdef(constdef_mutex.clone()); @@ -1913,6 +1012,7 @@ impl<'a> Semantic<'a> { start: case.handle_body.start, end: case.handle_body.end, message: "default case must be the last case".to_string(), + is_warning: false, }, ); } @@ -1964,6 +1064,7 @@ impl<'a> Semantic<'a> { start: typedef.symbol_start, end: typedef.symbol_end, message: "local type alias cannot have params".to_string(), + is_warning: false, }, ); } @@ -1976,6 +1077,7 @@ impl<'a> Semantic<'a> { start: typedef.symbol_start, end: typedef.symbol_end, message: "local typedef cannot with impls".to_string(), + is_warning: false, }, ); } @@ -1998,6 +1100,7 @@ impl<'a> Semantic<'a> { start: typedef.symbol_start, end: typedef.symbol_end, message: e, + is_warning: false, }, ); } @@ -2008,32 +1111,4 @@ impl<'a> Semantic<'a> { } } } - - pub fn analyze_var_decl(&mut self, var_decl_mutex: &Arc>) { - let mut var_decl = var_decl_mutex.lock().unwrap(); - - self.analyze_type(&mut var_decl.type_); - - // 添加到符号表,返回值 sysmbol_id 添加到 var_decl 中, 已经包含了 redeclare check - match self.symbol_table.define_symbol_in_scope( - var_decl.ident.clone(), - SymbolKind::Var(var_decl_mutex.clone()), - var_decl.symbol_start, - self.current_scope_id, - ) { - Ok(symbol_id) => { - var_decl.symbol_id = symbol_id; - } - Err(e) => { - errors_push( - self.module, - AnalyzerError { - start: var_decl.symbol_start, - end: var_decl.symbol_end, - message: e, - }, - ); - } - } - } } diff --git a/nls/src/analyzer/semantic/mod.rs b/nls/src/analyzer/semantic/mod.rs new file mode 100644 index 00000000..0cbbc942 --- /dev/null +++ b/nls/src/analyzer/semantic/mod.rs @@ -0,0 +1,843 @@ +mod declarations; +mod expressions; + +use tower_lsp::lsp_types::SemanticTokenType; + +use crate::project::Module; +use crate::utils::{errors_push, format_global_ident, format_impl_ident}; + +use super::common::*; +use super::lexer::semantic_token_type_index; +use super::symbol::{NodeId, ScopeKind, SymbolKind, SymbolTable}; +use std::sync::{Arc, Mutex}; + +#[derive(Debug)] +pub struct Semantic<'a> { + pub(crate) symbol_table: &'a mut SymbolTable, + pub(crate) errors: Vec, + pub(crate) module: &'a mut Module, + pub(crate) stmts: Vec>, + pub(crate) imports: Vec, + pub(crate) current_local_fn_list: Vec>>, + pub(crate) current_scope_id: NodeId, +} + +impl<'a> Semantic<'a> { + pub fn new(m: &'a mut Module, symbol_table: &'a mut SymbolTable) -> Self { + Self { + symbol_table, + errors: Vec::new(), + stmts: m.stmts.clone(), + imports: m.dependencies.clone(), + current_scope_id: m.scope_id, // m.scope_id 是 global scope id + module: m, + current_local_fn_list: Vec::new(), + } + } + + pub(crate) fn enter_scope(&mut self, kind: ScopeKind, start: usize, end: usize) { + let scope_id = self.symbol_table.create_scope(kind, self.current_scope_id, start, end); + self.current_scope_id = scope_id; + } + + pub(crate) fn exit_scope(&mut self) { + self.current_scope_id = self.symbol_table.exit_scope(self.current_scope_id); + } + + fn analyze_special_type_rewrite(&mut self, t: &mut Type) -> bool { + debug_assert!(t.import_as.is_empty()); + + // void ptr rewrite + if t.ident == "anyptr" { + t.kind = TypeKind::Anyptr; + t.ident = "".to_string(); + t.ident_kind = TypeIdentKind::Unknown; + + if t.args.len() > 0 { + errors_push( + self.module, + AnalyzerError { + start: t.start, + end: t.end, + message: format!("anyptr cannot contains arg"), + is_warning: false, + }, + ); + t.err = true; + } + + return true; + } + + // raw ptr rewrite + if t.ident == "ptr".to_string() { + // extract first args to type_ + if t.args.len() > 0 { + let mut first_arg_type = t.args[0].clone(); + self.analyze_type(&mut first_arg_type); + t.kind = TypeKind::Ptr(Box::new(first_arg_type)); + } else { + errors_push( + self.module, + AnalyzerError { + start: t.start, + end: t.end, + message: format!("ptr must contains one arg"), + is_warning: false, + }, + ); + } + + t.ident = "".to_string(); + t.ident_kind = TypeIdentKind::Unknown; + return true; + } + + // ref rewrite + if t.ident == "ref".to_string() { + if t.args.len() > 0 { + let mut first_arg_type = t.args[0].clone(); + self.analyze_type(&mut first_arg_type); + t.kind = TypeKind::Ref(Box::new(first_arg_type)); + } else { + errors_push( + self.module, + AnalyzerError { + start: t.start, + end: t.end, + message: format!("ref must contains one arg"), + is_warning: false, + }, + ); + } + + t.ident = "".to_string(); + t.ident_kind = TypeIdentKind::Unknown; + return true; + } + + // all_t rewrite + if t.ident == "all_t".to_string() { + t.kind = TypeKind::Anyptr; // 底层类型 + t.ident = "all_t".to_string(); + t.ident_kind = TypeIdentKind::Builtin; + if t.args.len() > 0 { + errors_push( + self.module, + AnalyzerError { + start: t.start, + end: t.end, + message: format!("all_type cannot contains arg"), + is_warning: false, + }, + ); + } + return true; + } + + // fn_t rewrite + if t.ident == "fn_t".to_string() { + t.kind = TypeKind::Anyptr; + t.ident = "fn_t".to_string(); + t.ident_kind = TypeIdentKind::Builtin; + if t.args.len() > 0 { + errors_push( + self.module, + AnalyzerError { + start: t.start, + end: t.end, + message: format!("fn_t cannot contains arg"), + is_warning: false, + }, + ); + } + return true; + } + + if t.ident == "integer_t".to_string() { + t.kind = TypeKind::Int; + t.ident = "integer_t".to_string(); + t.ident_kind = TypeIdentKind::Builtin; + + if t.args.len() > 0 { + errors_push( + self.module, + AnalyzerError { + start: t.start, + end: t.end, + message: format!("fn_t cannot contains arg"), + is_warning: false, + }, + ); + } + return true; + } + + if t.ident == "floater_t".to_string() { + t.kind = TypeKind::Int; + t.ident = "floater_t".to_string(); + t.ident_kind = TypeIdentKind::Builtin; + + if t.args.len() > 0 { + errors_push( + self.module, + AnalyzerError { + start: t.start, + end: t.end, + message: format!("fn_t cannot contains arg"), + is_warning: false, + }, + ); + } + return true; + } + + return false; + } + + pub(crate) fn analyze_type(&mut self, t: &mut Type) { + if Type::is_ident(t) || t.ident_kind == TypeIdentKind::Interface { + // 处理导入的全局模式别名,例如 package.foo_t + if !t.import_as.is_empty() { + // 只要存在 import as, 就必须能够在 imports 中找到对应的 import + let import_stmt = self.imports.iter().find(|i| i.as_name == t.import_as); + if import_stmt.is_none() { + errors_push( + self.module, + AnalyzerError { + start: t.start, + end: t.end, + message: format!("import '{}' undeclared", t.import_as), + is_warning: false, + }, + ); + t.err = true; + return; + } + + let import_stmt = import_stmt.unwrap(); + + // 从 symbol table 中查找相关的 global symbol id + if let Some(symbol_id) = self.symbol_table.find_module_symbol_id(&import_stmt.module_ident, &t.ident) { + t.import_as = "".to_string(); + t.ident = format_global_ident(import_stmt.module_ident.clone(), t.ident.clone()); + t.symbol_id = symbol_id; + } else { + errors_push( + self.module, + AnalyzerError { + start: t.start, + end: t.end, + message: format!("type '{}' undeclared in {} module", t.ident, import_stmt.module_ident), + is_warning: false, + }, + ); + t.err = true; + return; + } + } else { + // no import as, maybe local ident or parent indet + if let Some(symbol_id) = self.resolve_typedef(&mut t.ident) { + t.symbol_id = symbol_id; + } else { + // maybe check is special type ident + if self.analyze_special_type_rewrite(t) { + return; + } + + errors_push( + self.module, + AnalyzerError { + start: t.start, + end: t.end, + message: format!("type '{}' undeclared", t.ident), + is_warning: false, + }, + ); + t.err = true; + return; + } + } + + if let Some(symbol) = self.symbol_table.get_symbol(t.symbol_id) { + let SymbolKind::Type(typedef_mutex) = &symbol.kind else { + errors_push( + self.module, + AnalyzerError { + start: t.start, + end: t.end, + message: format!("'{}' not a type", t.ident), + is_warning: false, + }, + ); + t.err = true; + return; + }; + let (is_alias, is_interface) = { + let typedef_stmt = typedef_mutex.lock().unwrap(); + (typedef_stmt.is_alias, typedef_stmt.is_interface) + }; + + // 确认具体类型 + if t.ident_kind == TypeIdentKind::Unknown { + if is_alias { + t.ident_kind = TypeIdentKind::Alias; + if t.args.len() > 0 { + errors_push( + self.module, + AnalyzerError { + start: t.start, + end: t.end, + message: format!("alias '{}' cannot contains generics type args", t.ident), + is_warning: false, + }, + ); + return; + } + } else if is_interface { + t.ident_kind = TypeIdentKind::Interface; + } else { + t.ident_kind = TypeIdentKind::Def; + } + } + } + + // analyzer args + if t.args.len() > 0 { + for arg_type in &mut t.args { + self.analyze_type(arg_type); + } + } + + return; + } + + match &mut t.kind { + TypeKind::Interface(elements) => { + for element in elements { + self.analyze_type(element); + } + } + TypeKind::Union(_, _, elements) => { + for element in elements.iter_mut() { + self.analyze_type(element); + } + } + TypeKind::Map(key_type, value_type) => { + self.analyze_type(key_type); + self.analyze_type(value_type); + } + TypeKind::Set(element_type) => { + self.analyze_type(element_type); + } + TypeKind::Vec(element_type) => { + self.analyze_type(element_type); + } + TypeKind::Chan(element_type) => { + self.analyze_type(element_type); + } + TypeKind::Arr(length_expr, length, element_type) => { + self.analyze_expr(length_expr); + if let AstNode::Literal(literal_kind, literal_value) = &mut length_expr.node { + if Type::is_integer(literal_kind) { + if let Ok(parsed_length) = literal_value.parse::() { + *length = parsed_length as u64; + } else { + errors_push( + self.module, + AnalyzerError { + start: t.start, + end: t.end, + message: "array length must be constans or integer literal".to_string(), + is_warning: false, + }, + ); + } + } else { + errors_push( + self.module, + AnalyzerError { + start: t.start, + end: t.end, + message: "array length must be constans or integer literal".to_string(), + is_warning: false, + }, + ); + } + } else { + // error push + errors_push( + self.module, + AnalyzerError { + start: t.start, + end: t.end, + message: "array length must be constans or integer literal".to_string(), + is_warning: false, + }, + ); + } + + self.analyze_type(element_type); + } + TypeKind::Tuple(elements, _align) => { + for element in elements { + self.analyze_type(element); + } + } + TypeKind::Ref(value_type) => { + self.analyze_type(value_type); + } + TypeKind::Ptr(value_type) => { + self.analyze_type(value_type); + } + TypeKind::Fn(fn_type) => { + self.analyze_type(&mut fn_type.return_type); + + for param_type in &mut fn_type.param_types { + self.analyze_type(param_type); + } + } + TypeKind::Struct(_ident, _, properties) => { + for property in properties.iter_mut() { + self.analyze_type(&mut property.type_); + + // 可选的又值 + if let Some(value) = &mut property.value { + self.analyze_expr(value); + + // value kind cannot is fndef + if let AstNode::FnDef(..) = value.node { + errors_push( + self.module, + AnalyzerError { + start: value.start, + end: value.end, + message: format!("struct field default value cannot be a fn def, use fn def ident instead"), + is_warning: false, + }, + ); + t.err = true; + } + } + } + } + TypeKind::Enum(element_type, properties) => { + // Analyze element type + self.analyze_type(element_type); + + // Analyze value expressions for each enum member + for property in properties.iter_mut() { + if let Some(value_expr) = &mut property.value_expr { + self.analyze_expr(value_expr); + } + } + } + TypeKind::TaggedUnion(_ident, elements) => { + for element in elements.iter_mut() { + self.analyze_type(&mut element.type_); + } + } + _ => { + return; + } + } + } + + /** + * Update the semantic token type for an identifier based on its resolved symbol kind. + * This ensures function references get FUNCTION coloring, types get TYPE, etc. + */ + pub(crate) fn update_ident_token_type(&mut self, start: usize, end: usize, symbol_id: NodeId) { + if symbol_id == 0 { + return; + } + let sem_type = if let Some(symbol) = self.symbol_table.get_symbol_ref(symbol_id) { + match &symbol.kind { + SymbolKind::Fn(_) => SemanticTokenType::FUNCTION, + SymbolKind::Type(_) => SemanticTokenType::TYPE, + SymbolKind::Const(_) => SemanticTokenType::MACRO, + SymbolKind::Var(_) => SemanticTokenType::VARIABLE, + } + } else { + return; + }; + let sem_idx = semantic_token_type_index(sem_type); + for token in self.module.sem_token_db.iter_mut() { + if token.start == start && token.end == end { + token.semantic_token_type = sem_idx; + break; + } + } + } + + /// Update the semantic token type for a token found by its end position (for select expr keys). + pub(crate) fn update_ident_token_by_end(&mut self, end: usize, symbol_id: NodeId) { + if symbol_id == 0 { + return; + } + let sem_type = if let Some(symbol) = self.symbol_table.get_symbol_ref(symbol_id) { + match &symbol.kind { + SymbolKind::Fn(_) => SemanticTokenType::FUNCTION, + SymbolKind::Type(_) => SemanticTokenType::TYPE, + SymbolKind::Const(_) => SemanticTokenType::MACRO, + SymbolKind::Var(_) => SemanticTokenType::PROPERTY, + } + } else { + return; + }; + let sem_idx = semantic_token_type_index(sem_type); + for token in self.module.sem_token_db.iter_mut().rev() { + if token.end == end && token.token_type == super::lexer::TokenType::Ident { + token.semantic_token_type = sem_idx; + break; + } + } + } + + /** + * 验证选择性导入的符号是否存在于目标模块中, + * 并根据符号类型设置正确的语义 token 颜色。 + * 例如: import co.mutex.{mutex_t} 时验证 mutex_t 是否真实存在 + */ + fn validate_selective_imports(&mut self) { + // Collect token updates first to avoid borrow conflicts + let mut token_updates: Vec<(usize, usize, usize)> = Vec::new(); // (start, end, sem_idx) + let mut alias_updates: Vec<(usize, usize, usize)> = Vec::new(); // (item_start, item_end, sem_idx) + let mut errors: Vec<(usize, usize, String, String)> = Vec::new(); + + for import in &self.imports { + if !import.is_selective { + continue; + } + + let Some(ref items) = import.select_items else { continue }; + + for item in items { + let global_ident = format_global_ident(import.module_ident.clone(), item.ident.clone()); + if let Some(symbol_id) = self.symbol_table.find_symbol_id(&global_ident, self.symbol_table.global_scope_id) { + // Classify the import symbol's semantic token based on what it actually is + if let Some(symbol) = self.symbol_table.get_symbol_ref(symbol_id) { + let sem_type = match &symbol.kind { + SymbolKind::Fn(_) => SemanticTokenType::FUNCTION, + SymbolKind::Type(_) => SemanticTokenType::TYPE, + SymbolKind::Const(_) => SemanticTokenType::MACRO, + SymbolKind::Var(_) => SemanticTokenType::VARIABLE, + }; + let sem_idx = semantic_token_type_index(sem_type); + token_updates.push((item.start, item.end, sem_idx)); + + if item.alias.is_some() { + alias_updates.push((item.start, item.end, sem_idx)); + } + } + continue; + } + + errors.push((import.start, import.end, item.ident.clone(), import.module_ident.clone())); + } + } + + // Apply semantic token updates + for (start, end, sem_idx) in token_updates { + for token in self.module.sem_token_db.iter_mut() { + if token.start == start && token.end == end { + token.semantic_token_type = sem_idx; + break; + } + } + } + + // Apply alias token updates (alias token is after the original ident within the item range) + for (item_start, item_end, sem_idx) in alias_updates { + for token in self.module.sem_token_db.iter_mut() { + if token.start > item_start && token.end == item_end { + token.semantic_token_type = sem_idx; + break; + } + } + } + + // Report errors + for (start, end, ident, module_ident) in errors { + errors_push( + self.module, + AnalyzerError { + start, + end, + message: format!("symbol '{}' not found in module '{}'", ident, module_ident), + is_warning: false, + }, + ); + } + } + + /** + * analyze 之前,相关 module 的 global symbol 都已经注册完成, 这里不能再重复注册了。 + */ + pub fn analyze(&mut self) { + // 验证选择性导入的符号是否存在 + self.validate_selective_imports(); + + let mut global_fn_stmt_list = Vec::>>::new(); + + let mut stmts = Vec::>::new(); + + let mut global_vardefs = Vec::new(); + + // 跳过 import + for i in 0..self.stmts.len() { + // 使用 clone 避免对 self 所有权占用 + let mut stmt = self.stmts[i].clone(); + + match &mut stmt.node { + AstNode::Import(..) => continue, + AstNode::FnDef(fndef_mutex) => { + let mut fndef = fndef_mutex.lock().unwrap(); + let symbol_name = fndef.symbol_name.clone(); + + if fndef.impl_type.kind.is_exist() { + let mut impl_type_ident = fndef.impl_type.ident.clone(); + + if Type::is_impl_builtin_type(&fndef.impl_type.kind) { + impl_type_ident = fndef.impl_type.kind.to_string(); + } + + // 非 builtin type 则进行 resolve type 查找 + if !Type::is_impl_builtin_type(&fndef.impl_type.kind) { + // resolve global ident + if let Some(symbol_id) = self.resolve_typedef(&mut fndef.impl_type.ident) { + // ident maybe change + fndef.impl_type.symbol_id = symbol_id; + impl_type_ident = fndef.impl_type.ident.clone(); + + // 自定义泛型 impl type 必须显式给出类型参数(仅检查 impl_type.args) + if let Some(symbol) = self.symbol_table.get_symbol(symbol_id) { + if let SymbolKind::Type(typedef_mutex) = &symbol.kind { + let typedef = typedef_mutex.lock().unwrap(); + if !typedef.params.is_empty() && fndef.impl_type.args.len() != typedef.params.len() { + errors_push( + self.module, + AnalyzerError { + start: fndef.symbol_start, + end: fndef.symbol_end, + message: format!("impl type '{}' must specify generics params", fndef.impl_type.ident), + is_warning: false, + }, + ); + } + } + } + } else { + errors_push( + self.module, + AnalyzerError { + start: fndef.symbol_start, + end: fndef.symbol_end, + message: format!("type '{}' undeclared", fndef.impl_type.symbol_id), + is_warning: false, + }, + ); + } + } + + fndef.symbol_name = format_impl_ident(impl_type_ident, symbol_name); + + // register to global symbol table + match self.symbol_table.define_symbol_in_scope( + fndef.symbol_name.clone(), + SymbolKind::Fn(fndef_mutex.clone()), + fndef.symbol_start, + self.module.scope_id, + ) { + Ok(symbol_id) => { + fndef.symbol_id = symbol_id; + } + Err(e) => { + errors_push( + self.module, + AnalyzerError { + start: fndef.symbol_start, + end: fndef.symbol_end, + message: e, + is_warning: false, + }, + ); + } + } + + // register to global symbol + let _ = self.symbol_table.define_global_symbol( + fndef.symbol_name.clone(), + SymbolKind::Fn(fndef_mutex.clone()), + fndef.symbol_start, + self.module.scope_id, + ); + } + + global_fn_stmt_list.push(fndef_mutex.clone()); + + if let Some(generics_params) = &mut fndef.generics_params { + for generics_param in generics_params { + for constraint in &mut generics_param.constraints { + self.analyze_type(constraint); + } + } + } + } + AstNode::VarDef(var_decl_mutex, right_expr) => { + let mut var_decl = var_decl_mutex.lock().unwrap(); + self.analyze_type(&mut var_decl.type_); + + // push to global_vardef + global_vardefs.push(AstNode::VarDef(var_decl_mutex.clone(), right_expr.clone())); + } + + AstNode::Typedef(type_alias_mutex) => { + let mut type_expr = { + let mut typedef = type_alias_mutex.lock().unwrap(); + + // 处理 params constraints, type foo = ... + if typedef.params.len() > 0 { + for param in typedef.params.iter_mut() { + // 遍历所有 constraints 类型 进行 analyze + for constraint in &mut param.constraints { + // TODO constraint 不能是自身 + self.analyze_type(constraint); + } + } + } + + if typedef.impl_interfaces.len() > 0 { + for impl_interface in &mut typedef.impl_interfaces { + debug_assert!(impl_interface.kind == TypeKind::Ident && impl_interface.ident_kind == TypeIdentKind::Interface); + self.analyze_type(impl_interface); + } + } + + // analyzer type expr, symbol table 中存储的是 type_expr 的 arc clone, 所以这里的修改会同步到 symbol table 中 + // 递归依赖处理 + typedef.type_expr.clone() + }; + + self.analyze_type(&mut type_expr); + + { + let mut typedef = type_alias_mutex.lock().unwrap(); + typedef.type_expr = type_expr; + } + } + + AstNode::ConstDef(const_mutex) => { + let mut constdef = const_mutex.lock().unwrap(); + self.analyze_expr(&mut constdef.right); + + if !matches!(constdef.right.node, AstNode::Literal(..)) { + errors_push( + self.module, + AnalyzerError { + start: constdef.symbol_start, + end: constdef.symbol_end, + message: format!("const cannot be initialized"), + is_warning: false, + }, + ); + } + } + _ => { + // 语义分析中包含许多错误 + } + } + + // 归还 stmt list + stmts.push(stmt); + } + + // 对 fn stmt list 进行 analyzer 处理。 + for fndef_mutex in &global_fn_stmt_list { + self.module.all_fndefs.push(fndef_mutex.clone()); + self.analyze_global_fn(fndef_mutex.clone()); + } + + // global vardef 的右值不在函数体里,需要独立做 analyze + for node in &mut global_vardefs { + match node { + AstNode::VarDef(_, right_expr) => { + if let AstNode::FnDef(_) = &right_expr.node { + // fn def 会自动 arc 引用传递,这里无需重复处理 + } else { + self.analyze_expr(right_expr); + } + } + _ => {} + } + } + + self.module.stmts = stmts; + self.module.global_vardefs = global_vardefs; + self.module.global_fndefs = global_fn_stmt_list; + self.module.analyzer_errors.extend(self.errors.clone()); + } + + pub fn resolve_typedef(&mut self, ident: &mut String) -> Option { + // 首先尝试在当前作用域和父级作用域中直接查找该符号, 最终会找到 m.scope_id, 这里包含当前 module 的全局符号 + if let Some(symbol_id) = self.symbol_table.lookup_symbol(ident, self.current_scope_id) { + return Some(symbol_id); + } + + // 首先尝试在当前 module 中查找该符号 + if let Some(symbol_id) = self.symbol_table.find_module_symbol_id(&self.module.ident, ident) { + let current_module_ident = format_global_ident(self.module.ident.clone(), ident.to_string()); + *ident = current_module_ident; + return Some(symbol_id); + } + + // Check selective imports: import math.{sqrt, pow, Point} + for import in &self.imports { + if !import.is_selective { + continue; + } + let Some(ref items) = import.select_items else { continue }; + for item in items { + let local_name = item.alias.as_ref().unwrap_or(&item.ident); + if local_name != ident { + continue; + } + let global_ident = format_global_ident(import.module_ident.clone(), item.ident.clone()); + if let Some(id) = self.symbol_table.find_symbol_id(&global_ident, self.symbol_table.global_scope_id) { + *ident = global_ident; + return Some(id); + } + } + } + + // import x as * 产生的全局符号 + for i in &self.imports { + if i.as_name != "*" { + continue; + }; + + if let Some(symbol_id) = self.symbol_table.find_module_symbol_id(&i.module_ident, ident) { + *ident = format_global_ident(i.module_ident.clone(), ident.to_string()); + return Some(symbol_id); + } + } + + // builtin 全局符号,不需要进行 format 链接,直接读取 global 符号表 + return self.symbol_table.find_symbol_id(ident, self.symbol_table.global_scope_id); + } + + pub fn symbol_typedef_add_method(&mut self, typedef_ident: String, method_ident: String, fndef: Arc>) -> Result<(), String> { + // get typedef from symbol table(global symbol) + let symbol = self + .symbol_table + .find_global_symbol(&typedef_ident) + .ok_or_else(|| format!("symbol {} not found", typedef_ident))?; + let SymbolKind::Type(typedef_mutex) = &symbol.kind else { + return Err(format!("symbol {} is not typedef", typedef_ident)); + }; + let mut typedef = typedef_mutex.lock().unwrap(); + typedef.method_table.insert(method_ident, fndef); + + return Ok(()); + } +} diff --git a/nls/src/analyzer/syntax.rs b/nls/src/analyzer/syntax.rs index 0070649c..7bf6bce5 100644 --- a/nls/src/analyzer/syntax.rs +++ b/nls/src/analyzer/syntax.rs @@ -185,6 +185,42 @@ impl<'a> Syntax { self.token_db[self.token_indexes[self.current]].semantic_token_type = semantic_token_type_index(token_type); } + /// Set the semantic token type for the previously consumed token (current - 1). + #[allow(dead_code)] + fn set_prev_token_type(&mut self, token_type: SemanticTokenType) { + if self.current == 0 { + return; + } + self.token_db[self.token_indexes[self.current - 1]].semantic_token_type = semantic_token_type_index(token_type); + } + + /// Set the semantic token type for a token found by its char-offset position. + fn set_token_type_at_pos(&mut self, pos: usize, token_type: SemanticTokenType) { + let idx = semantic_token_type_index(token_type); + for token in self.token_db.iter_mut() { + if token.start == pos && token.token_type == TokenType::Ident { + token.semantic_token_type = idx; + return; + } + } + } + + /// Reset semantic token types after speculative parsing (e.g. is_type_begin_stmt). + /// Speculative calls to parser_type() mark Ident tokens as TYPE/KEYWORD as a + /// side effect. This undoes those changes and restores the parser position so + /// the actual parse can re-set them correctly. + fn reset_speculative_tokens(&mut self, saved_pos: usize) { + let end_pos = self.current; + self.current = saved_pos; + let var_idx = semantic_token_type_index(SemanticTokenType::VARIABLE); + for i in saved_pos..end_pos.min(self.token_indexes.len()) { + let token_idx = self.token_indexes[i]; + if self.token_db[token_idx].token_type == TokenType::Ident { + self.token_db[token_idx].semantic_token_type = var_idx; + } + } + } + fn advance(&mut self) -> &Token { debug_assert!(self.current + 1 < self.token_indexes.len(), "Syntax::advance: current index out of range"); @@ -702,7 +738,8 @@ impl<'a> Syntax { start: e.0, end: e.1, message: e.2, - }, + is_warning: false, + }, ); // 查找到下一个同步点 @@ -748,7 +785,8 @@ impl<'a> Syntax { start: e.0, end: e.1, message: e.2, - }, + is_warning: false, + }, ); let found = self.synchronize(1); @@ -949,6 +987,7 @@ impl<'a> Syntax { let token = self.peek(); // vec if token.literal == "vec" { + self.set_current_token_type(SemanticTokenType::KEYWORD); self.must(TokenType::Ident)?; self.must(TokenType::LeftAngle)?; let element_type = self.parser_type()?; @@ -961,6 +1000,7 @@ impl<'a> Syntax { // map if token.literal == "map" { + self.set_current_token_type(SemanticTokenType::KEYWORD); self.must(TokenType::Ident)?; self.must(TokenType::LeftAngle)?; let key_type = self.parser_type()?; @@ -975,6 +1015,7 @@ impl<'a> Syntax { // set if token.literal == "set" { + self.set_current_token_type(SemanticTokenType::KEYWORD); self.must(TokenType::Ident)?; self.must(TokenType::LeftAngle)?; let element_type = self.parser_type()?; @@ -987,6 +1028,7 @@ impl<'a> Syntax { // tup if token.literal == "tup" { + self.set_current_token_type(SemanticTokenType::KEYWORD); self.must(TokenType::Ident)?; self.must(TokenType::LeftAngle)?; let mut elements = Vec::new(); @@ -1114,7 +1156,13 @@ impl<'a> Syntax { // ident foo = 12 if self.is(TokenType::Ident) { - self.set_current_token_type(SemanticTokenType::TYPE); + // Mark as TYPE by default; override to KEYWORD for context-sensitive keywords. + let is_type_keyword = matches!(self.peek().literal.as_str(), "ptr" | "ref"); + if is_type_keyword { + self.set_current_token_type(SemanticTokenType::KEYWORD); + } else { + self.set_current_token_type(SemanticTokenType::TYPE); + } let first = self.must(TokenType::Ident)?.clone(); // ------------- handle param @@ -1258,7 +1306,8 @@ impl<'a> Syntax { start: field_type.start, end: field_type.end, message: format!("struct field '{}' already exists", field_name), - }, + is_warning: false, + }, ); } exists.insert(field_name.clone(), 1); @@ -1315,7 +1364,8 @@ impl<'a> Syntax { start: fn_name_token.start, end: fn_name_token.end, message: format!("interface method '{}' already exists", fn_name), - }, + is_warning: false, + }, ); } exists.insert(fn_name.clone(), 1); @@ -1345,7 +1395,8 @@ impl<'a> Syntax { start: element_type.start, end: element_type.end, message: format!("enum only supports integer types"), - }, + is_warning: false, + }, ); } @@ -1365,7 +1416,8 @@ impl<'a> Syntax { start: name_token.start, end: name_token.end, message: format!("enum member '{}' already exists", name), - }, + is_warning: false, + }, ); } exists.insert(name.clone(), 1); @@ -1403,7 +1455,8 @@ impl<'a> Syntax { start: element_type.start, end: element_type.end, message: format!("union tag '{}' already exists", tag_name), - }, + is_warning: false, + }, ); } exists.insert(tag_name.clone(), 1); @@ -1653,13 +1706,13 @@ impl<'a> Syntax { // 尝试解析第一个类型 if let Err(_) = self.parser_type() { // 类型解析存在错误 - self.current = current_pos; + self.reset_speculative_tokens(current_pos); return false; } // 检查是否直接以 > 结束 (大多数情况) if self.is(TokenType::RightAngle) { - self.current = current_pos; + self.reset_speculative_tokens(current_pos); return true; } @@ -1667,7 +1720,7 @@ impl<'a> Syntax { // 处理多个类型参数的情况 loop { if let Err(_) = self.parser_type() { - self.current = current_pos; + self.reset_speculative_tokens(current_pos); return false; } @@ -1677,21 +1730,21 @@ impl<'a> Syntax { } if !self.is(TokenType::RightAngle) { - self.current = current_pos; + self.reset_speculative_tokens(current_pos); return false; } // type args 后面不能紧跟 { 或 (, 这两者通常是 generics params if !self.next_is(1, TokenType::LeftCurly) && !self.next_is(1, TokenType::LeftParen) { - self.current = current_pos; + self.reset_speculative_tokens(current_pos); return false; } - self.current = current_pos; + self.reset_speculative_tokens(current_pos); return true; } - self.current = current_pos; + self.reset_speculative_tokens(current_pos); return false; } @@ -2095,6 +2148,20 @@ impl<'a> Syntax { self.must(TokenType::Dot)?; + // Error recovery: if the next token isn't an ident (e.g., user is mid-typing "x."), + // create a partial SelectExpr with empty key so the rest of the file still parses. + if !self.is(TokenType::Ident) { + expr.start = left.start; + expr.end = self.prev().unwrap().end; + expr.node = AstNode::SelectExpr(left, String::new(), None); + return Ok(expr); + } + + // Mark the property token as PROPERTY by default. + // If this select_expr is the target of a call (e.g. `obj.method()`), + // parser_call_expr will override this to FUNCTION later. + self.set_current_token_type(SemanticTokenType::PROPERTY); + let property_token = self.must(TokenType::Ident)?; expr.start = left.start; expr.node = AstNode::SelectExpr(left, property_token.literal.clone(), None); @@ -2149,6 +2216,26 @@ impl<'a> Syntax { let mut expr = self.expr_new(); expr.start = left.start; + // Mark the call target identifier as FUNCTION for semantic highlighting. + match &left.node { + AstNode::Ident(_, _) => { + // Simple call: `create(...)` → mark `create` as FUNCTION + self.set_token_type_at_pos(left.start, SemanticTokenType::FUNCTION); + } + AstNode::SelectExpr(_, key, _) if !key.is_empty() => { + // Method call: `obj.method(...)` → find the key token and mark it FUNCTION. + // The key token is the last Ident in the SelectExpr (ends at left.end). + let idx = semantic_token_type_index(SemanticTokenType::FUNCTION); + for token in self.token_db.iter_mut().rev() { + if token.end == left.end && token.token_type == TokenType::Ident { + token.semantic_token_type = idx; + break; + } + } + } + _ => {} + } + let mut call = AstCall { return_type: Type::unknown(), left, @@ -2263,8 +2350,8 @@ impl<'a> Syntax { } } - // 恢复解析位置 - self.current = current_pos; + // Undo speculative semantic token changes and restore position + self.reset_speculative_tokens(current_pos); result } @@ -2324,7 +2411,9 @@ impl<'a> Syntax { _ => {} } - self.current = current_pos; + // Undo speculative semantic token changes and restore position + self.reset_speculative_tokens(current_pos); + result } @@ -2533,7 +2622,7 @@ impl<'a> Syntax { break; } - let ident = self.must(TokenType::Ident)?; + let ident = self.must(TokenType::Ident)?.clone(); // Check for space after dot: ident should start right after dot ends if ident.start != dot_token.end { @@ -2546,7 +2635,7 @@ impl<'a> Syntax { package.push(ident.literal.clone()); import_end = ident.end; - prev_token = ident.clone(); + prev_token = ident; } (None, Some(package)) } else { @@ -2569,15 +2658,27 @@ impl<'a> Syntax { let mut items = Vec::new(); loop { + // Tolerate incomplete input: if next token is not an ident, stop gracefully + if !self.is(TokenType::Ident) { + break; + } let ident_token = self.must(TokenType::Ident)?; let ident = ident_token.literal.clone(); + let item_start = ident_token.start; + let mut item_end = ident_token.end; let alias = if self.consume(TokenType::As) { - Some(self.must(TokenType::Ident)?.literal.clone()) + if self.is(TokenType::Ident) { + let alias_token = self.must(TokenType::Ident)?; + item_end = alias_token.end; + Some(alias_token.literal.clone()) + } else { + None // incomplete alias, tolerate it + } } else { None }; - items.push(ImportSelectItem { ident, alias }); + items.push(ImportSelectItem { ident, alias, start: item_start, end: item_end }); if !self.consume(TokenType::Comma) { break; @@ -2822,6 +2923,7 @@ impl<'a> Syntax { */ fn parser_new_expr(&mut self) -> Result, SyntaxError> { let mut expr = self.expr_new(); + self.set_current_token_type(SemanticTokenType::KEYWORD); self.must(TokenType::Ident)?; // ident = new let t = self.parser_type()?; diff --git a/nls/src/analyzer/typesys.rs b/nls/src/analyzer/typesys.rs index 143ce7ef..0843d20c 100644 --- a/nls/src/analyzer/typesys.rs +++ b/nls/src/analyzer/typesys.rs @@ -431,7 +431,8 @@ impl<'a> Typesys<'a> { start: result.start, end: result.end, message: format!("recycle use type '{}'", found.unwrap()), - }); + is_warning: false, + }); } return Ok(result); @@ -456,7 +457,8 @@ impl<'a> Typesys<'a> { start: typedef_stmt.symbol_start, end: typedef_stmt.symbol_end, message: "typedef type is not interface".to_string(), - }); + is_warning: false, + }); }; // 创建一个 HashMap 用于跟踪已存在的方法 @@ -488,7 +490,8 @@ impl<'a> Typesys<'a> { start: element.start, end: element.end, message: format!("duplicate method '{}'", type_fn.name), - }); + is_warning: false, + }); } continue; } @@ -514,7 +517,8 @@ impl<'a> Typesys<'a> { start: 0, end: 0, message: format!("typedef '{}' symbol_id not found", t.ident), - }); + is_warning: false, + }); } // 获取符号定义 @@ -522,7 +526,8 @@ impl<'a> Typesys<'a> { start, end, message: format!("typedef '{}' not found", t.ident), - })?; + is_warning: false, + })?; // 检查符号类型 let SymbolKind::Type(typedef_mutex) = symbol.kind.clone() else { @@ -530,7 +535,8 @@ impl<'a> Typesys<'a> { start, end, message: format!("'{}' is not a type", symbol.ident), - }); + is_warning: false, + }); }; { @@ -541,6 +547,7 @@ impl<'a> Typesys<'a> { start, end, message: format!("typedef '{}' args mismatch", t.ident), + is_warning: false, }); } @@ -582,7 +589,8 @@ impl<'a> Typesys<'a> { start, end, message: format!("typedef '{}' need params", t.ident), - }); + is_warning: false, + }); } if t.args.len() != generic_typedef.params.len() { @@ -594,7 +602,8 @@ impl<'a> Typesys<'a> { start, end, message: format!("typedef '{}' params mismatch", t.ident), - }); + is_warning: false, + }); } let mut args_table = HashMap::new(); @@ -620,7 +629,8 @@ impl<'a> Typesys<'a> { start, end, message: format!("generics constraint check failed: {}", e), - }); + is_warning: false, + }); } impl_args.push(arg.clone()); @@ -641,7 +651,7 @@ impl<'a> Typesys<'a> { for impl_interface in &generic_typedef.impl_interfaces { self.check_typedef_impl(impl_interface, t.ident.clone(), &generic_typedef) - .map_err(|e| AnalyzerError { start, end, message: e })?; + .map_err(|e| AnalyzerError { start, end, message: e, is_warning: false })?; } } } @@ -731,7 +741,7 @@ impl<'a> Typesys<'a> { if reduction_ident_depth_entered { Self::reduction_ident_depth_leave(visited, &t.ident); } - return Err(AnalyzerError { start, end, message: e }); + return Err(AnalyzerError { start, end, message: e, is_warning: false }); } } } @@ -791,7 +801,8 @@ impl<'a> Typesys<'a> { start: t.start, end: t.end, message: format!("type '{}' not support as map key", key_type), - }); + is_warning: false, + }); } result.ident_kind = TypeIdentKind::Builtin; @@ -809,7 +820,8 @@ impl<'a> Typesys<'a> { start: t.start, end: t.end, message: format!("type '{}' not support as set element", element_type), - }); + is_warning: false, + }); } result.ident_kind = TypeIdentKind::Builtin; @@ -824,7 +836,8 @@ impl<'a> Typesys<'a> { start: t.start, end: t.end, message: "tuple element empty".to_string(), - }); + is_warning: false, + }); } for element_type in elements.iter_mut() { @@ -883,7 +896,8 @@ impl<'a> Typesys<'a> { start: 0, end: 0, message: "unknown type".to_string(), - }) + is_warning: false, + }) } } @@ -913,7 +927,8 @@ impl<'a> Typesys<'a> { start: 0, end: 0, message: format!("type {} already has error", t), - }); + is_warning: false, + }); } if t.kind.is_unknown() { @@ -977,7 +992,8 @@ impl<'a> Typesys<'a> { start: t.start, end: t.end, message: format!("enum only supports integer types, got '{}'", element_type), - }); + is_warning: false, + }); } // 计算所有枚举成员的值 @@ -998,7 +1014,8 @@ impl<'a> Typesys<'a> { start: t.start, end: t.end, message: format!("enum member '{}' value must be a literal", prop.name), - }); + is_warning: false, + }); } } else { // 没有显式值,使用自动递增值 @@ -1030,7 +1047,8 @@ impl<'a> Typesys<'a> { start: 0, end: 0, message: "unknown type".to_string(), - }); + is_warning: false, + }); } } } @@ -1091,7 +1109,8 @@ impl<'a> Typesys<'a> { start: 0, end: 0, message: "unknown as source type".to_string(), - }); + is_warning: false, + }); } src.type_ = src_type.clone(); @@ -1103,7 +1122,8 @@ impl<'a> Typesys<'a> { start: as_expr.start, end: as_expr.end, message: "unexpected as expr, expected tagged union".to_string(), - }); + is_warning: false, + }); } self.infer_tagged_union_element(ut, src.type_.clone())?; @@ -1134,7 +1154,8 @@ impl<'a> Typesys<'a> { start: as_expr.start, end: as_expr.end, message: "union to union type is not supported".to_string(), - }); + is_warning: false, + }); } if !self.union_type_contains(&(*any, elements.clone()), &target_type) { @@ -1142,7 +1163,8 @@ impl<'a> Typesys<'a> { start: as_expr.start, end: as_expr.end, message: format!("type {} not contains in union type", target_type), - }); + is_warning: false, + }); } return Ok(target_type.clone()); @@ -1161,7 +1183,8 @@ impl<'a> Typesys<'a> { start: as_expr.start, end: as_expr.end, message: format!("type {} not impl interface", src_type), - }); + is_warning: false, + }); } // get symbol from symbol table @@ -1172,7 +1195,8 @@ impl<'a> Typesys<'a> { start: 0, end: 0, message: format!("type '{}' not found", temp_target_type.ident), - }); + is_warning: false, + }); } }; @@ -1186,7 +1210,8 @@ impl<'a> Typesys<'a> { start: as_expr.start, end: as_expr.end, message: format!("type '{}' not impl '{}' interface", temp_target_type.ident, src_type), - }); + is_warning: false, + }); } self.check_typedef_impl(&src_type, temp_target_type.ident.clone(), &typedef) @@ -1194,7 +1219,8 @@ impl<'a> Typesys<'a> { start: as_expr.start, end: as_expr.end, message: e, - })?; + is_warning: false, + })?; } else { unreachable!(); } @@ -1220,7 +1246,8 @@ impl<'a> Typesys<'a> { start: as_expr.start, end: as_expr.end, message: e, - })?; + is_warning: false, + })?; } return Ok(target_type.clone()); @@ -1257,7 +1284,8 @@ impl<'a> Typesys<'a> { start: as_expr.start, end: as_expr.end, message: format!("cannot casting to '{}'", target_type), - }); + is_warning: false, + }); } pub fn infer_match( @@ -1281,7 +1309,8 @@ impl<'a> Typesys<'a> { start: subject_expr.start, end: subject_expr.end, message: "match subject type not confirm".to_string(), - }); + is_warning: false, + }); } } @@ -1306,7 +1335,8 @@ impl<'a> Typesys<'a> { start: cond_expr.start, end: cond_expr.end, message: "match 'union' only support 'is' assert".to_string(), - }); + is_warning: false, + }); } } @@ -1317,7 +1347,8 @@ impl<'a> Typesys<'a> { start: cond_expr.start, end: cond_expr.end, message: format!("{} cannot use 'is' operator", subject_type), - }); + is_warning: false, + }); } // 处理 tagged union 的 is 匹配 @@ -1327,7 +1358,8 @@ impl<'a> Typesys<'a> { start: cond_expr.start, end: cond_expr.end, message: "tagged union match requires union tag".to_string(), - }); + is_warning: false, + }); }; // 推断 tagged union element @@ -1381,7 +1413,8 @@ impl<'a> Typesys<'a> { "match expression lacks a default case '_' and union element type lacks, for example 'is {}'", element_type ), - }); + is_warning: false, + }); } } } else { @@ -1389,7 +1422,8 @@ impl<'a> Typesys<'a> { start, end, message: "match expression lacks a default case '_'".to_string(), - }); + is_warning: false, + }); } } else if let TypeKind::TaggedUnion(_, elements) = &subject_type.kind { // tagged union 穷尽检查 @@ -1402,7 +1436,8 @@ impl<'a> Typesys<'a> { "match expression lacks a default case '_' and tagged union element lacks, for example 'is {}'", element.tag ), - }); + is_warning: false, + }); } } } else if let TypeKind::Enum(_, properties) = &subject_type.kind { @@ -1413,7 +1448,8 @@ impl<'a> Typesys<'a> { start, end, message: format!("match expression lacks a default case '_' and enum value lacks, for example '{}'", prop.name), - }); + is_warning: false, + }); } } } else { @@ -1421,7 +1457,8 @@ impl<'a> Typesys<'a> { start, end, message: "match expression lacks a default case '_'".to_string(), - }); + is_warning: false, + }); } } @@ -1446,7 +1483,8 @@ impl<'a> Typesys<'a> { start: property.start, end: property.end, message: format!("not found property '{}'", property.key), - })?; + is_warning: false, + })?; exists.insert(property.key.clone(), true); @@ -1493,7 +1531,8 @@ impl<'a> Typesys<'a> { start: start, end: end, message: format!("struct field '{}' must be assigned default value", type_prop.name), - }); + is_warning: false, + }); } } @@ -1516,7 +1555,8 @@ impl<'a> Typesys<'a> { start: 0, end: 0, message: "unknown binary expr left type".to_string(), - }); + is_warning: false, + }); } let right_target_type = if let TypeKind::Union(..) = left_type.kind.clone() { @@ -1534,7 +1574,8 @@ impl<'a> Typesys<'a> { start: left.start, end: right.end, message: format!("binary type inconsistency: left is '{}', right is '{}'", left_type, right_type), - }); + is_warning: false, + }); } // 处理数值类型运算 @@ -1548,7 +1589,8 @@ impl<'a> Typesys<'a> { "binary operator '{}' only support number operand, actual '{} {} {}'", op, left_type, op, right_type ), - }); + is_warning: false, + }); } } @@ -1563,7 +1605,8 @@ impl<'a> Typesys<'a> { "binary operator '{}' only support string operand, actual '{} {} {}'", op, left_type, op, right_type ), - }); + is_warning: false, + }); } } @@ -1576,7 +1619,8 @@ impl<'a> Typesys<'a> { "binary operator '{}' only support bool operand, actual '{} {} {}'", op, left_type, op, right_type ), - }); + is_warning: false, + }); } } @@ -1588,7 +1632,8 @@ impl<'a> Typesys<'a> { start: left.start, end: right.end, message: format!("binary operator '{}' only integer operand", op), - }); + is_warning: false, + }); } } @@ -1605,7 +1650,8 @@ impl<'a> Typesys<'a> { start: 0, end: 0, message: format!("unknown operator '{}'", op), - }) + is_warning: false, + }) } } @@ -1630,7 +1676,8 @@ impl<'a> Typesys<'a> { start: condition.start, end: condition.end, message: format!("ternary condition must be bool, pointer, or nullable type, actual '{}'", cond_type), - }); + is_warning: false, + }); } // Infer consequent expression with target type @@ -1645,7 +1692,8 @@ impl<'a> Typesys<'a> { start: consequent.start, end: alternate.end, message: format!("ternary branches must have compatible types: '{}' vs '{}'", consequent_type, alternate_type), - }); + is_warning: false, + }); } Ok(consequent_type) @@ -1657,7 +1705,8 @@ impl<'a> Typesys<'a> { start: operand.start, end: operand.end, message: format!("unary operator '{}' cannot use void as target type", op), - }); + is_warning: false, + }); } // 处理逻辑非运算符 @@ -1677,7 +1726,8 @@ impl<'a> Typesys<'a> { start: operand.start, end: operand.end, message: format!("neg(-) must use in number, actual '{}'", operand_type), - }); + is_warning: false, + }); } // 处理取地址运算符 & @@ -1688,7 +1738,8 @@ impl<'a> Typesys<'a> { start: operand.start, end: operand.end, message: "cannot load address of an literal or call".to_string(), - }); + is_warning: false, + }); } return Ok(Type::ptr_of(operand_type)); @@ -1706,7 +1757,8 @@ impl<'a> Typesys<'a> { start: operand.start, end: operand.end, message: format!("cannot dereference non-pointer type '{}'", operand_type), - }); + is_warning: false, + }); } } } @@ -1721,7 +1773,8 @@ impl<'a> Typesys<'a> { start: 0, end: 0, message: format!("ident '{}' symbol_id is none", ident), - }); + is_warning: false, + }); }; let symbol = self.symbol_table.get_symbol(*symbol_id).unwrap(); @@ -1743,7 +1796,8 @@ impl<'a> Typesys<'a> { start: 0, end: 0, message: format!("generics symbol rewrite failed, new ident '{}' not found", new_ident), - }); + is_warning: false, + }); } } } @@ -1758,7 +1812,8 @@ impl<'a> Typesys<'a> { start: 0, end: 0, message: "unknown type".to_string(), - }); + is_warning: false, + }); } debug_assert!(var_decl.type_.kind.is_exist()); @@ -1773,7 +1828,8 @@ impl<'a> Typesys<'a> { start, end, message: format!("generic fn `{}` cannot be passed as ident", fndef.fn_name), - }); + is_warning: false, + }); } } @@ -1784,7 +1840,8 @@ impl<'a> Typesys<'a> { start: start, end: end, message: "symbol of 'type' cannot be used as an identity".to_string(), - }); + is_warning: false, + }); } } } @@ -1835,7 +1892,8 @@ impl<'a> Typesys<'a> { start: expr.start, end: expr.end, message: "vec element type not confirm".to_string(), - }); + is_warning: false, + }); } let mut result = Type::undo_new(TypeKind::Vec(Box::new(element_type))); if infer_target_type.kind.is_exist() { @@ -1897,14 +1955,16 @@ impl<'a> Typesys<'a> { start, end, message: "map key type not confirm".to_string(), - }); + is_warning: false, + }); } if !self.type_confirm(&value_type) { return Err(AnalyzerError { start, end, message: "map value type not confirm".to_string(), - }); + is_warning: false, + }); } return self.reduction_type(Type::undo_new(TypeKind::Map(Box::new(key_type), Box::new(value_type)))); } @@ -1953,7 +2013,8 @@ impl<'a> Typesys<'a> { start, end, message: "empty set element type not confirm".to_string(), - }); + is_warning: false, + }); } return self.reduction_type(Type::undo_new(TypeKind::Set(Box::new(element_type)))); } @@ -1988,7 +2049,8 @@ impl<'a> Typesys<'a> { start, end, message: "tuple elements empty".to_string(), - }); + is_warning: false, + }); } // 收集所有元素的类型 @@ -2009,7 +2071,8 @@ impl<'a> Typesys<'a> { start, end, message: "tuple element type cannot be confirmed".to_string(), - }); + is_warning: false, + }); } element_types.push(expr_type); @@ -2049,19 +2112,22 @@ impl<'a> Typesys<'a> { start: length_expr.start, end: length_expr.end, message: "array length must be integer literal".to_string(), - }); + is_warning: false, + }); } value.parse::().map_err(|_| AnalyzerError { start: length_expr.start, end: length_expr.end, message: "invalid array length".to_string(), - })? + is_warning: false, + })? } else { return Err(AnalyzerError { start: length_expr.start, end: length_expr.end, message: "array length must be constant".to_string(), - }); + is_warning: false, + }); }; // 检查长度是否大于0 @@ -2070,7 +2136,8 @@ impl<'a> Typesys<'a> { start: length_expr.start, end: length_expr.end, message: "array length must be greater than 0".to_string(), - }); + is_warning: false, + }); } let result = Type::undo_new(TypeKind::Arr(Box::new(Expr::default()), length as u64, element_type.clone())); @@ -2158,7 +2225,8 @@ impl<'a> Typesys<'a> { start: key.start, end: key.end, message: "tuple index must be integer literal".to_string(), - }); + is_warning: false, + }); } value.parse::().unwrap_or(u64::MAX) } else { @@ -2166,7 +2234,8 @@ impl<'a> Typesys<'a> { start: key.start, end: key.end, message: "tuple index must be immediate value".to_string(), - }); + is_warning: false, + }); }; // 检查索引是否越界 @@ -2175,7 +2244,8 @@ impl<'a> Typesys<'a> { start: key.start, end: key.end, message: format!("tuple index {} out of range", index), - }); + is_warning: false, + }); } let element_type = elements[index as usize].clone(); @@ -2191,7 +2261,8 @@ impl<'a> Typesys<'a> { start: expr.start, end: expr.end, message: format!("access only support map/vec/string/array/tuple, cannot '{}'", left_type), - }) + is_warning: false, + }) } /// 尝试推断 enum 成员访问表达式 (如 Color.RED) @@ -2237,7 +2308,8 @@ impl<'a> Typesys<'a> { start, end, message: format!("enum '{}' has no member '{}'", left_ident, key), - }); + is_warning: false, + }); }; let Some(value) = &prop.value else { @@ -2245,7 +2317,8 @@ impl<'a> Typesys<'a> { start, end, message: format!("enum '{}' member '{}' has no value", left_ident, key), - }); + is_warning: false, + }); }; // 返回需要改写的节点和类型 @@ -2265,7 +2338,8 @@ impl<'a> Typesys<'a> { start: expr.start, end: expr.end, message: "expected tagged union element".to_string(), - }); + is_warning: false, + }); }; // 如果 target_type 存在且 union_type 未设置,使用 target_type @@ -2280,7 +2354,8 @@ impl<'a> Typesys<'a> { start: expr.start, end: expr.end, message: format!("type inconsistency, expect={}, actual={}", target_type.ident, union_type.ident), - }); + is_warning: false, + }); } *union_type = target_type.clone(); } @@ -2293,7 +2368,8 @@ impl<'a> Typesys<'a> { start: expr.start, end: expr.end, message: format!("expected tagged union type, got {}", union_type), - }); + is_warning: false, + }); }; // 查找匹配的 variant @@ -2303,7 +2379,8 @@ impl<'a> Typesys<'a> { start: expr.start, end: expr.end, message: format!("enum '{}' has no variant '{}'", union_type.ident, tagged_name), - }); + is_warning: false, + }); }; // 保存 element 引用 @@ -2320,7 +2397,8 @@ impl<'a> Typesys<'a> { start: expr.start, end: expr.end, message: "expected tagged union new".to_string(), - }); + is_warning: false, + }); }; // 如果 target_type 存在且 union_type 未设置,使用 target_type @@ -2335,7 +2413,8 @@ impl<'a> Typesys<'a> { start: expr.start, end: expr.end, message: format!("type inconsistency, expect={}, actual={}", target_type.ident, union_type.ident), - }); + is_warning: false, + }); } *union_type = target_type.clone(); } @@ -2348,7 +2427,8 @@ impl<'a> Typesys<'a> { start: expr.start, end: expr.end, message: format!("expected tagged union type, got {}", union_type), - }); + is_warning: false, + }); }; // 查找匹配的 variant @@ -2358,7 +2438,8 @@ impl<'a> Typesys<'a> { start: expr.start, end: expr.end, message: format!("enum '{}' has no variant '{}'", union_type.ident, tagged_name), - }); + is_warning: false, + }); }; // 保存 element 引用 @@ -2430,7 +2511,8 @@ impl<'a> Typesys<'a> { start: expr.start, end: expr.end, message: format!("type struct '{}' no field '{}'", deref_type.ident, key), - }); + is_warning: false, + }); } } @@ -2439,7 +2521,8 @@ impl<'a> Typesys<'a> { start: expr.start, end: expr.end, message: format!("no field named '{}' found in type '{}'", key, left_type), - }) + is_warning: false, + }) } pub fn infer_async(&mut self, expr: &mut Box) -> Result { @@ -2477,7 +2560,8 @@ impl<'a> Typesys<'a> { start: expr.start, end: expr.end, message: "async expression must call a fn".to_string(), - }); + is_warning: false, + }); } // 构造异步调用 @@ -2570,7 +2654,8 @@ impl<'a> Typesys<'a> { start: expr.start, end: expr.end, message: format!("{} cannot use 'is' operator", src_type), - }); + is_warning: false, + }); } // 处理 tagged union 的 union_tag @@ -2580,7 +2665,8 @@ impl<'a> Typesys<'a> { start: expr.start, end: expr.end, message: "unexpected is expr".to_string(), - }); + is_warning: false, + }); } self.infer_tagged_union_element(ut, src_type)?; } @@ -2626,7 +2712,8 @@ impl<'a> Typesys<'a> { start: expr.start, end: expr.end, message: "'new' operator can only be used with scalar types (number/boolean/struct/array)".to_string(), - }); + is_warning: false, + }); } if let Some(expr) = expr_option { @@ -2671,7 +2758,8 @@ impl<'a> Typesys<'a> { start: expr.start, end: expr.end, message: format!("empty curly new cannot ref type {}", infer_target_type), - }); + is_warning: false, + }); } } @@ -2690,7 +2778,8 @@ impl<'a> Typesys<'a> { start: expr.start, end: expr.end, message: format!("cannot use 'new' operator on non-struct type {}", type_), - }); + is_warning: false, + }); } return Ok(type_.clone()); @@ -2709,7 +2798,8 @@ impl<'a> Typesys<'a> { start: 0, end: 0, message: "unknown operand".to_string(), - }); + is_warning: false, + }); } }; } @@ -2745,7 +2835,8 @@ impl<'a> Typesys<'a> { start: expr.start, end: expr.end, message: format!("type '{}' cannot casting to interface '{}'", src_type, interface_type.ident), - }); + is_warning: false, + }); } if src_type.symbol_id == 0 { @@ -2753,7 +2844,8 @@ impl<'a> Typesys<'a> { start: 0, end: 0, message: "src type symbol id is zero".to_string(), - }); + is_warning: false, + }); } // 获取类型定义 @@ -2772,7 +2864,8 @@ impl<'a> Typesys<'a> { start: expr.start, end: expr.end, message: format!("type '{}' not impl '{}' interface", src_type.ident, interface_type.ident), - }); + is_warning: false, + }); } // 检查接口实现的完整性 @@ -2781,7 +2874,8 @@ impl<'a> Typesys<'a> { start: expr.start, end: expr.end, message: e, - })?; + is_warning: false, + })?; // 创建类型转换表达式 Ok(Box::new(Expr { @@ -2839,7 +2933,8 @@ impl<'a> Typesys<'a> { start: expr.start, end: expr.end, message: e.to_string(), - })?; + is_warning: false, + })?; if is_negative { i = -i; @@ -2854,7 +2949,8 @@ impl<'a> Typesys<'a> { start: expr.start, end: expr.end, message: format!("literal {} out of range for type '{}'", literal_value, infer_target_type), - }); + is_warning: false, + }); } return Ok(literal_type); @@ -2951,7 +3047,8 @@ impl<'a> Typesys<'a> { start: expr.start, end: expr.end, message: format!("union type not contains '{}'", expr.type_), - }); + is_warning: false, + }); } // expr 改成成 union 类型 @@ -2966,7 +3063,8 @@ impl<'a> Typesys<'a> { start: expr.start, end: expr.end, message: format!("type inconsistency: expect '{}', actual '{}'", target_type, expr.type_), - }); + is_warning: false, + }); } Ok(expr.type_.clone()) @@ -3010,7 +3108,8 @@ impl<'a> Typesys<'a> { start: expr.start, end: expr.end, message: "unary operand cannot used in left".to_string(), - }); + is_warning: false, + }); } } @@ -3018,7 +3117,8 @@ impl<'a> Typesys<'a> { start: expr.start, end: expr.end, message: "operand cannot be used as left value".to_string(), - }), + is_warning: false, + }), }; return match type_result { @@ -3052,7 +3152,8 @@ impl<'a> Typesys<'a> { start: var_decl.symbol_start, end: var_decl.symbol_end, message: "cannot assign to void".to_string(), - }); + is_warning: false, + }); } } @@ -3068,7 +3169,8 @@ impl<'a> Typesys<'a> { start: right_expr.start, end: right_expr.end, message: "cannot assign void to var".to_string(), - }); + is_warning: false, + }); } if matches!(var_decl.type_.kind, TypeKind::Unknown) { @@ -3078,7 +3180,8 @@ impl<'a> Typesys<'a> { start: right_expr.start, end: right_expr.end, message: "stmt right type not confirmed".to_string(), - }); + is_warning: false, + }); } // 使用右值类型作为变量类型 @@ -3100,7 +3203,8 @@ impl<'a> Typesys<'a> { start, end, message: format!("tuple length mismatch, expect {}, got {}", type_elements.len(), elements.len()), - }); + is_warning: false, + }); } // 遍历按顺序对比类型,并且顺便 rewrite @@ -3123,7 +3227,8 @@ impl<'a> Typesys<'a> { start: 0, end: 0, message: "var symbol id is zero, cannot infer".to_string(), - }); + is_warning: false, + }); } var_decl.type_ = target_type.clone(); @@ -3615,7 +3720,8 @@ impl<'a> Typesys<'a> { start: self_arg.start, end: self_arg.end, message: format!("type mismatch: method requires '{}' receiver, got '{}'", self_param_type, self_arg.type_), - }); + is_warning: false, + }); } if matches!(self_arg.type_.kind, TypeKind::Ref(_)) || self_arg.type_.is_heap_impl() { @@ -3626,7 +3732,8 @@ impl<'a> Typesys<'a> { start: self_arg.start, end: self_arg.end, message: format!("type mismatch: method requires '{}' receiver, got '{}'", self_param_type, self_arg.type_), - }); + is_warning: false, + }); } if matches!(self_param_type.kind, TypeKind::Ptr(_)) { @@ -3666,7 +3773,8 @@ impl<'a> Typesys<'a> { start, end, message: format!("symbol '{}' not found", ident), - }); + is_warning: false, + }); } let symbol = self.symbol_table.get_symbol(*symbol_id).unwrap(); @@ -3701,7 +3809,7 @@ impl<'a> Typesys<'a> { temp_fndef_mutex, module_scope_id, ) - .map_err(|e| AnalyzerError { start, end, message: e })?; + .map_err(|e| AnalyzerError { start, end, message: e, is_warning: false })?; let special_fn = special_fn.lock().unwrap(); @@ -3734,7 +3842,8 @@ impl<'a> Typesys<'a> { start, end, message: format!("type '{}' expects {} type argument(s), but got {}", typedef.ident, expected, actual), - }); + is_warning: false, + }); } } } @@ -3824,7 +3933,8 @@ impl<'a> Typesys<'a> { start, end, message: format!("type '{}' no impl fn '{}'", extract_type, key), - }); + is_warning: false, + }); } } } else { @@ -3832,14 +3942,16 @@ impl<'a> Typesys<'a> { start, end, message: format!("type '{}' no impl fn '{}'", extract_type, key), - }); + is_warning: false, + }); } } else { return Err(AnalyzerError { start, end, message: format!("type '{}' no impl fn '{}'", extract_type, key), - }); + is_warning: false, + }); } } }; @@ -3867,7 +3979,8 @@ impl<'a> Typesys<'a> { start, end, message: "cannot call non-fn".to_string(), - }); + is_warning: false, + }); }; let needs_self = match &call.left.node { @@ -3877,14 +3990,16 @@ impl<'a> Typesys<'a> { start, end, message: "symbol not found".to_string(), - }); + is_warning: false, + }); } let symbol = self.symbol_table.get_symbol(*symbol_id).ok_or(AnalyzerError { start, end, message: "symbol not found".to_string(), - })?; + is_warning: false, + })?; match &symbol.kind { SymbolKind::Fn(fndef_mutex) => { let fndef = fndef_mutex.lock().unwrap(); @@ -3900,7 +4015,8 @@ impl<'a> Typesys<'a> { start, end, message: format!("method '{}' requires a receiver; use a value instead of a type", key), - }); + is_warning: false, + }); } // 构建新的参数列表 @@ -3929,7 +4045,8 @@ impl<'a> Typesys<'a> { start, end, message: "symbol not found".to_string(), - })?; + is_warning: false, + })?; let SymbolKind::Type(typedef_mutex) = &symbol.kind else { return Ok(None); @@ -3988,7 +4105,8 @@ impl<'a> Typesys<'a> { start: expr.start, end: expr.end, message: "symbol not found".to_string(), - })?; + is_warning: false, + })?; let SymbolKind::Type(typedef_mutex) = &symbol.kind else { return Ok(false); }; @@ -4041,7 +4159,8 @@ impl<'a> Typesys<'a> { start: expr.start, end: expr.end, message: "symbol not found".to_string(), - })?; + is_warning: false, + })?; let SymbolKind::Type(typedef_mutex) = &symbol.kind else { return Ok(false); }; @@ -4093,7 +4212,8 @@ impl<'a> Typesys<'a> { start, end, message: format!("interface '{}' not declare '{}' fn", select_left_type.ident, key), - }); + is_warning: false, + }); } } } @@ -4117,7 +4237,8 @@ impl<'a> Typesys<'a> { start, end, message: "cannot call non-fn".to_string(), - }); + is_warning: false, + }); } Ok(left_type.kind) @@ -4142,7 +4263,8 @@ impl<'a> Typesys<'a> { if type_fn.name.is_empty() { "lambda".to_string() } else { type_fn.name }, current_fn.fn_name ), - }); + is_warning: false, + }); } } @@ -4271,7 +4393,8 @@ impl<'a> Typesys<'a> { start: right.start, end: right.end, message: format!("cannot assign {} to tuple", right_type), - }); + is_warning: false, + }); } self.infer_var_tuple_destr(elements, right_type, stmt.start, stmt.end)?; @@ -4283,7 +4406,8 @@ impl<'a> Typesys<'a> { start: left.start, end: left.end, message: format!("cannot assign to void"), - }); + is_warning: false, + }); } self.infer_right_expr(right, left_type)?; @@ -4358,7 +4482,8 @@ impl<'a> Typesys<'a> { message: "break or continue must in for body".to_string(), start: stmt.start, end: stmt.end, - }); + is_warning: false, + }); } } AstNode::ForTradition(init, condition, update, body) => { @@ -4531,8 +4656,13 @@ impl<'a> Typesys<'a> { return false; } - debug_assert!(!Type::ident_is_generics_param(&dst)); - debug_assert!(!Type::ident_is_generics_param(src)); + // Unresolved generics params can leak through when the generics pass + // hasn't fully specialised a type (e.g. Chan before instantiation). + // Returning false ("types are incomparable") is safe and avoids + // crashing the language server. + if Type::ident_is_generics_param(&dst) || Type::ident_is_generics_param(src) { + return false; + } // 检查类型状态 if dst.status != ReductionStatus::Done { @@ -4844,7 +4974,8 @@ impl<'a> Typesys<'a> { start: 0, end: 0, message: format!("cannot infer generics fn `{}`", fndef.fn_name), - }); + is_warning: false, + }); } // arg table 必须存在,且已经推导 @@ -4858,7 +4989,8 @@ impl<'a> Typesys<'a> { start: 0, end: 0, message: format!("cannot infer generics fn {}", fndef.fn_name), - }) + is_warning: false, + }) } }; } @@ -4903,7 +5035,8 @@ impl<'a> Typesys<'a> { start: 0, end: 0, message: format!("cannot reduction param {}", param_type), - }); + is_warning: false, + }); } // 为什么要在这里进行 ptr of, 只有在 infer 之后才能确定 alias 的具体类型,从而进一步判断是否需要 ptrof @@ -5053,7 +5186,8 @@ impl<'a> Typesys<'a> { message: format!("variable declaration cannot use type {}", var_decl.type_), start: var_decl.symbol_start, end: var_decl.symbol_end, - }); + is_warning: false, + }); } Ok(()) @@ -5131,7 +5265,8 @@ impl<'a> Typesys<'a> { start: 0, end: 0, message: "cannot infer function without interface".to_string(), - }); + is_warning: false, + }); } let mut type_fn = TypeFn { @@ -5415,7 +5550,7 @@ impl<'a> Typesys<'a> { return; } - errors_push(self.module, AnalyzerError { start, end, message }); + errors_push(self.module, AnalyzerError { start, end, message, is_warning: false }); } pub fn infer(&mut self) -> Vec { diff --git a/nls/src/analyzer/workspace_index.rs b/nls/src/analyzer/workspace_index.rs new file mode 100644 index 00000000..d7cfbb38 --- /dev/null +++ b/nls/src/analyzer/workspace_index.rs @@ -0,0 +1,476 @@ +use log::debug; +use std::collections::HashMap; +use std::path::Path; + +/// Represents a top-level symbol found in a workspace file +#[derive(Debug, Clone)] +pub struct IndexedSymbol { + pub name: String, // The symbol name (e.g., "MyController") + pub kind: IndexedSymbolKind, + pub file_path: String, // Absolute path to the .n file +} + +#[derive(Debug, Clone, PartialEq)] +pub enum IndexedSymbolKind { + Type, // type definitions (struct, interface, alias, enum) + Function, // fn definitions + Variable, // var definitions + Constant, // const definitions +} + +/// Lightweight workspace index that scans all .n files for top-level declarations +/// without performing full semantic analysis. +#[derive(Debug, Clone)] +pub struct WorkspaceIndex { + /// Map from symbol name -> list of indexed symbols (multiple files can define same name) + pub symbols: HashMap>, + /// Set of all indexed file paths (to track what's been indexed) + pub indexed_files: HashMap, // path -> last modified timestamp (0 if unknown) +} + +impl WorkspaceIndex { + pub fn new() -> Self { + Self { + symbols: HashMap::new(), + indexed_files: HashMap::new(), + } + } + + /// Scan a workspace root directory and index all .n files + pub fn scan_workspace(&mut self, root: &str, nature_root: &str) { + debug!("WorkspaceIndex: scanning workspace root '{}'", root); + + // 1. Scan project directory + self.scan_directory(root, root); + + // 2. Scan standard library + let std_dir = Path::new(nature_root).join("std"); + if std_dir.exists() { + self.scan_directory(std_dir.to_str().unwrap_or(""), root); + } + + debug!( + "WorkspaceIndex: indexed {} files, {} unique symbol names", + self.indexed_files.len(), + self.symbols.len() + ); + } + + /// Recursively scan a directory for .n files + fn scan_directory(&mut self, dir: &str, project_root: &str) { + let dir_path = Path::new(dir); + if !dir_path.exists() || !dir_path.is_dir() { + return; + } + + let entries = match std::fs::read_dir(dir_path) { + Ok(entries) => entries, + Err(_) => return, + }; + + for entry in entries { + let entry = match entry { + Ok(e) => e, + Err(_) => continue, + }; + + let path = entry.path(); + + // Skip hidden directories and common non-source directories + if let Some(name) = path.file_name().and_then(|n| n.to_str()) { + if name.starts_with('.') || name == "build" || name == "build-runtime" + || name == "node_modules" || name == "target" || name == "release" + { + continue; + } + } + + if path.is_dir() { + self.scan_directory(path.to_str().unwrap_or(""), project_root); + } else if path.is_file() { + if let Some(ext) = path.extension().and_then(|e| e.to_str()) { + if ext == "n" { + self.index_file(path.to_str().unwrap_or(""), project_root); + } + } + } + } + } + + /// Index a single .n file by doing a lightweight line-by-line scan for top-level declarations + pub fn index_file(&mut self, file_path: &str, _project_root: &str) { + let content = match std::fs::read_to_string(file_path) { + Ok(c) => c, + Err(_) => return, + }; + + // Get file modification time for incremental updates + let mtime = std::fs::metadata(file_path) + .and_then(|m| m.modified()) + .and_then(|t| t.duration_since(std::time::UNIX_EPOCH).map_err(|_| std::io::Error::new(std::io::ErrorKind::Other, ""))) + .map(|d| d.as_secs()) + .unwrap_or(0); + + // Check if file hasn't changed + if let Some(&cached_mtime) = self.indexed_files.get(file_path) { + if cached_mtime == mtime && mtime > 0 { + return; // File hasn't changed, skip re-indexing + } + } + + // Remove old symbols from this file + self.remove_file_symbols(file_path); + + // Parse top-level symbols from the file content + let symbols = Self::extract_top_level_symbols(&content, file_path); + + // Register symbols + for symbol in symbols { + self.symbols + .entry(symbol.name.clone()) + .or_insert_with(Vec::new) + .push(symbol); + } + + self.indexed_files.insert(file_path.to_string(), mtime); + } + + /// Remove all symbols associated with a file path + pub fn remove_file_symbols(&mut self, file_path: &str) { + // Remove symbols from each entry and clean up empty entries + self.symbols.retain(|_, symbols| { + symbols.retain(|s| s.file_path != file_path); + !symbols.is_empty() + }); + self.indexed_files.remove(file_path); + } + + /// Lightweight extraction of top-level symbol names from source code. + /// This does NOT do full parsing — it simply scans for declaration patterns + /// at the top indentation level. + fn extract_top_level_symbols(content: &str, file_path: &str) -> Vec { + let mut symbols = Vec::new(); + let mut in_block_comment = false; + + let lines: Vec<&str> = content.lines().collect(); + + // We need to track brace depth to know if we're at top level. + // Process character by character but extract symbols line by line. + let mut line_start_brace_depths: Vec = Vec::with_capacity(lines.len()); + + // First pass: compute brace depth at the start of each line + let mut depth: i32 = 0; + for line in &lines { + line_start_brace_depths.push(depth); + + let mut chars = line.chars().peekable(); + let mut prev = '\0'; + let mut in_str = false; + let mut in_lc = false; + + while let Some(ch) = chars.next() { + if in_lc { + break; // rest of line is comment + } + + match ch { + '/' if !in_str => { + if let Some(&next) = chars.peek() { + if next == '/' { + in_lc = true; + chars.next(); + continue; + } else if next == '*' { + in_block_comment = true; + chars.next(); + continue; + } + } + } + '*' if in_block_comment => { + if let Some(&next) = chars.peek() { + if next == '/' { + in_block_comment = false; + chars.next(); + continue; + } + } + } + _ if in_block_comment => continue, + '"' if prev != '\\' => { + in_str = !in_str; + } + '\'' if prev != '\\' && !in_str => { + // Skip character literals / string literals with single quotes + // In Nature, single-quoted strings are file imports, skip content + } + '{' if !in_str => depth += 1, + '}' if !in_str => depth -= 1, + _ => {} + } + prev = ch; + } + } + + // Second pass: extract top-level declarations + for (i, line) in lines.iter().enumerate() { + let depth = line_start_brace_depths[i]; + if depth != 0 { + continue; // Not at top level + } + + let trimmed = line.trim(); + + // Skip empty lines and comments + if trimmed.is_empty() || trimmed.starts_with("//") || trimmed.starts_with("/*") || trimmed.starts_with("*") { + continue; + } + + // Skip import statements + if trimmed.starts_with("import ") { + continue; + } + + // Skip preprocessor/macro directives + if trimmed.starts_with('#') { + continue; + } + + // type = ... + if trimmed.starts_with("type ") { + if let Some(name) = Self::extract_type_name(trimmed) { + symbols.push(IndexedSymbol { + name, + kind: IndexedSymbolKind::Type, + file_path: file_path.to_string(), + }); + } + continue; + } + + // fn (...) or fn .(...) + if trimmed.starts_with("fn ") { + if let Some(name) = Self::extract_fn_name(trimmed) { + // Only index top-level functions, not methods (type.method) + if !name.contains('.') { + symbols.push(IndexedSymbol { + name, + kind: IndexedSymbolKind::Function, + file_path: file_path.to_string(), + }); + } + } + continue; + } + + // const = ... + if trimmed.starts_with("const ") { + if let Some(name) = Self::extract_const_name(trimmed) { + symbols.push(IndexedSymbol { + name, + kind: IndexedSymbolKind::Constant, + file_path: file_path.to_string(), + }); + } + continue; + } + + // var = ... OR = ... + if trimmed.starts_with("var ") { + if let Some(name) = Self::extract_var_name(trimmed) { + symbols.push(IndexedSymbol { + name, + kind: IndexedSymbolKind::Variable, + file_path: file_path.to_string(), + }); + } + continue; + } + + // Explicit typed variable: = ... + // This is trickier - we need to detect patterns like: string foo = "bar" + // We skip this for now to avoid false positives; these are less common for + // cross-file usage since they're typically module-level state. + } + + symbols + } + + /// Extract type name from "type = ..." or "type : = ..." + fn extract_type_name(line: &str) -> Option { + let rest = line.strip_prefix("type ")?.trim_start(); + let name: String = rest.chars().take_while(|c| c.is_alphanumeric() || *c == '_').collect(); + if name.is_empty() { + return None; + } + Some(name) + } + + /// Extract function name from "fn (...)" + fn extract_fn_name(line: &str) -> Option { + let rest = line.strip_prefix("fn ")?.trim_start(); + let name: String = rest.chars().take_while(|c| c.is_alphanumeric() || *c == '_' || *c == '.').collect(); + if name.is_empty() { + return None; + } + Some(name) + } + + /// Extract const name from "const = ..." + fn extract_const_name(line: &str) -> Option { + let rest = line.strip_prefix("const ")?.trim_start(); + let name: String = rest.chars().take_while(|c| c.is_alphanumeric() || *c == '_').collect(); + if name.is_empty() { + return None; + } + Some(name) + } + + /// Extract var name from "var = ..." + fn extract_var_name(line: &str) -> Option { + let rest = line.strip_prefix("var ")?.trim_start(); + let name: String = rest.chars().take_while(|c| c.is_alphanumeric() || *c == '_').collect(); + if name.is_empty() { + return None; + } + Some(name) + } + + /// Search for symbols matching a prefix (case-sensitive) + pub fn find_symbols_by_prefix(&self, prefix: &str) -> Vec<&IndexedSymbol> { + if prefix.is_empty() { + return Vec::new(); + } + + let mut results = Vec::new(); + for (name, symbols) in &self.symbols { + if name.starts_with(prefix) { + for symbol in symbols { + results.push(symbol); + } + } + } + results + } + + /// Search for symbols with case-insensitive prefix matching + pub fn find_symbols_by_prefix_case_insensitive(&self, prefix: &str) -> Vec<&IndexedSymbol> { + if prefix.is_empty() { + return Vec::new(); + } + + let prefix_lower = prefix.to_lowercase(); + let mut results = Vec::new(); + for (name, symbols) in &self.symbols { + if name.to_lowercase().starts_with(&prefix_lower) { + for symbol in symbols { + results.push(symbol); + } + } + } + results + } + + /// Compute the import statement and module name for a given file path, relative to either: + /// - The current module's directory (file-based import: import 'foo.n') + /// - The project package (package-based import: import pkg.subdir.module) + /// - The standard library (import module_name) + pub fn compute_import_info( + &self, + symbol_file: &str, + current_module_dir: &str, + current_module_path: &str, + project_root: &str, + nature_root: &str, + package_name: Option<&str>, + ) -> Option { + let symbol_path = Path::new(symbol_file); + + // Don't import from the same file + if symbol_file == current_module_path { + return None; + } + + let symbol_dir = symbol_path.parent()?.to_str()?; + let file_stem = symbol_path.file_stem()?.to_str()?; + + // 1. Check if it's in the same directory -> file-based import + if symbol_dir == current_module_dir { + let file_name = symbol_path.file_name()?.to_str()?; + return Some(ImportInfo { + import_statement: format!("import '{}'\n", file_name), + module_as_name: file_stem.to_string(), + import_base: format!("import '{}'", file_name), + }); + } + + // 2. Check if it's in the standard library + let std_dir = Path::new(nature_root).join("std"); + if let Ok(std_canonical) = std_dir.canonicalize() { + if let Ok(symbol_canonical) = symbol_path.canonicalize() { + if symbol_canonical.starts_with(&std_canonical) { + // Get the relative path from std/ + if let Ok(rel) = symbol_canonical.strip_prefix(&std_canonical) { + let components: Vec<&str> = rel + .components() + .filter_map(|c| c.as_os_str().to_str()) + .collect(); + + if components.len() >= 1 { + // Standard library: first component is the package name + // e.g., std/fmt/... -> import fmt + let std_package = components[0]; + + if components.len() == 1 { + // It's the package root file (shouldn't usually happen) + let pkg = std_package.trim_end_matches(".n"); + return Some(ImportInfo { + import_statement: format!("import {}\n", pkg), + module_as_name: pkg.to_string(), + import_base: format!("import {}", pkg), + }); + } else { + // It's a submodule of a std package + let module_path: Vec = components.iter() + .map(|c| c.trim_end_matches(".n").to_string()) + .collect(); + let as_name = module_path.last()?.clone(); + let import_path = module_path.join("."); + return Some(ImportInfo { + import_statement: format!("import {}\n", import_path), + module_as_name: as_name, + import_base: format!("import {}", import_path), + }); + } + } + } + } + } + } + + // 3. Check if it's in the project -> package-based import + if let Some(pkg_name) = package_name { + if symbol_file.starts_with(project_root) { + let rel = &symbol_file[project_root.len()..].trim_start_matches('/'); + let rel_no_ext = rel.trim_end_matches(".n"); + let import_path = format!("{}.{}", pkg_name, rel_no_ext.replace('/', ".")); + let as_name = file_stem.to_string(); + return Some(ImportInfo { + import_statement: format!("import {}\n", import_path), + module_as_name: as_name, + import_base: format!("import {}", import_path), + }); + } + } + + None + } +} + +/// Information about how to import a module +#[derive(Debug, Clone)] +pub struct ImportInfo { + pub import_statement: String, // Full module import: import 'foo.n'\n + pub module_as_name: String, // Module name for qualified access: foo + pub import_base: String, // Base path for selective import (without \n): import 'foo.n' or import pkg.mod +} diff --git a/nls/src/document.rs b/nls/src/document.rs new file mode 100644 index 00000000..8ee8f95b --- /dev/null +++ b/nls/src/document.rs @@ -0,0 +1,377 @@ +//! Thread-safe document store backed by [`DashMap`] and [`ropey::Rope`]. +//! +//! Each open file is tracked as a [`Document`] containing the full source text +//! as a rope plus metadata (version, language id). The [`DocumentStore`] +//! provides a concurrent-safe façade used by the LSP dispatch layer. + +use dashmap::DashMap; +use ropey::Rope; +use tower_lsp::lsp_types::{Position, TextDocumentContentChangeEvent, Url}; + +// ─── Document ─────────────────────────────────────────────────────────────────── + +/// A single open document. +#[derive(Debug, Clone)] +pub struct Document { + /// Full source text. + pub rope: Rope, + /// Editor-assigned version (monotonically increasing per file). + pub version: i32, + /// Language identifier (e.g. `"nature"` / `"n"`). + pub language_id: String, +} + +impl Document { + pub fn new(text: &str, version: i32, language_id: String) -> Self { + Self { + rope: Rope::from_str(text), + version, + language_id, + } + } + + /// Return the full source text as a `String`. + pub fn text(&self) -> String { + self.rope.to_string() + } +} + +// ─── DocumentStore ────────────────────────────────────────────────────────────── + +/// Concurrent map of open documents keyed by file path (string). +/// +/// We key by **file path** (`String`) rather than `Url` so that look-ups from +/// other subsystems (project, analyzer) that work with plain paths don't need +/// to round-trip through URL parsing. +#[derive(Debug, Default)] +pub struct DocumentStore { + docs: DashMap, +} + +impl DocumentStore { + pub fn new() -> Self { + Self { + docs: DashMap::new(), + } + } + + // ── Lifecycle ─────────────────────────────────────────────────────── + + /// Track a newly opened document. + pub fn open(&self, uri: &Url, text: &str, version: i32, language_id: String) { + let path = uri.path().to_string(); + self.docs + .insert(path, Document::new(text, version, language_id)); + } + + /// Stop tracking a closed document. + pub fn close(&self, uri: &Url) { + let path = uri.path().to_string(); + self.docs.remove(&path); + } + + // ── Queries ───────────────────────────────────────────────────────── + + /// Retrieve a clone of the document for the given URI, if open. + pub fn get(&self, uri: &Url) -> Option { + let path = uri.path().to_string(); + self.docs.get(&path).map(|d| d.clone()) + } + + /// Retrieve a clone of the document by file path. + pub fn get_by_path(&self, path: &str) -> Option { + self.docs.get(path).map(|d| d.clone()) + } + + /// Get the rope for a file path (convenience accessor). + pub fn get_rope(&self, path: &str) -> Option { + self.docs.get(path).map(|d| d.rope.clone()) + } + + /// Get the full source text for a file path. + pub fn get_text(&self, path: &str) -> Option { + self.docs.get(path).map(|d| d.rope.to_string()) + } + + /// Check whether a document is currently open. + pub fn is_open(&self, path: &str) -> bool { + self.docs.contains_key(path) + } + + /// Number of open documents. + pub fn len(&self) -> usize { + self.docs.len() + } + + /// Whether the store is empty. + pub fn is_empty(&self) -> bool { + self.docs.is_empty() + } + + // ── Incremental sync ──────────────────────────────────────────────── + + /// Apply a batch of LSP content-change events to a document. + /// + /// Supports both **full** replacements (no range) and **incremental** edits + /// (with a range). Returns the full text after all changes are applied, or + /// `None` if the document isn't tracked. + pub fn apply_changes( + &self, + uri: &Url, + version: i32, + changes: &[TextDocumentContentChangeEvent], + ) -> Option { + let path = uri.path().to_string(); + let mut entry = self.docs.get_mut(&path)?; + + for change in changes { + if let Some(range) = change.range { + // Incremental edit. + let start = position_to_char_idx(&entry.rope, range.start); + let end = position_to_char_idx(&entry.rope, range.end); + + let start = start.min(entry.rope.len_chars()); + let end = end.min(entry.rope.len_chars()); + + if start < end { + entry.rope.remove(start..end); + } + if !change.text.is_empty() { + entry.rope.insert(start, &change.text); + } + } else { + // Full replacement. + entry.rope = Rope::from_str(&change.text); + } + } + + entry.version = version; + Some(entry.rope.to_string()) + } +} + +// ─── Helpers ──────────────────────────────────────────────────────────────────── + +/// Convert an LSP `Position` (line, character — both 0-based) to a rope char +/// index, clamping to valid bounds. +fn position_to_char_idx(rope: &Rope, pos: Position) -> usize { + let line = (pos.line as usize).min(rope.len_lines().saturating_sub(1)); + let line_start = rope.line_to_char(line); + let line_len = rope.line(line).len_chars(); + let col = (pos.character as usize).min(line_len); + line_start + col +} + +// ─── Tests ────────────────────────────────────────────────────────────────────── + +#[cfg(test)] +mod tests { + use super::*; + use tower_lsp::lsp_types::Range; + + fn test_uri(name: &str) -> Url { + Url::parse(&format!("file:///tmp/{name}.n")).unwrap() + } + + // ── Lifecycle ─────────────────────────────────────────────────────── + + #[test] + fn open_and_get() { + let store = DocumentStore::new(); + let uri = test_uri("a"); + store.open(&uri, "hello world", 1, "nature".into()); + + let doc = store.get(&uri).unwrap(); + assert_eq!(doc.text(), "hello world"); + assert_eq!(doc.version, 1); + assert_eq!(doc.language_id, "nature"); + } + + #[test] + fn close_removes_document() { + let store = DocumentStore::new(); + let uri = test_uri("b"); + store.open(&uri, "content", 1, "nature".into()); + assert!(store.is_open(uri.path())); + + store.close(&uri); + assert!(!store.is_open(uri.path())); + assert!(store.get(&uri).is_none()); + } + + #[test] + fn get_by_path() { + let store = DocumentStore::new(); + let uri = test_uri("c"); + store.open(&uri, "fn main() {}", 1, "nature".into()); + + let doc = store.get_by_path(uri.path()).unwrap(); + assert_eq!(doc.text(), "fn main() {}"); + } + + #[test] + fn get_missing_returns_none() { + let store = DocumentStore::new(); + assert!(store.get_by_path("/nonexistent").is_none()); + } + + #[test] + fn len_and_is_empty() { + let store = DocumentStore::new(); + assert!(store.is_empty()); + assert_eq!(store.len(), 0); + + store.open(&test_uri("x"), "", 1, "n".into()); + assert!(!store.is_empty()); + assert_eq!(store.len(), 1); + } + + // ── Full replacement ──────────────────────────────────────────────── + + #[test] + fn apply_full_replacement() { + let store = DocumentStore::new(); + let uri = test_uri("full"); + store.open(&uri, "old content", 1, "nature".into()); + + let changes = vec![TextDocumentContentChangeEvent { + range: None, + range_length: None, + text: "new content".into(), + }]; + let text = store.apply_changes(&uri, 2, &changes).unwrap(); + assert_eq!(text, "new content"); + + let doc = store.get(&uri).unwrap(); + assert_eq!(doc.version, 2); + } + + // ── Incremental edits ─────────────────────────────────────────────── + + #[test] + fn apply_incremental_insert() { + let store = DocumentStore::new(); + let uri = test_uri("inc_ins"); + store.open(&uri, "helo world", 1, "nature".into()); + + // Insert 'l' at position (0, 3) → "hello world" + let changes = vec![TextDocumentContentChangeEvent { + range: Some(Range::new(Position::new(0, 3), Position::new(0, 3))), + range_length: None, + text: "l".into(), + }]; + let text = store.apply_changes(&uri, 2, &changes).unwrap(); + assert_eq!(text, "hello world"); + } + + #[test] + fn apply_incremental_delete() { + let store = DocumentStore::new(); + let uri = test_uri("inc_del"); + store.open(&uri, "helllo world", 1, "nature".into()); + + // Delete one 'l' at (0,3)..(0,4) → "hello world" + let changes = vec![TextDocumentContentChangeEvent { + range: Some(Range::new(Position::new(0, 3), Position::new(0, 4))), + range_length: None, + text: "".into(), + }]; + let text = store.apply_changes(&uri, 2, &changes).unwrap(); + assert_eq!(text, "hello world"); + } + + #[test] + fn apply_incremental_replace() { + let store = DocumentStore::new(); + let uri = test_uri("inc_rep"); + store.open(&uri, "fn foo() {}", 1, "nature".into()); + + // Replace "foo" (3..6) with "bar" + let changes = vec![TextDocumentContentChangeEvent { + range: Some(Range::new(Position::new(0, 3), Position::new(0, 6))), + range_length: None, + text: "bar".into(), + }]; + let text = store.apply_changes(&uri, 2, &changes).unwrap(); + assert_eq!(text, "fn bar() {}"); + } + + #[test] + fn apply_multiline_edit() { + let store = DocumentStore::new(); + let uri = test_uri("multiline"); + store.open(&uri, "line1\nline2\nline3", 1, "nature".into()); + + // Replace "line2" (line 1, chars 0..5) with "replaced" + let changes = vec![TextDocumentContentChangeEvent { + range: Some(Range::new(Position::new(1, 0), Position::new(1, 5))), + range_length: None, + text: "replaced".into(), + }]; + let text = store.apply_changes(&uri, 2, &changes).unwrap(); + assert_eq!(text, "line1\nreplaced\nline3"); + } + + #[test] + fn apply_multiple_changes_in_batch() { + let store = DocumentStore::new(); + let uri = test_uri("batch"); + store.open(&uri, "aaa bbb ccc", 1, "nature".into()); + + // Two changes in one batch: replace "aaa" → "xxx", then after that + // the text is "xxx bbb ccc"; replace "ccc" is now at (0,8..11) + let changes = vec![ + TextDocumentContentChangeEvent { + range: Some(Range::new(Position::new(0, 0), Position::new(0, 3))), + range_length: None, + text: "xxx".into(), + }, + TextDocumentContentChangeEvent { + range: Some(Range::new(Position::new(0, 8), Position::new(0, 11))), + range_length: None, + text: "zzz".into(), + }, + ]; + let text = store.apply_changes(&uri, 2, &changes).unwrap(); + assert_eq!(text, "xxx bbb zzz"); + } + + #[test] + fn apply_changes_to_missing_doc_returns_none() { + let store = DocumentStore::new(); + let uri = test_uri("missing"); + let changes = vec![TextDocumentContentChangeEvent { + range: None, + range_length: None, + text: "text".into(), + }]; + assert!(store.apply_changes(&uri, 1, &changes).is_none()); + } + + // ── position_to_char_idx ──────────────────────────────────────────── + + #[test] + fn position_to_char_idx_basic() { + let rope = Rope::from_str("abc\ndef\nghi"); + assert_eq!(position_to_char_idx(&rope, Position::new(0, 0)), 0); + assert_eq!(position_to_char_idx(&rope, Position::new(0, 2)), 2); + assert_eq!(position_to_char_idx(&rope, Position::new(1, 0)), 4); // 'd' + assert_eq!(position_to_char_idx(&rope, Position::new(2, 1)), 9); // 'h' + } + + #[test] + fn position_to_char_idx_clamps_column() { + let rope = Rope::from_str("ab\ncd"); + // Column 99 on a 3-char line (including \n) should clamp + let idx = position_to_char_idx(&rope, Position::new(0, 99)); + assert!(idx <= rope.len_chars()); + } + + #[test] + fn position_to_char_idx_clamps_line() { + let rope = Rope::from_str("only one line"); + // Line 99 should clamp to last line + let idx = position_to_char_idx(&rope, Position::new(99, 0)); + assert!(idx <= rope.len_chars()); + } +} diff --git a/nls/src/lib.rs b/nls/src/lib.rs index beb3380b..a6e58f5c 100644 --- a/nls/src/lib.rs +++ b/nls/src/lib.rs @@ -1,4 +1,16 @@ +//! Nature Language Server (NLS) — library root. +//! +//! Module boundaries: +//! - [`analyzer`] — lexer, parser, semantic analysis, type system (existing). +//! - [`document`] — thread-safe document store backed by Rope. +//! - [`package`] — `package.toml` parsing. +//! - [`project`] — workspace/project state and build pipeline. +//! - [`server`] — LSP backend, capabilities, and request dispatch. +//! - [`utils`] — position/offset conversion, identifier helpers. + pub mod analyzer; +pub mod document; pub mod package; pub mod project; +pub mod server; pub mod utils; diff --git a/nls/src/main.rs b/nls/src/main.rs index 10dabeb8..8abd7725 100644 --- a/nls/src/main.rs +++ b/nls/src/main.rs @@ -1,739 +1,8 @@ use dashmap::DashMap; -use log::debug; -use nls::analyzer::completion::{CompletionItemKind, CompletionProvider}; -use nls::analyzer::lexer::{TokenType, LEGEND_TYPE}; -use nls::analyzer::module_unique_ident; -use nls::package::parse_package; -use nls::project::Project; -use nls::utils::offset_to_position; -use serde::{Deserialize, Serialize}; -use serde_json::Value; -use tower_lsp::jsonrpc::Result; -use tower_lsp::lsp_types::notification::Notification; -use tower_lsp::lsp_types::*; -use tower_lsp::{Client, LanguageServer, LspService, Server}; +use tower_lsp::{LspService, Server}; -#[derive(Debug)] -struct Backend { - client: Client, - // document_map: DashMap, - projects: DashMap, // key 是工作区 URI,value 是对应的项目 -} - -// backend 除了实现自身的方法,还实现了 LanguageServer trait 的方法 -#[tower_lsp::async_trait] -impl LanguageServer for Backend { - async fn initialize(&self, params: InitializeParams) -> Result { - // 获取工作区根目录 - if let Some(workspace_folders) = params.workspace_folders { - for folder in workspace_folders { - // folder.uri 是工作区根目录的 URI - let project_root = folder - .uri - .to_file_path() - .expect("Failed to convert URI to file path") - .to_string_lossy() - .to_string(); - let project = Project::new(project_root.clone()).await; - project.backend_handle_queue(); - debug!("project new success root: {}", project_root); - - // 多工作区处理 - self.projects.insert(project_root, project); - } - } - - Ok(InitializeResult { - server_info: None, - offset_encoding: None, - capabilities: ServerCapabilities { - // 开启内联提示 - inlay_hint_provider: Some(OneOf::Left(true)), - // 文档同步配置 - text_document_sync: Some(TextDocumentSyncCapability::Options(TextDocumentSyncOptions { - open_close: Some(true), - change: Some(TextDocumentSyncKind::FULL), - save: Some(TextDocumentSyncSaveOptions::SaveOptions(SaveOptions { include_text: Some(true) })), - ..Default::default() - })), - // 代码补全配置 - completion_provider: Some(CompletionOptions { - resolve_provider: Some(false), - trigger_characters: Some(vec![".".to_string()]), - work_done_progress_options: Default::default(), - all_commit_characters: None, - completion_item: None, - }), - execute_command_provider: Some(ExecuteCommandOptions { - commands: vec!["dummy.do_something".to_string()], - work_done_progress_options: Default::default(), - }), - - // 工作区配置 - workspace: Some(WorkspaceServerCapabilities { - workspace_folders: Some(WorkspaceFoldersServerCapabilities { - supported: Some(true), - change_notifications: Some(OneOf::Left(true)), - }), - file_operations: None, - }), - // 语义标记配置 - semantic_tokens_provider: Some(SemanticTokensServerCapabilities::SemanticTokensRegistrationOptions( - SemanticTokensRegistrationOptions { - text_document_registration_options: { - TextDocumentRegistrationOptions { - document_selector: Some(vec![DocumentFilter { - language: Some("n".to_string()), - scheme: Some("file".to_string()), - pattern: None, - }]), - } - }, - semantic_tokens_options: SemanticTokensOptions { - work_done_progress_options: WorkDoneProgressOptions::default(), - legend: SemanticTokensLegend { - // // LEGEND_TYPE 通常定义在 semantic_token.rs 中,包含所有支持的标记类型 - token_types: LEGEND_TYPE.into(), // 支持的标记类型, 如函数、变量、字符串等 - token_modifiers: vec![], // 支持的标记修饰符, 例如 readonly, static 等 - }, - range: Some(true), // 范围增量更新语义 - full: Some(SemanticTokensFullOptions::Bool(true)), - }, - static_registration_options: StaticRegistrationOptions::default(), - }, - )), - // definition: Some(GotoCapability::default()), - definition_provider: Some(OneOf::Left(true)), - references_provider: Some(OneOf::Left(true)), - rename_provider: Some(OneOf::Left(true)), - ..ServerCapabilities::default() - }, - }) - } - async fn initialized(&self, _: InitializedParams) { - debug!("initialized"); - } - - async fn shutdown(&self) -> Result<()> { - Ok(()) - } - - async fn did_open(&self, params: DidOpenTextDocumentParams) { - debug!("file opened {}", params.text_document.uri); - - // 从 uri 所在目录开始逐层向上读取,判断是否存在 package.toml, 如果存在 package.toml, 那 package.toml 所在目录就是一个新的 project - let file_path = params.text_document.uri.path(); - let file_dir = std::path::Path::new(file_path).parent(); - - if let Some(dir) = file_dir { - // Check if need to create new project - let (project_root, log_message) = if let Some(package_dir) = self.find_package_dir(dir) { - (package_dir.to_string_lossy().to_string(), "Found new project at") - } else { - // If no package_dir found, use current file's directory as project root - (dir.to_string_lossy().to_string(), "Creating new project using file directory as root:") - }; - - // Check if this project already exists and create if needed - if !self.projects.contains_key(&project_root) { - debug!("{} {}", log_message, project_root); - let project = Project::new(project_root.clone()).await; - project.backend_handle_queue(); - debug!("project new success root: {}", project_root); - self.projects.insert(project_root, project); - } - } - - self.on_change(TextDocumentItem { - uri: params.text_document.uri, - text: ¶ms.text_document.text, - version: Some(params.text_document.version), - }) - .await - } - - async fn did_change(&self, params: DidChangeTextDocumentParams) { - self.on_change(TextDocumentItem { - text: ¶ms.content_changes[0].text, - uri: params.text_document.uri, - version: Some(params.text_document.version), - }) - .await - } - - async fn did_save(&self, params: DidSaveTextDocumentParams) { - if let Some(text) = params.text { - let item = TextDocumentItem { - uri: params.text_document.uri, - text: &text, - version: None, - }; - self.on_change(item).await; - _ = self.client.semantic_tokens_refresh().await; - } - debug!("file saved!"); - } - async fn did_close(&self, _: DidCloseTextDocumentParams) { - debug!("file closed!"); - } - - // did open 中已经对 document 进行了处理,所以这里只需要从 semantic 中获取信息 - async fn goto_definition(&self, _params: GotoDefinitionParams) -> Result> { - let definition = || -> Option { - // uri 标识当前文档的唯一标识 - // let uri = params.text_document_position_params.text_document.uri; - - // self 表示当前 backend, 通过 uri 标识快速获取对应的 semantic - // let semantic = self.semantic_map.get(uri.as_str())?; - - // let rope = self.document_map.get(uri.as_str())?; - - // // 获取当前光标位置 - // let position = params.text_document_position_params.position; - - // // 将光标位置转换为偏移量 - // let offset = position_to_offset(position, &rope)?; - - // // 获取当前光标位置的符号 - // let interval = semantic.ident_range.find(offset, offset + 1).next()?; - // let interval_val = interval.val; - - // let range = match interval_val { - // // 回到定义点 - // IdentType::Binding(symbol_id) => { - // let span = &semantic.table.symbol_id_to_span[symbol_id]; - // Some(span.clone()) - // } - - // // 通过引用获取符号定义的位置 - // IdentType::Reference(reference_id) => { - // let reference = semantic.table.reference_id_to_reference.get(reference_id)?; - // let symbol_id = reference.symbol_id?; - // let symbol_range = semantic.table.symbol_id_to_span.get(symbol_id)?; - // Some(symbol_range.clone()) - // } - // }; - - // // 将源代码转换为 lsp 输出需要的位置格式 - // range.and_then(|range| { - // let start_position = offset_to_position(range.start, &rope)?; - // let end_position = offset_to_position(range.end, &rope)?; - // Some(GotoDefinitionResponse::Scalar(Location::new(uri, Range::new(start_position, end_position)))) - // }) - None - }(); - Ok(definition) - } - - async fn references(&self, params: ReferenceParams) -> Result>> { - let reference_list = || -> Option> { - let _uri = params.text_document_position.text_document.uri; - // let semantic = self.semantic_map.get(uri.as_str())?; - // let rope = self.document_map.get(uri.as_str())?; - // let position = params.text_document_position.position; - // let offset = position_to_offset(position, &rope)?; - // let reference_span_list = get_references(&semantic, offset, offset + 1, false)?; - - // let ret = reference_span_list - // .into_iter() - // .filter_map(|range| { - // let start_position = offset_to_position(range.start, &rope)?; - // let end_position = offset_to_position(range.end, &rope)?; - - // let range = Range::new(start_position, end_position); - - // Some(Location::new(uri.clone(), range)) - // }) - // .collect::>(); - // Some(ret) - None - }(); - Ok(reference_list) - } - - async fn semantic_tokens_full(&self, params: SemanticTokensParams) -> Result> { - let file_path = params.text_document.uri.path(); - debug!("semantic_token_full"); - - // semantic_tokens 是一个闭包, 返回 vscode 要求的 SemanticToken 结构 - let semantic_tokens = || -> Option> { - let Some(project) = self.get_file_project(&file_path) else { unreachable!() }; - - // 直接从 module_handled 中获取 - let module_index = { - let module_handled = project.module_handled.lock().unwrap(); - module_handled.get(file_path)?.clone() - }; - - let mut module_db = project.module_db.lock().unwrap(); - let m = &mut module_db[module_index]; - - // 获取 semantic_token_map 中的 token - let im_complete_tokens = m.sem_token_db.clone(); - let rope = m.rope.clone(); - - // im_complete_tokens.sort_by(|a, b| a.start.cmp(&b.start)); - - let mut pre_line = 0; - let mut pre_line_start = 0; - let mut pre_start = 0; - let semantic_tokens: Vec = im_complete_tokens - .iter() - .filter_map(|token| { - let line = rope.try_char_to_line(token.start).ok()? as u32; - let _end_line = rope.try_char_to_line(token.end).ok()? as u32; - let line_first = rope.try_line_to_char(line as usize).ok()? as u32; - let line_start = token.start as u32 - line_first; - - // dbg!( - // "--------------", - // token.clone(), - // line, - // end_line, - // pre_line, - // pre_line_start, - // line_start, - // line_start < pre_line_start, - // "-------------" - // ); - - if token.start < pre_start && token.token_type == TokenType::StringLiteral { - // 多行字符串跳过 - return None; - } - - let delta_line = line - pre_line; - let delta_start = if delta_line == 0 { line_start - pre_line_start } else { line_start }; - - let ret = Some(SemanticToken { - delta_line, - delta_start, - length: token.length as u32, - token_type: token.semantic_token_type.clone() as u32, - token_modifiers_bitset: 0, - }); - pre_line = line; - pre_line_start = line_start; - pre_start = token.start; - ret - }) - .collect::>(); - Some(semantic_tokens) - }(); - if let Some(semantic_token) = semantic_tokens { - return Ok(Some(SemanticTokensResult::Tokens(SemanticTokens { - result_id: None, - data: semantic_token, - }))); - } - Ok(None) - } - - async fn semantic_tokens_range(&self, params: SemanticTokensRangeParams) -> Result> { - let file_path = params.text_document.uri.path(); - let semantic_tokens = || -> Option> { - let Some(project) = self.get_file_project(&file_path) else { unreachable!() }; - - // 直接从 module_handled 中获取 - let module_index = { - let module_handled = project.module_handled.lock().unwrap(); - module_handled.get(file_path)?.clone() - }; - - let mut module_db = project.module_db.lock().unwrap(); - let m = &mut module_db[module_index]; - - // 获取 semantic_token_map 中的 token - let im_complete_tokens = m.sem_token_db.clone(); - let rope = m.rope.clone(); - - let mut pre_line = 0; - let mut pre_start = 0; - let semantic_tokens = im_complete_tokens - .iter() - .filter_map(|token| { - let line = rope.try_byte_to_line(token.start).ok()? as u32; - let first = rope.try_line_to_char(line as usize).ok()? as u32; - let start = rope.try_byte_to_char(token.start).ok()? as u32 - first; - let ret = Some(SemanticToken { - delta_line: line - pre_line, - delta_start: if start >= pre_start { start - pre_start } else { start }, - length: token.length as u32, - token_type: token.token_type.clone() as u32, - token_modifiers_bitset: 0, - }); - pre_line = line; - pre_start = start; - ret - }) - .collect::>(); - Some(semantic_tokens) - }(); - Ok(semantic_tokens.map(|data| SemanticTokensRangeResult::Tokens(SemanticTokens { result_id: None, data }))) - } - - async fn inlay_hint(&self, _params: tower_lsp::lsp_types::InlayHintParams) -> Result>> { - // dbg!("inlay hint"); - // let uri = ¶ms.text_document.uri; - // let mut hashmap = HashMap::new(); - // if let Some(ast) = self.ast_map.get(uri.as_str()) { - // ast.iter().for_each(|(func, _)| { - // type_inference(&func.body, &mut hashmap); - // }); - // } - - // let document = match self.document_map.get(uri.as_str()) { - // Some(rope) => rope, - // None => return Ok(None), - // }; - // let inlay_hint_list = hashmap - // .into_iter() - // .map(|(k, v)| { - // ( - // k.start, - // k.end, - // match v { - // nls::nrs_lang::Value::Null => "null".to_string(), - // nls::nrs_lang::Value::Bool(_) => "bool".to_string(), - // nls::nrs_lang::Value::Num(_) => "number".to_string(), - // nls::nrs_lang::Value::Str(_) => "string".to_string(), - // }, - // ) - // }) - // .filter_map(|item| { - // // let start_position = offset_to_position(item.0, document)?; - // let end_position = offset_to_position(item.1, &document)?; - // let inlay_hint = InlayHint { - // text_edits: None, - // tooltip: None, - // kind: Some(InlayHintKind::TYPE), - // padding_left: None, - // padding_right: None, - // data: None, - // position: end_position, - // label: InlayHintLabel::LabelParts(vec![InlayHintLabelPart { - // value: item.2, - // tooltip: None, - // location: Some(Location { - // uri: params.text_document.uri.clone(), - // range: Range { - // start: Position::new(0, 4), - // end: Position::new(0, 10), - // }, - // }), - // command: None, - // }]), - // }; - // Some(inlay_hint) - // }) - // .collect::>(); - - // Ok(Some(inlay_hint_list)) - Ok(None) - } - - async fn completion(&self, params: CompletionParams) -> Result> { - dbg!("completion requested"); - - let uri = params.text_document_position.text_document.uri; - let position: Position = params.text_document_position.position; - - let completions = || -> Option> { - let file_path = uri.path(); - let project = self.get_file_project(&file_path)?; - - // 获取模块索引 - let module_index = { - let module_handled = project.module_handled.lock().unwrap(); - module_handled.get(file_path)?.clone() - }; - - let mut module_db = project.module_db.lock().unwrap(); - let module = &mut module_db[module_index]; - - // 将LSP位置转换为字节偏移 - let rope = &module.rope; - let line_char = rope.try_line_to_char(position.line as usize).ok()?; - let byte_offset = line_char + position.character as usize; - - // 获取当前位置的前缀 - let text = rope.to_string(); - debug!("Getting completions at byte_offset {}, module_ident '{}'", byte_offset, module.ident.clone()); - - // Get symbol table and package config - let mut symbol_table = project.symbol_table.lock().unwrap(); - let package_config = project.package_config.lock().unwrap().clone(); - // Create completion provider and get completion items - let completion_items = CompletionProvider::new(&mut symbol_table, module, project.nature_root.clone(), project.root.clone(), package_config) - .get_completions(byte_offset, &text); - - // 转换为LSP格式 - let lsp_items: Vec = completion_items - .into_iter() - .map(|item| { - let lsp_kind = match item.kind { - CompletionItemKind::Variable => tower_lsp::lsp_types::CompletionItemKind::VARIABLE, - CompletionItemKind::Parameter => tower_lsp::lsp_types::CompletionItemKind::VARIABLE, - CompletionItemKind::Function => tower_lsp::lsp_types::CompletionItemKind::FUNCTION, - CompletionItemKind::Constant => tower_lsp::lsp_types::CompletionItemKind::CONSTANT, - CompletionItemKind::Module => tower_lsp::lsp_types::CompletionItemKind::MODULE, - CompletionItemKind::Struct => tower_lsp::lsp_types::CompletionItemKind::STRUCT, - }; - - // Check if insert_text contains snippet syntax - let has_snippet = item.insert_text.contains("$0"); - - // Convert additional_text_edits to LSP format - let additional_edits = if !item.additional_text_edits.is_empty() { - Some( - item.additional_text_edits - .into_iter() - .map(|edit| tower_lsp::lsp_types::TextEdit { - range: tower_lsp::lsp_types::Range { - start: tower_lsp::lsp_types::Position { - line: edit.line as u32, - character: edit.character as u32, - }, - end: tower_lsp::lsp_types::Position { - line: edit.line as u32, - character: edit.character as u32, - }, - }, - new_text: edit.new_text, - }) - .collect(), - ) - } else { - None - }; - - tower_lsp::lsp_types::CompletionItem { - label: item.label, - kind: Some(lsp_kind), - detail: item.detail, - documentation: item.documentation.map(|doc| tower_lsp::lsp_types::Documentation::String(doc)), - insert_text: Some(item.insert_text), - insert_text_format: if has_snippet { - Some(tower_lsp::lsp_types::InsertTextFormat::SNIPPET) - } else { - Some(tower_lsp::lsp_types::InsertTextFormat::PLAIN_TEXT) - }, - sort_text: item.sort_text, - additional_text_edits: additional_edits, - ..Default::default() - } - }) - .collect(); - - debug!("Returning {} completion items", lsp_items.len()); - Some(lsp_items) - }(); - - Ok(completions.map(CompletionResponse::Array)) - } - - async fn rename(&self, _params: RenameParams) -> Result> { - let workspace_edit = || -> Option { - // let uri = params.text_document_position.text_document.uri; - // let semantic = self.semantic_map.get(uri.as_str())?; - // let rope = self.document_map.get(uri.as_str())?; - // let position = params.text_document_position.position; - // let offset = position_to_offset(position, &rope)?; - // let reference_list = get_references(&semantic, offset, offset + 1, true)?; - - // let new_name = params.new_name; - // (!reference_list.is_empty()).then_some(()).map(|_| { - // let edit_list = reference_list - // .into_iter() - // .filter_map(|range| { - // let start_position = offset_to_position(range.start, &rope)?; - // let end_position = offset_to_position(range.end, &rope)?; - // Some(TextEdit::new(Range::new(start_position, end_position), new_name.clone())) - // }) - // .collect::>(); - // let mut map = HashMap::new(); - // map.insert(uri, edit_list); - // WorkspaceEdit::new(map) - // }) - None - }(); - Ok(workspace_edit) - } - - async fn did_change_configuration(&self, _: DidChangeConfigurationParams) { - debug!("configuration changed!"); - } - - async fn did_change_workspace_folders(&self, _: DidChangeWorkspaceFoldersParams) { - debug!("workspace folders changed!"); - } - - async fn did_change_watched_files(&self, params: DidChangeWatchedFilesParams) { - debug!("package.toml files have changed!"); - - for param in params.changes { - let file_path = param.uri.path(); - let Some(project) = self.get_file_project(&file_path) else { unreachable!() }; - - if !file_path.ends_with("package.toml") { - panic!("unexpected package file '{}'", file_path); - } - - match parse_package(&file_path) { - Ok(package_conf) => { - { - let mut package_option = project.package_config.lock().unwrap(); - *package_option = Some(package_conf); - } - - self.client.publish_diagnostics(param.uri.clone(), vec![], None).await; - } - Err(e) => { - // read file content - if let Ok(content) = std::fs::read_to_string(&file_path) { - // 创建 rope 用于将偏移量转换为位置 - let rope = ropey::Rope::from_str(&content); - let start_position = offset_to_position(e.start, &rope).unwrap_or(Position::new(0, 0)); - - let end_position = offset_to_position(e.end, &rope).unwrap_or(Position::new(0, 0)); - - let diagnostic = Diagnostic::new_simple(Range::new(start_position, end_position), format!("parser package.toml failed: {}", e.message)); - self.client.publish_diagnostics(param.uri.clone(), vec![diagnostic], None).await; - } - } - } - } - - debug!("package.toml updated"); - } - - async fn execute_command(&self, _: ExecuteCommandParams) -> Result> { - debug!("command executed!"); - - match self.client.apply_edit(WorkspaceEdit::default()).await { - Ok(res) if res.applied => self.client.log_message(MessageType::INFO, "applied").await, - Ok(_) => self.client.log_message(MessageType::INFO, "rejected").await, - Err(err) => self.client.log_message(MessageType::ERROR, err).await, - } - - Ok(None) - } -} -#[derive(Debug, Deserialize, Serialize)] -struct InlayHintParams { - path: String, -} - -#[allow(unused)] -enum CustomNotification {} -impl Notification for CustomNotification { - type Params = InlayHintParams; - const METHOD: &'static str = "custom/notification"; -} -struct TextDocumentItem<'a> { - uri: Url, - text: &'a str, // 'a 是声明周期引用,表示 str 的生命周期与 TextDocumentItem 的生命周期一致, 而不是 } 后就结束 - version: Option, -} - -impl Backend { - // 添加一个辅助方法来根据文件 URI 找到对应的项目 - fn get_file_project(&self, file_path: &str) -> Option { - // 遍历所有项目,找到最匹配(路径最长)的项目 - let mut best_match: Option<(usize, Project)> = None; - - for entry in self.projects.iter() { - let workspace_uri = entry.key(); - - // 检查文件是否属于这个项目 - if file_path.starts_with(workspace_uri) { - // 如果文件属于这个项目,检查这是否是最佳匹配(路径最长) - let uri_len = workspace_uri.len(); - - if let Some((best_len, _)) = &best_match { - if uri_len > *best_len { - // 找到更长的匹配,更新最佳匹配 - best_match = Some((uri_len, entry.value().clone())); - } - } else { - // 第一个匹配 - best_match = Some((uri_len, entry.value().clone())); - } - } - } - - // 返回最佳匹配的项目 - best_match.map(|(_, project)| project) - } - - fn find_package_dir(&self, start_dir: &std::path::Path) -> Option { - let mut current_dir = Some(start_dir.to_path_buf()); - - while let Some(dir) = current_dir { - let package_path = dir.join("package.toml"); - if package_path.exists() { - return Some(dir); - } - current_dir = dir.parent().map(|p| p.to_path_buf()); - } - - None - } - - async fn on_change<'a>(&self, params: TextDocumentItem<'a>) { - debug!( - r#"Text content: - {} - "#, - params.text - ); - - let file_path = params.uri.path(); - let Some(mut project) = self.get_file_project(&file_path) else { - unreachable!() - }; - - // package.toml specail handle - if file_path.ends_with("package.toml") { - panic!("unexpected package file '{}'", file_path); - } - - let module_ident = module_unique_ident(&project.root, &file_path); - debug!("will build, module ident: {}", module_ident); - - // 基于 project path 计算 moudle ident - let module_index = project.build(&file_path, &module_ident, Some(params.text.to_string())).await; - debug!("build success"); - - let diagnostics = { - let mut module_db = project.module_db.lock().unwrap(); - let m = &mut module_db[module_index]; - - dbg!(&m.ident, &m.analyzer_errors); - - let mut seen_positions = std::collections::HashSet::new(); - - m.analyzer_errors - .clone() - .into_iter() - .filter(|error| error.end > 0) // 过滤掉 start = end = 0 的错误 - .filter(|error| { - // 仅保留第一个相同 start 和 end 的错误 - let position_key = (error.start, error.end); - seen_positions.insert(position_key) - }) - .filter_map(|error| { - let start_position = offset_to_position(error.start, &m.rope)?; - let end_position = offset_to_position(error.end, &m.rope)?; - Some(Diagnostic::new_simple(Range::new(start_position, end_position), error.message)) - }) - .collect::>() - }; // MutexGuard 在这里被释放 - - // 现在可以安全地使用 await - self.client.publish_diagnostics(params.uri.clone(), diagnostics, params.version).await; - } -} +use nls::document::DocumentStore; +use nls::server::Backend; #[tokio::main] async fn main() { @@ -742,12 +11,13 @@ async fn main() { let stdin = tokio::io::stdin(); let stdout = tokio::io::stdout(); - let (service, socket) = LspService::build(|client| Backend { + let (service, socket) = LspService::new(|client| Backend { client, - // document_map: DashMap::new(), + documents: DocumentStore::new(), projects: DashMap::new(), - }) - .finish(); + debounce_versions: DashMap::new(), + config: DashMap::new(), + }); Server::new(stdin, stdout, socket).serve(service).await; } diff --git a/nls/src/package.rs b/nls/src/package.rs index eaf27df1..35c811ea 100644 --- a/nls/src/package.rs +++ b/nls/src/package.rs @@ -1,38 +1,31 @@ +//! Parse `package.toml` into the analyzer's [`PackageConfig`]. + use crate::analyzer::common::{AnalyzerError, PackageConfig}; -/** - * 解析 toml 解析正确返回 package config, 如果解析错误则返回 AnalyzerError 错误信息 - */ +/// Read and parse a `package.toml` file. +/// +/// Returns `Ok(PackageConfig)` on success, or an [`AnalyzerError`] with span +/// information pointing at the TOML parse error location. pub fn parse_package(path: &str) -> Result { let content = std::fs::read_to_string(path).map_err(|e| AnalyzerError { start: 0, end: 0, message: e.to_string(), + is_warning: false, })?; match toml::from_str(&content) { - Ok(package) => { - let package_config = PackageConfig { - path: path.to_string(), - package_data: package, - }; - - Ok(package_config) - } + Ok(package_data) => Ok(PackageConfig { + path: path.to_string(), + package_data, + }), Err(e) => { let span = e.span().unwrap_or(0..1); - // let start_position = offset_to_position(span.start, &rope).unwrap_or_default(); - // let end_position = offset_to_position(span.end, &rope).unwrap_or_default(); - - // let diagnostic = Diagnostic::new_simple(Range::new(start_position, end_position), e.message().to_string()); - // self.client - // .publish_diagnostics(Url::parse(&format!("file://{}", path)).unwrap(), vec![diagnostic], None) - // .await; - Err(AnalyzerError { start: span.start, end: span.end, message: e.message().to_string(), + is_warning: false, }) } } diff --git a/nls/src/project.rs b/nls/src/project.rs index cf0aea29..b5ca087e 100644 --- a/nls/src/project.rs +++ b/nls/src/project.rs @@ -1,3 +1,9 @@ +//! Project state: module database, symbol table, and multi-phase build pipeline. +//! +//! A [`Project`] represents a single workspace root. It owns the full +//! compilation state: all parsed modules, the cross-module symbol table, and +//! the workspace-wide symbol index. + use crate::analyzer::common::{AnalyzerError, AstFnDef, AstNode, ImportStmt, PackageConfig, Stmt}; use crate::analyzer::flow::Flow; use crate::analyzer::generics::Generics; @@ -7,50 +13,70 @@ use crate::analyzer::semantic::Semantic; use crate::analyzer::symbol::{NodeId, SymbolTable}; use crate::analyzer::syntax::Syntax; use crate::analyzer::typesys::Typesys; +use crate::analyzer::workspace_index::WorkspaceIndex; use crate::analyzer::{analyze_imports, register_global_symbol}; use crate::package::parse_package; -use log::debug; +use log::{debug, error}; use ropey::Rope; use std::collections::{HashMap, HashSet}; +use std::panic::{catch_unwind, AssertUnwindSafe}; use std::path::Path; +use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, Mutex}; +/// Default path to the Nature standard library root. pub const DEFAULT_NATURE_ROOT: &str = "/usr/local/nature"; -// pub const DEFAULT_NATURE_ROOT: &str = "/Users/weiwenhao/Code/nature"; -// 单个文件称为 module, package 通常是包含 package.toml 的多级目录 +// ─── Module ───────────────────────────────────────────────────────────────────── + +/// A single compiled `.n` source file. #[derive(Debug, Clone)] pub struct Module { pub index: usize, pub ident: String, - pub source: String, // 源码内容 + /// Full source text. + pub source: String, pub rope: Rope, - pub path: String, // 文件 路径 - pub dir: String, // 文件 所在目录 + /// Absolute file path. + pub path: String, + /// Parent directory. + pub dir: String, + /// Lexer tokens. pub token_db: Vec, pub token_indexes: Vec, + /// Semantic (resolved) tokens for semantic-token requests. pub sem_token_db: Vec, + /// Top-level statements (AST). pub stmts: Vec>, pub global_vardefs: Vec, pub global_fndefs: Vec>>, - pub all_fndefs: Vec>>, // 包含 global 和 local fn def + /// All function definitions (global + local). + pub all_fndefs: Vec>>, + /// Errors collected during analysis. pub analyzer_errors: Vec, - - pub scope_id: NodeId, // 当前 module 对应的 scope - - pub references: Vec, // 哪些模块依赖于当前模块 - pub dependencies: Vec, // 当前模块依赖 哪些模块 + /// Scope id for this module in the symbol table. + pub scope_id: NodeId, + /// Modules that depend on this one (reverse deps). + pub references: Vec, + /// Modules this one imports (forward deps). + pub dependencies: Vec, } impl Module { - pub fn new(ident: String, source: String, path: String, index: usize, scope_id: NodeId) -> Self { - // 计算 module ident, 和 analyze_import 中的 import.module_ident 需要采取相同的策略 + pub fn new( + ident: String, + source: String, + path: String, + index: usize, + scope_id: NodeId, + ) -> Self { + let dir = Path::new(&path) + .parent() + .and_then(|p| p.to_str()) + .unwrap_or("") + .to_string(); + let rope = Rope::from_str(&source); - let dir = Path::new(&path).parent().and_then(|p| p.to_str()).unwrap_or("").to_string(); - - let rope = ropey::Rope::from_str(&source); - - // create module scope Self { index, ident, @@ -72,13 +98,11 @@ impl Module { } } + /// Check if the on-disk source differs from the cached source. pub fn need_rebuild(&self) -> bool { - // 读取文件最新内容 - if let Ok(content) = std::fs::read_to_string(&self.path) { - // 如果内容发生变化,需要重新构建 - return content != self.source; - } - false + std::fs::read_to_string(&self.path) + .map(|content| content != self.source) + .unwrap_or(false) } } @@ -87,10 +111,10 @@ impl Default for Module { Self { scope_id: 0, index: 0, - ident: "".to_string(), - source: "".to_string(), - path: "".to_string(), - dir: "".to_string(), + ident: String::new(), + source: String::new(), + path: String::new(), + dir: String::new(), references: Vec::new(), dependencies: Vec::new(), token_db: Vec::new(), @@ -106,392 +130,432 @@ impl Default for Module { } } +// ─── Queue ────────────────────────────────────────────────────────────────────── + +/// An item in the rebuild queue. #[derive(Debug, Clone)] pub struct QueueItem { pub path: String, - pub notify: Option, // 编译完成后需要通知的 module + /// If set, the module at this path should be notified after build completes. + pub notify: Option, } +// ─── Project ──────────────────────────────────────────────────────────────────── + +/// Owns the full analysis state for a workspace root. #[derive(Debug, Clone)] pub struct Project { + /// Path to the Nature standard library. pub nature_root: String, + /// Workspace root path. pub root: String, - pub module_db: Arc>>, // key = uri, 记录所有已经编译的 module - pub module_handled: Arc>>, // key = path, 记录所有已经编译的 module, usize 指向 module db - // queue 中的每一个 module 都可以视为 main.n 来编译,主要是由于用户打开文件 A import B or C 产生的 B 和 C 注册到 queue 中进行处理 + /// All compiled modules. + pub module_db: Arc>>, + /// path → index into `module_db`. + pub module_handled: Arc>>, + /// Async rebuild queue for dependent modules. pub queue: Arc>>, - pub package_config: Arc>>, // 当前 project 如果包含 package.toml, 则可以解析出 package_config 等信息,import 需要借助该信息进行解析 + /// Parsed `package.toml`, if present. + pub package_config: Arc>>, + /// Cross-module symbol table. pub symbol_table: Arc>, + /// Lightweight workspace-wide symbol index (for workspace/symbol search). + pub workspace_index: Arc>, + /// Guard: true while a build is in progress. + pub is_building: Arc, } impl Project { + /// Create a new project for the given workspace root. + /// + /// Loads builtin modules from `$NATURE_ROOT/std/builtin/` and parses + /// `package.toml` if present. pub async fn new(project_root: String) -> Self { - // 1. check nature root by env - let nature_root = std::env::var("NATURE_ROOT").unwrap_or(DEFAULT_NATURE_ROOT.to_string()); + let nature_root = + std::env::var("NATURE_ROOT").unwrap_or_else(|_| DEFAULT_NATURE_ROOT.to_string()); + // Collect builtin .n files. let mut builtin_list: Vec = Vec::new(); - - // 3. builtin package load by nature root std - let std_dir = Path::new(&nature_root).join("std"); - let std_builtin_dir = std_dir.join("builtin"); - - // 加载 builtin 中的所有文件(.n 结尾) - let dirs = std::fs::read_dir(std_builtin_dir).unwrap(); - for dir in dirs { - let dir_path = dir.unwrap().path(); - if !dir_path.is_file() { - continue; - } - - let file_name = dir_path.file_name().unwrap().to_str().unwrap(); - if !file_name.ends_with(".n") { - continue; + let std_builtin_dir = Path::new(&nature_root).join("std").join("builtin"); + if let Ok(entries) = std::fs::read_dir(&std_builtin_dir) { + for entry in entries.flatten() { + let p = entry.path(); + if p.is_file() && p.extension().map_or(false, |ext| ext == "n") { + if let Some(s) = p.to_str() { + builtin_list.push(s.to_string()); + } + } } - - // 将 完整文件路径加入到 await_queue 中, 进行异步解析 - let file_path = dir_path.to_str().unwrap(); - - builtin_list.push(file_path.to_string()); } - let package_path = Path::new(&project_root).join("package.toml").to_str().unwrap().to_string(); + // Parse package.toml. + let package_path = Path::new(&project_root) + .join("package.toml") + .to_string_lossy() + .to_string(); let package_config = match parse_package(&package_path) { - Ok(package_config) => Arc::new(Mutex::new(Some(package_config))), - Err(_e) => Arc::new(Mutex::new(None)), + Ok(cfg) => Arc::new(Mutex::new(Some(cfg))), + Err(_) => Arc::new(Mutex::new(None)), }; let mut project = Self { - nature_root, - root: project_root, + nature_root: nature_root.clone(), + root: project_root.clone(), module_db: Arc::new(Mutex::new(Vec::new())), module_handled: Arc::new(Mutex::new(HashMap::new())), queue: Arc::new(Mutex::new(Vec::new())), package_config, symbol_table: Arc::new(Mutex::new(SymbolTable::new())), + workspace_index: Arc::new(Mutex::new(WorkspaceIndex::new())), + is_building: Arc::new(AtomicBool::new(false)), }; - // handle builtin list - for file_path in builtin_list { - project.build(&file_path, "", None).await; + // Initial workspace scan. + { + let mut index = project.workspace_index.lock().unwrap(); + index.scan_workspace(&project_root, &nature_root); + } + + // Build builtins. + for path in builtin_list { + project.build(&path, "", None).await; } - return project; + project } - pub fn backend_handle_queue(&self) { - let mut self_clone = self.clone(); + /// Spawn a background task that continuously drains the rebuild queue. + pub fn start_queue_worker(&self) { + let mut clone = self.clone(); tokio::spawn(async move { - self_clone.handle_queue().await; + clone.run_queue().await; }); } - pub async fn need_build(&self, item: &QueueItem) -> bool { - let module_handled = self.module_handled.lock().unwrap(); - let index_option = module_handled.get(&item.path); - if let Some(index) = index_option { - let module_db = self.module_db.lock().unwrap(); - let m = &module_db[*index]; - return m.need_rebuild(); - } else { - return true; - } - } - - pub async fn handle_queue(&mut self) { + async fn run_queue(&mut self) { loop { - // 尝试从 await_queue 中获取一个 file, 当前语句结束之后,await_queue 会自动解锁 - let item_option = self.queue.lock().unwrap().pop(); - if item_option.is_none() { - tokio::time::sleep(tokio::time::Duration::from_millis(100)).await; - continue; - } - - let item = item_option.unwrap(); - - if !self.need_build(&item).await { - continue; + let item = self.queue.lock().unwrap().pop(); + match item { + Some(item) => { + if self.needs_build(&item) { + self.build(&item.path, "", None).await; + } + } + None => { + tokio::time::sleep(tokio::time::Duration::from_millis(100)).await; + } } + } + } - // build - self.build(&item.path, "", None).await; + fn needs_build(&self, item: &QueueItem) -> bool { + let handled = self.module_handled.lock().unwrap(); + if let Some(&idx) = handled.get(&item.path) { + let db = self.module_db.lock().unwrap(); + db[idx].need_rebuild() + } else { + true } } - /** - * 当前 module 更新后,需要更新所有依赖了当前 module 的 module - */ + /// Collect all modules that transitively depend on `index`. pub fn all_references(&self, index: usize) -> Vec { - let mut handled: HashSet = HashSet::new(); - let mut result: Vec = Vec::new(); + let mut handled = HashSet::new(); + let mut result = Vec::new(); + let mut worklist = vec![index]; - let mut worklist: Vec = Vec::new(); - worklist.push(index); - - while let Some(current_index) = worklist.pop() { - // 如果已经处理过该模块,则跳过 - if handled.contains(¤t_index) { + while let Some(current) = worklist.pop() { + if !handled.insert(current) { continue; } - // 标记该模块已处理 - handled.insert(current_index); - - let module_db = self.module_db.lock().unwrap(); - let current_module = &module_db[current_index]; - - // 遍历所有引用当前模块的模块 - for &ref_index in ¤t_module.references { - let ref_module = &module_db[ref_index]; - result.push(ref_module.path.clone()); - - // 将引用模块加入工作列表,以便递归查找 - worklist.push(ref_index); + let db = self.module_db.lock().unwrap(); + if let Some(m) = db.get(current) { + for &ref_idx in &m.references { + if let Some(ref_m) = db.get(ref_idx) { + result.push(ref_m.path.clone()); + worklist.push(ref_idx); + } + } } } result } - pub async fn build(&mut self, main_path: &str, module_ident: &str, content_option: Option) -> usize { - // 所有未编译的 import 模块, 都需要进行关联处理 + // ── Build pipeline ────────────────────────────────────────────────── + + /// Run the full build pipeline for a module and its transitive imports. + pub async fn build( + &mut self, + main_path: &str, + module_ident: &str, + content: Option, + ) -> usize { + self.is_building.store(true, Ordering::SeqCst); + let result = self.build_inner(main_path, module_ident, content).await; + self.is_building.store(false, Ordering::SeqCst); + result + } + + async fn build_inner( + &mut self, + main_path: &str, + module_ident: &str, + content: Option, + ) -> usize { let mut worklist: Vec = Vec::new(); let mut handled: HashSet = HashSet::new(); - let mut module_indexes = Vec::new(); + let mut module_indexes: Vec = Vec::new(); - // 创建一个简单的 import 语句作为起始点 + // Seed with the main module. let mut main_import = ImportStmt::default(); main_import.full_path = main_path.to_string(); main_import.module_ident = module_ident.to_string(); - worklist.push(main_import); handled.insert(main_path.to_string()); - debug!("{} handle work list", main_path); + // ── Phase 1: Lex + Parse + collect imports ────────────────────── + debug!("{} processing worklist", main_path); while let Some(import_stmt) = worklist.pop() { - let module_handled = self.module_handled.lock().unwrap(); - let index_option = module_handled.get(&import_stmt.full_path).copied(); - drop(module_handled); - - let index: usize = if let Some(i) = index_option { - // 如果 import module 已经存在 module 则不需要进行重复编译, main path module 则进行强制更新 - if import_stmt.full_path == main_path { - // 需要更新现有模块的内容(也就是当前文件) - if let Some(ref content) = content_option { - let mut module_db = self.module_db.lock().unwrap(); - let m = &mut module_db[i]; - m.source = content.clone(); - m.rope = ropey::Rope::from_str(&m.source); + let index = { + let module_handled = self.module_handled.lock().unwrap(); + let existing = module_handled.get(&import_stmt.full_path).copied(); + drop(module_handled); + + if let Some(i) = existing { + if import_stmt.full_path == main_path { + // Update content of the main module. + if let Some(ref c) = content { + let mut db = self.module_db.lock().unwrap(); + let m = &mut db[i]; + m.source = c.clone(); + m.rope = Rope::from_str(&m.source); + } + i + } else { + continue; // already compiled } - i.clone() } else { - continue; - } - } else { - let content_option = std::fs::read_to_string(import_stmt.full_path.clone()); - if content_option.is_err() { - continue; + // New module — read from disk. + let Ok(file_content) = std::fs::read_to_string(&import_stmt.full_path) else { + continue; + }; + + let mut mh = self.module_handled.lock().unwrap(); + let mut db = self.module_db.lock().unwrap(); + let idx = db.len(); + let scope_id = self + .symbol_table + .lock() + .unwrap() + .create_module_scope(import_stmt.module_ident.clone()); + db.push(Module::new( + import_stmt.module_ident, + file_content, + import_stmt.full_path.clone(), + idx, + scope_id, + )); + mh.insert(import_stmt.full_path, idx); + idx } - let content = content_option.unwrap(); - - // push to module_db get index lock - let mut module_handled = self.module_handled.lock().unwrap(); - let mut module_db = self.module_db.lock().unwrap(); - let index = module_db.len(); - - // create module scope - let scope_id = self.symbol_table.lock().unwrap().create_module_scope(import_stmt.module_ident.clone()); - - let temp = Module::new(import_stmt.module_ident, content, import_stmt.full_path.to_string(), index, scope_id); - module_db.push(temp); - - // set module_handled - module_handled.insert(import_stmt.full_path, index); - // unlock - - index }; - let mut module_db = self.module_db.lock().unwrap(); - let m = &mut module_db[index]; + let mut db = self.module_db.lock().unwrap(); + let m = &mut db[index]; debug!( - "build, m.index {}, m.path {}, module_dir {}, project root {}", + "build module #{} path={} dir={} root={}", m.index, m.path, m.dir, self.root ); - // clean module symbol table - self.symbol_table.lock().unwrap().clean_module_scope(m.ident.clone()); + // Clean scope for re-analysis. + self.symbol_table + .lock() + .unwrap() + .clean_module_scope(m.ident.clone()); - // - lexer + // Lex. let (token_db, token_indexes, lexer_errors) = Lexer::new(m.source.clone()).scan(); m.token_db = token_db.clone(); m.token_indexes = token_indexes.clone(); - m.analyzer_errors = lexer_errors; // 清空 error 从 analyzer 起重新计算 + m.analyzer_errors = lexer_errors; - // - parser - let (mut stmts, sem_token_db, syntax_errors) = Syntax::new(m.clone(), token_db, token_indexes).parser(); - m.sem_token_db = sem_token_db.clone(); - // m.stmts = stmts; + // Parse. + let (mut stmts, sem_token_db, syntax_errors) = + Syntax::new(m.clone(), token_db, token_indexes).parser(); + m.sem_token_db = sem_token_db; m.analyzer_errors.extend(syntax_errors); - // collection all relation module module_indexes.push(index); - // analyzer global ast to symbol table - let mut symbol_table = self.symbol_table.lock().unwrap(); - register_global_symbol(m, &mut symbol_table, &stmts); - drop(symbol_table); + // Register global symbols. + let mut st = self.symbol_table.lock().unwrap(); + register_global_symbol(m, &mut st, &stmts); + drop(st); - // analyzer imports to worklist + // Resolve imports. let imports = analyze_imports(self.root.clone(), &self.package_config, m, &mut stmts); m.stmts = stmts; let mut filter_imports: Vec = Vec::new(); - - // import to worklist for import in imports { - // handle 重复进入表示 build module 发生了循环引用, 发送错误并跳过该 import 处理。 - if handled.contains(&import.full_path) { - // TODO 暂时不做处理 - // errors_push( - // m, - // AnalyzerError { - // start: import.start, - // end: import.end, - // message: format!("circular import"), - // }, - // ); - - debug!("circular import, will handle {}", import.full_path); - // continue; - } - if import.full_path == main_path { continue; } - filter_imports.push(import.clone()); - worklist.push(import.clone()); - handled.insert(import.full_path.clone()); + if !handled.contains(&import.full_path) { + worklist.push(import.clone()); + handled.insert(import.full_path.clone()); + } } - drop(module_db); + drop(db); - // to module dep, and call diff (add and remove) - self.update_module_dep(index, filter_imports); + self.update_module_deps(index, filter_imports); } - debug!("{} will semantic handle, module_indexes: {:?}", main_path, &module_indexes); + // ── Phase 2: Semantic analysis passes ─────────────────────────── + debug!( + "{} semantic passes, modules: {:?}", + main_path, module_indexes + ); - for index in module_indexes.clone() { - let mut module_db = self.module_db.lock().unwrap(); - let mut symbol_table = self.symbol_table.lock().unwrap(); - let m = &mut module_db[index]; + for &idx in &module_indexes { + let mut db = self.module_db.lock().unwrap(); + let mut st = self.symbol_table.lock().unwrap(); + let m = &mut db[idx]; m.all_fndefs = Vec::new(); - Semantic::new(m, &mut symbol_table).analyze(); + if let Err(e) = catch_unwind(AssertUnwindSafe(|| { + Semantic::new(m, &mut st).analyze(); + })) { + error!("panic in semantic pass for module #{}: {:?}", idx, e); + } } - for index in module_indexes.clone() { - let mut module_db = self.module_db.lock().unwrap(); - let symbol_table = self.symbol_table.lock().unwrap(); - let m = &mut module_db[index]; - Generics::new(m, &symbol_table).analyze(); + for &idx in &module_indexes { + let mut db = self.module_db.lock().unwrap(); + let st = self.symbol_table.lock().unwrap(); + let m = &mut db[idx]; + if let Err(e) = catch_unwind(AssertUnwindSafe(|| { + Generics::new(m, &st).analyze(); + })) { + error!("panic in generics pass for module #{}: {:?}", idx, e); + } } - // all pre infer - for index in module_indexes.clone() { - let mut module_db = self.module_db.lock().unwrap(); - let mut symbol_table = self.symbol_table.lock().unwrap(); - let m = &mut module_db[index]; - let errors = Typesys::new(&mut symbol_table, m).pre_infer(); - m.analyzer_errors.extend(errors); + for &idx in &module_indexes { + let mut db = self.module_db.lock().unwrap(); + let mut st = self.symbol_table.lock().unwrap(); + let m = &mut db[idx]; + match catch_unwind(AssertUnwindSafe(|| { + Typesys::new(&mut st, m).pre_infer() + })) { + Ok(errors) => m.analyzer_errors.extend(errors), + Err(e) => error!("panic in typesys pre_infer for module #{}: {:?}", idx, e), + } } - for index in module_indexes.clone() { - let mut module_db = self.module_db.lock().unwrap(); - let mut symbol_table = self.symbol_table.lock().unwrap(); - let m = &mut module_db[index]; - let errors = GlobalEval::new(m, &mut symbol_table).analyze(); - m.analyzer_errors.extend(errors); + for &idx in &module_indexes { + let mut db = self.module_db.lock().unwrap(); + let mut st = self.symbol_table.lock().unwrap(); + let m = &mut db[idx]; + match catch_unwind(AssertUnwindSafe(|| { + GlobalEval::new(m, &mut st).analyze() + })) { + Ok(errors) => m.analyzer_errors.extend(errors), + Err(e) => error!("panic in global_eval for module #{}: {:?}", idx, e), + } } - for index in module_indexes.clone() { - let mut module_db = self.module_db.lock().unwrap(); - let mut symbol_table = self.symbol_table.lock().unwrap(); - let m = &mut module_db[index]; - let mut errors = Typesys::new(&mut symbol_table, m).infer(); - m.analyzer_errors.extend(errors); - - // check returns - errors = Flow::new(m).analyze(); - m.analyzer_errors.extend(errors); + for &idx in &module_indexes { + let mut db = self.module_db.lock().unwrap(); + let mut st = self.symbol_table.lock().unwrap(); + let m = &mut db[idx]; + match catch_unwind(AssertUnwindSafe(|| { + let errors = Typesys::new(&mut st, m).infer(); + let flow_errors = Flow::new(m).analyze(); + (errors, flow_errors) + })) { + Ok((errors, flow_errors)) => { + m.analyzer_errors.extend(errors); + m.analyzer_errors.extend(flow_errors); + } + Err(e) => error!("panic in typesys infer/flow for module #{}: {:?}", idx, e), + } } - // handle all refers - let module_handled = self.module_handled.lock().unwrap(); - let index_option = module_handled.get(main_path); - let main_index = match index_option { - Some(i) => i.clone(), - None => { - return 0; + // ── Phase 3: Queue dependent rebuilds ─────────────────────────── + let main_index = { + let mh = self.module_handled.lock().unwrap(); + match mh.get(main_path).copied() { + Some(i) => i, + None => return 0, } }; let refers = self.all_references(main_index); - - // refers push to queue for refer in refers { - debug!("{} handle to queue, refer {}", main_path, refer); if refer == main_path { continue; } - + debug!("{} queuing dependent: {}", main_path, refer); self.queue.lock().unwrap().push(QueueItem { path: refer, - notify: Some(main_path.to_string()), // 当引用模块编译完成后,通知主模块重新编译(主模块必定已经注册完成,包含完整的 module 信息) + notify: Some(main_path.to_string()), }); } - return main_index; - } - - /** - * 更新 module 的依赖, 尤其是反向依赖的 references 更新 - */ - pub fn update_module_dep(&mut self, module_index: usize, imports: Vec) { - // 当前模块依赖这些目标模块,这些目标模块的 refers 需要进行相应的更新 - let mut dependency_indices = Vec::new(); + // Re-index the built file. { - let module_handled = self.module_handled.lock().unwrap(); - for import in &imports { - if let Some(&index) = module_handled.get(&import.full_path) { - dependency_indices.push(index); - } - } + let mut ws = self.workspace_index.lock().unwrap(); + ws.remove_file_symbols(main_path); + ws.index_file(main_path, &self.root); + debug!("workspace index: re-indexed {}", main_path); } - // 然后处理 module_db - let mut module_db = self.module_db.lock().unwrap(); - let m = &mut module_db[module_index]; - - // 将当前的依赖转换为 HashSet - let old_deps: HashSet = m.dependencies.iter().map(|dep| dep.full_path.clone()).collect(); - let new_deps: HashSet = imports.iter().map(|dep| dep.full_path.clone()).collect(); - - // old deps 存在,但是 new deps 中删除的数据 - for removed_dep in old_deps.difference(&new_deps) { - if let Some(dep_index) = dependency_indices.iter().find(|&&i| module_db[i].path == *removed_dep) { - let dep_module = &mut module_db[*dep_index]; - dep_module.references.retain(|&x| x != module_index); + + main_index + } + + /// Update forward/reverse dependency links for a module. + fn update_module_deps(&mut self, module_index: usize, imports: Vec) { + let dep_indices: Vec = { + let mh = self.module_handled.lock().unwrap(); + imports + .iter() + .filter_map(|imp| mh.get(&imp.full_path).copied()) + .collect() + }; + + let mut db = self.module_db.lock().unwrap(); + + // Remove stale reverse references. + let old_deps: HashSet = db[module_index] + .dependencies + .iter() + .map(|d| d.full_path.clone()) + .collect(); + let new_deps: HashSet = imports.iter().map(|d| d.full_path.clone()).collect(); + + for removed in old_deps.difference(&new_deps) { + if let Some(&dep_idx) = dep_indices + .iter() + .find(|&&i| db[i].path == *removed) + { + db[dep_idx].references.retain(|&x| x != module_index); } } - // 更新当前模块的依赖列表 - module_db[module_index].dependencies = imports.clone(); - - // 添加反向引用关系 - for import in imports { - if let Some(dep_index) = dependency_indices.iter().find(|&&i| module_db[i].path == import.full_path) { - let dep_module = &mut module_db[*dep_index]; - if !dep_module.references.contains(&module_index) { - dep_module.references.push(module_index); + // Update forward deps. + db[module_index].dependencies = imports.clone(); + + // Add reverse references. + for import in &imports { + if let Some(&dep_idx) = dep_indices + .iter() + .find(|&&i| db[i].path == import.full_path) + { + if !db[dep_idx].references.contains(&module_index) { + db[dep_idx].references.push(module_index); } } } diff --git a/nls/src/server/capabilities.rs b/nls/src/server/capabilities.rs new file mode 100644 index 00000000..4c8ded7c --- /dev/null +++ b/nls/src/server/capabilities.rs @@ -0,0 +1,276 @@ +//! `initialize` / `initialized` — capability advertisement and startup. + +use log::debug; +use tower_lsp::lsp_types::*; + +use crate::analyzer::lexer::LEGEND_TYPE; +use crate::project::Project; + +use super::Backend; + +/// Pure function that constructs `ServerCapabilities`. +/// +/// Extracted so it can be snapshot-tested without needing an LSP client. +pub fn build_server_capabilities() -> ServerCapabilities { + ServerCapabilities { + // ── Text sync (incremental) ───────────────────────────────── + text_document_sync: Some(TextDocumentSyncCapability::Options( + TextDocumentSyncOptions { + open_close: Some(true), + change: Some(TextDocumentSyncKind::INCREMENTAL), + save: Some(TextDocumentSyncSaveOptions::SaveOptions(SaveOptions { + include_text: Some(true), + })), + ..Default::default() + }, + )), + + // ── Semantic tokens ───────────────────────────────────────── + semantic_tokens_provider: Some( + SemanticTokensServerCapabilities::SemanticTokensRegistrationOptions( + SemanticTokensRegistrationOptions { + text_document_registration_options: TextDocumentRegistrationOptions { + document_selector: Some(vec![DocumentFilter { + language: Some("n".into()), + scheme: Some("file".into()), + pattern: None, + }]), + }, + semantic_tokens_options: SemanticTokensOptions { + work_done_progress_options: Default::default(), + legend: SemanticTokensLegend { + token_types: LEGEND_TYPE.into(), + token_modifiers: vec![], + }, + range: Some(true), + full: Some(SemanticTokensFullOptions::Bool(true)), + }, + static_registration_options: Default::default(), + }, + ), + ), + + // ── Workspace ─────────────────────────────────────────────── + workspace: Some(WorkspaceServerCapabilities { + workspace_folders: Some(WorkspaceFoldersServerCapabilities { + supported: Some(true), + change_notifications: Some(OneOf::Left(true)), + }), + file_operations: None, + }), + + // ── Navigation (Tier 3 + 7) ──────────────────────────────────── + definition_provider: Some(OneOf::Left(true)), + type_definition_provider: Some(TypeDefinitionProviderCapability::Simple(true)), + implementation_provider: Some(ImplementationProviderCapability::Simple(true)), + references_provider: Some(OneOf::Left(true)), + document_symbol_provider: Some(OneOf::Left(true)), + workspace_symbol_provider: Some(OneOf::Left(true)), + + // ── Inline information (Tier 4) ────────────────────────────── + hover_provider: Some(HoverProviderCapability::Simple(true)), + inlay_hint_provider: Some(OneOf::Left(true)), + signature_help_provider: Some(SignatureHelpOptions { + trigger_characters: Some(vec!["(".into(), ",".into()]), + retrigger_characters: None, + work_done_progress_options: Default::default(), + }), + + // ── Completions (Tier 5) ───────────────────────────────────── + completion_provider: Some(CompletionOptions { + trigger_characters: Some(vec![".".into()]), + resolve_provider: None, + work_done_progress_options: Default::default(), + all_commit_characters: None, + completion_item: None, + }), + + // ── Code actions (Tier 6) ──────────────────────────────────── + code_action_provider: Some(CodeActionProviderCapability::Options( + CodeActionOptions { + code_action_kinds: Some(vec![ + CodeActionKind::QUICKFIX, + CodeActionKind::new("source.organizeImports"), + ]), + work_done_progress_options: Default::default(), + resolve_provider: None, + }, + )), + + // ── Features not yet enabled ──────────────────────────────────── + // rename_provider: None, + + ..ServerCapabilities::default() + } +} + +impl Backend { + /// Build the full `ServerCapabilities` returned to the client. + pub(crate) async fn handle_initialize(&self, params: InitializeParams) -> ServerCapabilities { + // Register workspace folders and create projects for each. + if let Some(workspace_folders) = params.workspace_folders { + for folder in workspace_folders { + let Some(project_root) = folder + .uri + .to_file_path() + .ok() + .map(|p| p.to_string_lossy().to_string()) + else { + debug!("skipping workspace folder with non-file URI: {}", folder.uri); + continue; + }; + + let project = Project::new(project_root.clone()).await; + project.start_queue_worker(); + debug!("project created: {}", project_root); + self.projects.insert(project_root, project); + } + } + + build_server_capabilities() + } + + /// Called after the client ACKs `initialize`. Register file watchers and + /// pull initial configuration. + pub(crate) async fn handle_initialized(&self) { + debug!("initialized"); + + // Register file watchers for .n files and package.toml. + let registrations = vec![Registration { + id: "nature-file-watcher".into(), + method: "workspace/didChangeWatchedFiles".into(), + register_options: serde_json::to_value(DidChangeWatchedFilesRegistrationOptions { + watchers: vec![ + FileSystemWatcher { + glob_pattern: GlobPattern::String("**/*.n".into()), + kind: Some(WatchKind::Create | WatchKind::Delete), + }, + FileSystemWatcher { + glob_pattern: GlobPattern::String("**/package.toml".into()), + kind: Some(WatchKind::all()), + }, + ], + }) + .ok(), + }]; + + if let Err(e) = self.client.register_capability(registrations).await { + debug!("failed to register file watchers: {:?}", e); + } + + // Pull initial configuration. + if let Ok(response) = self + .client + .configuration(vec![ConfigurationItem { + scope_uri: None, + section: Some("nature".into()), + }]) + .await + { + if let Some(settings) = response.into_iter().next() { + self.apply_config(&settings); + } + } + } +} + +// ─── Tests ────────────────────────────────────────────────────────────────────── + +#[cfg(test)] +mod tests { + use super::*; + + /// If someone accidentally removes `text_document_sync`, this fails. + #[test] + fn caps_has_text_document_sync() { + let caps = build_server_capabilities(); + let sync = caps + .text_document_sync + .expect("text_document_sync must be set"); + match sync { + TextDocumentSyncCapability::Options(opts) => { + assert_eq!(opts.open_close, Some(true)); + assert_eq!(opts.change, Some(TextDocumentSyncKind::INCREMENTAL)); + // save must include text + match opts.save { + Some(TextDocumentSyncSaveOptions::SaveOptions(save)) => { + assert_eq!(save.include_text, Some(true)); + } + other => panic!("expected SaveOptions, got {:?}", other), + } + } + other => panic!("expected Options variant, got {:?}", other), + } + } + + /// Semantic tokens must be enabled with full + range support. + #[test] + fn caps_has_semantic_tokens() { + let caps = build_server_capabilities(); + let provider = caps + .semantic_tokens_provider + .expect("semantic_tokens_provider must be set"); + match provider { + SemanticTokensServerCapabilities::SemanticTokensRegistrationOptions(reg) => { + let opts = ®.semantic_tokens_options; + assert_eq!(opts.full, Some(SemanticTokensFullOptions::Bool(true))); + assert_eq!(opts.range, Some(true)); + assert!( + !opts.legend.token_types.is_empty(), + "legend must have token types" + ); + } + other => panic!("expected registration options, got {:?}", other), + } + } + + /// Semantic tokens document filter must target "nature" language. + #[test] + fn caps_semantic_tokens_filter_is_nature() { + let caps = build_server_capabilities(); + if let Some(SemanticTokensServerCapabilities::SemanticTokensRegistrationOptions(reg)) = + caps.semantic_tokens_provider + { + let filters = reg + .text_document_registration_options + .document_selector + .expect("document_selector must be set"); + assert_eq!(filters.len(), 1); + assert_eq!(filters[0].language.as_deref(), Some("n")); + assert_eq!(filters[0].scheme.as_deref(), Some("file")); + } else { + panic!("semantic_tokens_provider missing or wrong variant"); + } + } + + /// Workspace folders must be supported. + #[test] + fn caps_has_workspace_folders() { + let caps = build_server_capabilities(); + let workspace = caps.workspace.expect("workspace must be set"); + let folders = workspace + .workspace_folders + .expect("workspace_folders must be set"); + assert_eq!(folders.supported, Some(true)); + assert_eq!( + folders.change_notifications, + Some(OneOf::Left(true)) + ); + } + + /// Features we haven't enabled yet must still be None. + #[test] + fn caps_disabled_features_are_none() { + let caps = build_server_capabilities(); + assert!(caps.definition_provider.is_some(), "definition must be enabled"); + assert!(caps.references_provider.is_some(), "references must be enabled"); + assert!(caps.document_symbol_provider.is_some(), "document symbols must be enabled"); + assert!(caps.workspace_symbol_provider.is_some(), "workspace symbols must be enabled"); + assert!(caps.hover_provider.is_some(), "hover must be enabled"); + assert!(caps.inlay_hint_provider.is_some(), "inlay hints must be enabled"); + assert!(caps.signature_help_provider.is_some(), "signature help must be enabled"); + assert!(caps.completion_provider.is_some(), "completion must be enabled"); + assert!(caps.rename_provider.is_none(), "rename not yet enabled"); + assert!(caps.code_action_provider.is_some(), "code actions must be enabled"); + } +} diff --git a/nls/src/server/code_actions.rs b/nls/src/server/code_actions.rs new file mode 100644 index 00000000..ed9b5d59 --- /dev/null +++ b/nls/src/server/code_actions.rs @@ -0,0 +1,391 @@ +//! Code actions: quick-fixes and refactoring operations. +//! +//! Currently supports: +//! - **Remove unused import** — offer to delete import lines that have an +//! `UNNECESSARY` diagnostic published by `build_diagnostics`. +//! - **Organize imports** — sort imports alphabetically, grouped by kind: +//! std library → package/dotted → relative file. + +use log::debug; +use tower_lsp::lsp_types::*; + +use crate::utils::offset_to_position; + +use super::Backend; + +/// Prefix used in diagnostic messages for unused imports. +pub(crate) const UNUSED_IMPORT_MSG_PREFIX: &str = "is imported but never used"; + +// ─── Handler wiring ───────────────────────────────────────────────────────────── + +impl Backend { + pub(crate) async fn handle_code_action( + &self, + params: CodeActionParams, + ) -> Option> { + let uri = ¶ms.text_document.uri; + let file_path = uri.path(); + + // Grab module data then release locks. + let module_data = { + let project = self.get_file_project(file_path)?; + let module_index = { + let mh = project.module_handled.lock().ok()?; + *mh.get(file_path)? + }; + let module_db = project.module_db.lock().ok()?; + let module = module_db.get(module_index)?; + + let import_ranges: Vec<(usize, usize)> = module + .dependencies + .iter() + .filter(|imp| imp.start > 0 || imp.end > 0) + .map(|imp| (imp.start, imp.end)) + .collect(); + + ModuleData { + source: module.source.clone(), + rope: module.rope.clone(), + import_ranges, + } + }; + + let mut actions: Vec = Vec::new(); + + // ── Remove unused import quick-fixes ──────────────────────────── + for diag in ¶ms.context.diagnostics { + let is_unused_import = diag + .tags + .as_ref() + .map_or(false, |t| t.contains(&DiagnosticTag::UNNECESSARY)) + && diag.message.contains(UNUSED_IMPORT_MSG_PREFIX); + + if !is_unused_import { + continue; + } + + debug!("code action for unused import: {:?}", diag.range); + + let delete_range = expand_to_full_line(&module_data.source, &diag.range); + + let mut changes = std::collections::HashMap::new(); + changes.insert( + uri.clone(), + vec![TextEdit { + range: delete_range, + new_text: String::new(), + }], + ); + + actions.push(CodeActionOrCommand::CodeAction(CodeAction { + title: "Remove unused import".into(), + kind: Some(CodeActionKind::QUICKFIX), + diagnostics: Some(vec![diag.clone()]), + edit: Some(WorkspaceEdit { + changes: Some(changes), + ..Default::default() + }), + is_preferred: Some(true), + ..Default::default() + })); + } + + // ── Aggregate "Remove all" when ≥ 2 ──────────────────────────── + if actions.len() > 1 { + let mut all_edits: Vec = Vec::new(); + let mut all_diags: Vec = Vec::new(); + for action in &actions { + if let CodeActionOrCommand::CodeAction(ca) = action { + if let Some(ref edit) = ca.edit { + if let Some(ref changes) = edit.changes { + if let Some(edits) = changes.get(uri) { + all_edits.extend(edits.clone()); + } + } + } + if let Some(ref ds) = ca.diagnostics { + all_diags.extend(ds.clone()); + } + } + } + + let mut changes = std::collections::HashMap::new(); + changes.insert(uri.clone(), all_edits); + + actions.push(CodeActionOrCommand::CodeAction(CodeAction { + title: "Remove all unused imports".into(), + kind: Some(CodeActionKind::QUICKFIX), + diagnostics: Some(all_diags), + edit: Some(WorkspaceEdit { + changes: Some(changes), + ..Default::default() + }), + is_preferred: Some(false), + ..Default::default() + })); + } + + // ── Organize imports ──────────────────────────────────────────── + if let Some(organize_action) = build_organize_imports_action(uri, &module_data) { + actions.push(organize_action); + } + + if actions.is_empty() { + None + } else { + Some(actions) + } + } +} + +/// Temporary struct to carry module data out of the lock scope. +struct ModuleData { + source: String, + rope: ropey::Rope, + import_ranges: Vec<(usize, usize)>, +} + +// ─── Organize imports ─────────────────────────────────────────────────────────── + +/// Build a `source.organizeImports` code action that sorts imports by group: +/// 1. Standard library (simple ident: `fmt`, `time`, `co`) +/// 2. Package/dotted imports (`testpkg.lib.math`, `co.mutex`) +/// 3. Relative file imports (`'mod.n'`, `'utils.n'.{add}`) +/// +/// Within each group, imports are sorted case-insensitively. +/// Groups are separated by a blank line. +fn build_organize_imports_action( + uri: &Url, + data: &ModuleData, +) -> Option { + if data.import_ranges.is_empty() { + return None; + } + + // Extract each import's source text (start/end are char offsets). + let chars: Vec = data.source.chars().collect(); + let char_len = chars.len(); + let mut import_texts: Vec = Vec::new(); + for &(start, end) in &data.import_ranges { + if start < char_len && end <= char_len && start < end { + let text: String = chars[start..end].iter().collect(); + import_texts.push(text.trim_end().to_string()); + } + } + + if import_texts.is_empty() { + return None; + } + + // Classify and sort. + let mut std_imports: Vec = Vec::new(); + let mut pkg_imports: Vec = Vec::new(); + let mut file_imports: Vec = Vec::new(); + + for text in &import_texts { + match classify_import(text) { + ImportCategory::Std => std_imports.push(text.clone()), + ImportCategory::Package => pkg_imports.push(text.clone()), + ImportCategory::File => file_imports.push(text.clone()), + } + } + + std_imports.sort_by(|a, b| a.to_lowercase().cmp(&b.to_lowercase())); + pkg_imports.sort_by(|a, b| a.to_lowercase().cmp(&b.to_lowercase())); + file_imports.sort_by(|a, b| a.to_lowercase().cmp(&b.to_lowercase())); + + // Join groups with blank line separators. + let mut groups: Vec<&Vec> = Vec::new(); + if !std_imports.is_empty() { + groups.push(&std_imports); + } + if !pkg_imports.is_empty() { + groups.push(&pkg_imports); + } + if !file_imports.is_empty() { + groups.push(&file_imports); + } + + let sorted_text = groups + .iter() + .map(|g| g.join("\n")) + .collect::>() + .join("\n\n"); + + // Don't offer if already sorted. + let current_text = import_texts.join("\n"); + if current_text == sorted_text { + return None; + } + + // Range covering all imports. + let first_start = data.import_ranges.first()?.0; + let last_end = data.import_ranges.last()?.1; + + let start_pos = offset_to_position(first_start, &data.rope)?; + let end_offset = find_line_end_chars(&data.source, last_end); + let end_pos = offset_to_position(end_offset, &data.rope)?; + + let range = Range::new(Position::new(start_pos.line, 0), end_pos); + let new_text = format!("{}\n", sorted_text); + + let mut changes = std::collections::HashMap::new(); + changes.insert(uri.clone(), vec![TextEdit { range, new_text }]); + + Some(CodeActionOrCommand::CodeAction(CodeAction { + title: "Organize imports".into(), + kind: Some(CodeActionKind::new("source.organizeImports")), + diagnostics: None, + edit: Some(WorkspaceEdit { + changes: Some(changes), + ..Default::default() + }), + is_preferred: Some(true), + ..Default::default() + })) +} + +#[derive(Debug, PartialEq)] +enum ImportCategory { + /// Simple standard library: `import fmt`, `import time` + Std, + /// Dotted package path: `import co.mutex`, `import testpkg.lib.math` + Package, + /// Relative file: `import 'mod.n'`, `import 'utils.n'.{add}` + File, +} + +fn classify_import(text: &str) -> ImportCategory { + let after_import = text + .trim_start() + .strip_prefix("import") + .unwrap_or(text) + .trim_start(); + + if after_import.starts_with('\'') || after_import.starts_with('"') { + ImportCategory::File + } else if after_import.contains('.') { + ImportCategory::Package + } else { + ImportCategory::Std + } +} + +// ─── Shared helpers ───────────────────────────────────────────────────────────── + +/// Expand a range to cover the full line(s), including trailing newline. +fn expand_to_full_line(source: &str, range: &Range) -> Range { + let start_line = range.start.line; + let end_line = range.end.line; + let next_line = end_line + 1; + let line_count = source.matches('\n').count() + 1; + + let end_pos = if (next_line as usize) < line_count { + Position::new(next_line, 0) + } else { + let last_line_len = source.lines().last().map(|l| l.len() as u32).unwrap_or(0); + Position::new(end_line, last_line_len) + }; + + Range::new(Position::new(start_line, 0), end_pos) +} + +/// Find the end of the line containing char `offset` (past the `\n`). +fn find_line_end_chars(source: &str, offset: usize) -> usize { + let chars: Vec = source.chars().collect(); + let mut pos = offset; + while pos < chars.len() { + if chars[pos] == '\n' { + return pos + 1; + } + pos += 1; + } + pos +} + +// ─── Tests ────────────────────────────────────────────────────────────────────── + +#[cfg(test)] +mod tests { + use super::*; + + // ── expand_to_full_line ───────────────────────────────────────── + + #[test] + fn expand_to_full_line_middle() { + let src = "line0\nline1\nline2\n"; + let range = Range::new(Position::new(1, 2), Position::new(1, 5)); + let expanded = expand_to_full_line(src, &range); + assert_eq!(expanded.start, Position::new(1, 0)); + assert_eq!(expanded.end, Position::new(2, 0)); + } + + #[test] + fn expand_to_full_line_first() { + let src = "import fmt\nvar x = 1\n"; + let range = Range::new(Position::new(0, 0), Position::new(0, 10)); + let expanded = expand_to_full_line(src, &range); + assert_eq!(expanded.start, Position::new(0, 0)); + assert_eq!(expanded.end, Position::new(1, 0)); + } + + #[test] + fn expand_to_full_line_last_no_newline() { + let src = "import fmt\nimport http"; + let range = Range::new(Position::new(1, 0), Position::new(1, 11)); + let expanded = expand_to_full_line(src, &range); + assert_eq!(expanded.start, Position::new(1, 0)); + assert_eq!(expanded.end, Position::new(1, 11)); + } + + // ── classify_import ───────────────────────────────────────────── + + #[test] + fn classify_std() { + assert_eq!(classify_import("import fmt"), ImportCategory::Std); + assert_eq!(classify_import("import time"), ImportCategory::Std); + } + + #[test] + fn classify_package() { + assert_eq!(classify_import("import co.mutex"), ImportCategory::Package); + assert_eq!( + classify_import("import testpkg.lib.math.{add, square}"), + ImportCategory::Package + ); + assert_eq!( + classify_import("import forest.app.create"), + ImportCategory::Package + ); + } + + #[test] + fn classify_file() { + assert_eq!(classify_import("import 'mod.n'"), ImportCategory::File); + assert_eq!( + classify_import("import 'utils.n'.{MAX_SIZE}"), + ImportCategory::File + ); + assert_eq!( + classify_import("import 'mod.n' as *"), + ImportCategory::File + ); + } + + // ── sorting ───────────────────────────────────────────────────── + + #[test] + fn sort_groups_correctly() { + let mut std = vec!["import time".to_string(), "import fmt".to_string(), "import co".to_string()]; + let mut pkg = vec!["import co.mutex".to_string(), "import app.create".to_string()]; + let mut file = vec!["import 'z.n'".to_string(), "import 'a.n'".to_string()]; + + std.sort_by(|a, b| a.to_lowercase().cmp(&b.to_lowercase())); + pkg.sort_by(|a, b| a.to_lowercase().cmp(&b.to_lowercase())); + file.sort_by(|a, b| a.to_lowercase().cmp(&b.to_lowercase())); + + assert_eq!(std, vec!["import co", "import fmt", "import time"]); + assert_eq!(pkg, vec!["import app.create", "import co.mutex"]); + assert_eq!(file, vec!["import 'a.n'", "import 'z.n'"]); + } +} diff --git a/nls/src/server/completion.rs b/nls/src/server/completion.rs new file mode 100644 index 00000000..474541fa --- /dev/null +++ b/nls/src/server/completion.rs @@ -0,0 +1,231 @@ +//! Completion handler: delegates to the existing `CompletionProvider` in the +//! analyzer and converts its results into LSP `CompletionItem`s. + +use log::debug; +use tower_lsp::lsp_types::*; + +use crate::analyzer::completion::{CompletionItemKind as AnalyzerKind, CompletionProvider}; + +use super::Backend; + +// ─── Handler wiring ───────────────────────────────────────────────────────────── + +impl Backend { + pub(crate) async fn handle_completion( + &self, + params: CompletionParams, + ) -> Option { + let uri = ¶ms.text_document_position.text_document.uri; + let position = params.text_document_position.position; + let file_path = uri.path(); + + let project = self.get_file_project(file_path)?; + + // Acquire locks — ordering is module_handled → module_db → + // symbol_table, matching build_inner, so no deadlock is possible. + // We block briefly if a build is running rather than returning + // empty completions (which causes VS Code to fall back to its + // word-based suggestions). + let module_index = { + let mh = project.module_handled.lock().ok()?; + *mh.get(file_path)? + }; + + let mut module_db = project.module_db.lock().ok()?; + let module = module_db.get_mut(module_index)?; + + // Use the live document rope when available (more up-to-date than the + // last-analysed rope). + let live_doc = self.documents.get_by_path(file_path); + let rope = match &live_doc { + Some(d) => &d.rope, + None => &module.rope, + }; + + let line_char = rope.try_line_to_char(position.line as usize).ok()?; + let char_offset = line_char + position.character as usize; + let text = rope.to_string(); + + debug!( + "completion at offset={}, module='{}'", + char_offset, module.ident + ); + + let mut symbol_table = project.symbol_table.lock().ok()?; + let package_config = project.package_config.lock().ok()?.clone(); + let workspace_index = project.workspace_index.lock().ok()?; + + let items = CompletionProvider::new( + &mut symbol_table, + module, + project.nature_root.clone(), + project.root.clone(), + package_config, + ) + .with_workspace_index(&workspace_index) + .get_completions(char_offset, &text); + + // Drop heavy guards before building LSP items. + drop(workspace_index); + drop(symbol_table); + drop(module_db); + + let lsp_items: Vec = items.into_iter().map(to_lsp_item).collect(); + + debug!("returning {} completions", lsp_items.len()); + Some(CompletionResponse::List(CompletionList { + // Mark as incomplete so VS Code re-requests as the user + // types more characters, allowing prefix-dependent sources + // (std modules, workspace symbols, keywords) to appear. + is_incomplete: true, + items: lsp_items, + })) + } +} + +// ─── Conversion ───────────────────────────────────────────────────────────────── + +/// Map an analyzer `CompletionItem` → LSP `CompletionItem`. +fn to_lsp_item(item: crate::analyzer::completion::CompletionItem) -> CompletionItem { + let kind = map_kind(&item.kind); + let has_snippet = item.insert_text.contains("$0"); + + let additional_text_edits = if item.additional_text_edits.is_empty() { + None + } else { + Some( + item.additional_text_edits + .into_iter() + .map(|edit| TextEdit { + range: Range { + start: Position { + line: edit.line as u32, + character: edit.character as u32, + }, + end: Position { + line: edit.line as u32, + character: edit.character as u32, + }, + }, + new_text: edit.new_text, + }) + .collect(), + ) + }; + + CompletionItem { + label: item.label, + kind: Some(kind), + detail: item.detail, + documentation: item + .documentation + .map(Documentation::String), + insert_text: Some(item.insert_text), + insert_text_format: Some(if has_snippet { + InsertTextFormat::SNIPPET + } else { + InsertTextFormat::PLAIN_TEXT + }), + sort_text: item.sort_text, + additional_text_edits, + ..Default::default() + } +} + +/// Map analyzer completion kind → LSP completion kind. +fn map_kind(kind: &AnalyzerKind) -> CompletionItemKind { + match kind { + AnalyzerKind::Variable | AnalyzerKind::Parameter => CompletionItemKind::VARIABLE, + AnalyzerKind::Function => CompletionItemKind::FUNCTION, + AnalyzerKind::Constant => CompletionItemKind::CONSTANT, + AnalyzerKind::Module => CompletionItemKind::MODULE, + AnalyzerKind::Struct => CompletionItemKind::STRUCT, + AnalyzerKind::Keyword => CompletionItemKind::KEYWORD, + } +} + +// ─── Tests ────────────────────────────────────────────────────────────────────── + +#[cfg(test)] +mod tests { + use super::*; + use crate::analyzer::completion::{ + CompletionItem as AnalyzerItem, CompletionItemKind as AK, TextEdit as AnalyzerEdit, + }; + + #[test] + fn map_kind_function() { + assert_eq!(map_kind(&AK::Function), CompletionItemKind::FUNCTION); + } + + #[test] + fn map_kind_variable() { + assert_eq!(map_kind(&AK::Variable), CompletionItemKind::VARIABLE); + } + + #[test] + fn map_kind_parameter_maps_to_variable() { + assert_eq!(map_kind(&AK::Parameter), CompletionItemKind::VARIABLE); + } + + #[test] + fn map_kind_keyword() { + assert_eq!(map_kind(&AK::Keyword), CompletionItemKind::KEYWORD); + } + + #[test] + fn to_lsp_plain_text() { + let item = AnalyzerItem { + label: "count".into(), + kind: AK::Variable, + detail: Some("var: i64".into()), + documentation: None, + insert_text: "count".into(), + sort_text: None, + additional_text_edits: vec![], + }; + let lsp = to_lsp_item(item); + assert_eq!(lsp.label, "count"); + assert_eq!(lsp.kind, Some(CompletionItemKind::VARIABLE)); + assert_eq!(lsp.insert_text_format, Some(InsertTextFormat::PLAIN_TEXT)); + assert!(lsp.additional_text_edits.is_none()); + } + + #[test] + fn to_lsp_snippet() { + let item = AnalyzerItem { + label: "greet".into(), + kind: AK::Function, + detail: Some("fn(string): void".into()), + documentation: None, + insert_text: "greet($0)".into(), + sort_text: Some("00000010".into()), + additional_text_edits: vec![], + }; + let lsp = to_lsp_item(item); + assert_eq!(lsp.insert_text_format, Some(InsertTextFormat::SNIPPET)); + assert_eq!(lsp.insert_text.as_deref(), Some("greet($0)")); + } + + #[test] + fn to_lsp_with_auto_import_edit() { + let item = AnalyzerItem { + label: "fmt".into(), + kind: AK::Module, + detail: Some("import fmt".into()), + documentation: None, + insert_text: "fmt".into(), + sort_text: None, + additional_text_edits: vec![AnalyzerEdit { + line: 0, + character: 0, + new_text: "import fmt\n".into(), + }], + }; + let lsp = to_lsp_item(item); + let edits = lsp.additional_text_edits.expect("should have edits"); + assert_eq!(edits.len(), 1); + assert_eq!(edits[0].new_text, "import fmt\n"); + assert_eq!(edits[0].range.start.line, 0); + } +} diff --git a/nls/src/server/config.rs b/nls/src/server/config.rs new file mode 100644 index 00000000..db2fd274 --- /dev/null +++ b/nls/src/server/config.rs @@ -0,0 +1,154 @@ +//! Configuration keys, typed accessors, and `apply_config` on [`Backend`]. + +use dashmap::DashMap; +use log::debug; +use serde_json::Value; + +use super::Backend; + +// ─── Defaults ─────────────────────────────────────────────────────────────────── + +/// Default debounce delay (ms) before re-analyzing after a `did_change`. +pub const DEBOUNCE_MS: u64 = 50; + +// ─── Configuration keys ───────────────────────────────────────────────────────── + +pub const CFG_INLAY_HINTS_ENABLED: &str = "inlayHints.enabled"; +pub const CFG_INLAY_TYPE_HINTS: &str = "inlayHints.typeHints"; +pub const CFG_INLAY_PARAM_HINTS: &str = "inlayHints.parameterHints"; +pub const CFG_DEBOUNCE_MS: &str = "analysis.debounceMs"; + +// ─── Typed accessors ──────────────────────────────────────────────────────────── + +/// Read a `bool` config value, returning `default` when absent or wrong type. +pub fn cfg_bool(store: &DashMap, key: &str, default: bool) -> bool { + store.get(key).and_then(|v| v.as_bool()).unwrap_or(default) +} + +/// Read a `u64` config value, returning `default` when absent or wrong type. +pub fn cfg_u64(store: &DashMap, key: &str, default: u64) -> u64 { + store.get(key).and_then(|v| v.as_u64()).unwrap_or(default) +} + +/// Read a `String` config value, returning `default` when absent or wrong type. +pub fn cfg_string(store: &DashMap, key: &str, default: &str) -> String { + store + .get(key) + .and_then(|v| v.as_str().map(String::from)) + .unwrap_or_else(|| default.to_owned()) +} + +// ─── Backend impl ─────────────────────────────────────────────────────────────── + +impl Backend { + /// Flatten a (possibly nested) JSON settings object into dot-separated keys. + /// + /// ```json + /// { "inlayHints": { "enabled": true } } + /// ``` + /// becomes `"inlayHints.enabled" → true`. + pub(crate) fn apply_config(&self, settings: &Value) { + if let Some(obj) = settings.as_object() { + for (section, value) in obj { + if let Some(inner) = value.as_object() { + for (key, val) in inner { + let config_key = format!("{section}.{key}"); + debug!("config: {config_key} = {val}"); + self.config.insert(config_key, val.clone()); + } + } else { + debug!("config: {section} = {value}"); + self.config.insert(section.clone(), value.clone()); + } + } + } + } + + /// Configured debounce delay in milliseconds. + pub(crate) fn debounce_delay(&self) -> u64 { + cfg_u64(&self.config, CFG_DEBOUNCE_MS, DEBOUNCE_MS) + } +} + +// ─── Tests ────────────────────────────────────────────────────────────────────── + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn cfg_bool_present() { + let store = DashMap::new(); + store.insert("a.b".into(), Value::Bool(true)); + assert!(cfg_bool(&store, "a.b", false)); + } + + #[test] + fn cfg_bool_missing_returns_default() { + let store: DashMap = DashMap::new(); + assert!(!cfg_bool(&store, "missing", false)); + assert!(cfg_bool(&store, "missing", true)); + } + + #[test] + fn cfg_bool_wrong_type_returns_default() { + let store = DashMap::new(); + store.insert("key".into(), Value::Number(42.into())); + assert!(!cfg_bool(&store, "key", false)); + } + + #[test] + fn cfg_u64_present() { + let store = DashMap::new(); + store.insert("delay".into(), Value::Number(500.into())); + assert_eq!(cfg_u64(&store, "delay", 0), 500); + } + + #[test] + fn cfg_u64_missing() { + let store: DashMap = DashMap::new(); + assert_eq!(cfg_u64(&store, "missing", 300), 300); + } + + #[test] + fn cfg_string_present() { + let store = DashMap::new(); + store.insert("name".into(), Value::String("hello".into())); + assert_eq!(cfg_string(&store, "name", ""), "hello"); + } + + #[test] + fn cfg_string_missing() { + let store: DashMap = DashMap::new(); + assert_eq!(cfg_string(&store, "missing", "fallback"), "fallback"); + } + + #[test] + fn flatten_nested_settings() { + let store = DashMap::new(); + let settings: Value = serde_json::json!({ + "inlayHints": { + "enabled": true, + "typeHints": false + }, + "topLevel": "value" + }); + + // Simulate apply_config logic directly + if let Some(obj) = settings.as_object() { + for (section, value) in obj { + if let Some(inner) = value.as_object() { + for (key, val) in inner { + store.insert(format!("{section}.{key}"), val.clone()); + } + } else { + store.insert(section.clone(), value.clone()); + } + } + } + + assert!(cfg_bool(&store, "inlayHints.enabled", false)); + assert!(!cfg_bool(&store, "inlayHints.typeHints", true)); + assert_eq!(cfg_string(&store, "topLevel", ""), "value"); + } +} diff --git a/nls/src/server/dispatch.rs b/nls/src/server/dispatch.rs new file mode 100644 index 00000000..3b96cd02 --- /dev/null +++ b/nls/src/server/dispatch.rs @@ -0,0 +1,418 @@ +//! Document lifecycle handlers: open, change, save, close, config, watched files. + +use std::sync::atomic::{AtomicU64, Ordering}; +use std::sync::Arc; + +use log::debug; +use tower_lsp::lsp_types::*; + +use crate::analyzer::module_unique_ident; +use crate::package::parse_package; +use crate::project::Project; +use crate::utils::offset_to_position; + +use super::Backend; + +/// Internal helper carrying document info for [`Backend::on_change`]. +pub(crate) struct TextDocumentItem<'a> { + pub uri: Url, + pub text: &'a str, + pub version: Option, +} + +impl Backend { + // ── Lifecycle handlers ────────────────────────────────────────────── + + pub(crate) async fn handle_did_open(&self, params: DidOpenTextDocumentParams) { + debug!("file opened: {}", params.text_document.uri); + + let file_path = params.text_document.uri.path(); + let file_dir = std::path::Path::new(file_path).parent(); + + // Ensure a project exists for this file. + if let Some(dir) = file_dir { + let (project_root, log_msg) = if let Some(pkg_dir) = self.find_package_dir(dir) { + (pkg_dir.to_string_lossy().to_string(), "found project at") + } else { + ( + dir.to_string_lossy().to_string(), + "creating project from file directory:", + ) + }; + + if !self.projects.contains_key(&project_root) { + debug!("{} {}", log_msg, project_root); + let project = Project::new(project_root.clone()).await; + project.start_queue_worker(); + debug!("project created: {}", project_root); + self.projects.insert(project_root, project); + } + } + + // Track the document in our store. + self.documents.open( + ¶ms.text_document.uri, + ¶ms.text_document.text, + params.text_document.version, + params.text_document.language_id.clone(), + ); + + self.on_change(TextDocumentItem { + uri: params.text_document.uri, + text: ¶ms.text_document.text, + version: Some(params.text_document.version), + }) + .await; + + let _ = self.client.semantic_tokens_refresh().await; + } + + pub(crate) async fn handle_did_change(&self, params: DidChangeTextDocumentParams) { + // Apply incremental edits and get the resulting full text. + let full_text = match self.documents.apply_changes( + ¶ms.text_document.uri, + params.text_document.version, + ¶ms.content_changes, + ) { + Some(text) => text, + None => { + debug!( + "did_change for unknown document: {}", + params.text_document.uri + ); + return; + } + }; + + // Debounce: only trigger on_change for the latest version. + let file_path = params.text_document.uri.path().to_string(); + let counter = self + .debounce_versions + .entry(file_path) + .or_insert_with(|| Arc::new(AtomicU64::new(0))) + .clone(); + let my_version = counter.fetch_add(1, Ordering::SeqCst) + 1; + + let delay = self.debounce_delay(); + tokio::time::sleep(std::time::Duration::from_millis(delay)).await; + + if counter.load(Ordering::SeqCst) != my_version { + debug!( + "debounce: skipping stale change (version {} superseded)", + my_version + ); + return; + } + + self.on_change(TextDocumentItem { + text: &full_text, + uri: params.text_document.uri, + version: Some(params.text_document.version), + }) + .await; + + let _ = self.client.semantic_tokens_refresh().await; + } + + pub(crate) async fn handle_did_save(&self, params: DidSaveTextDocumentParams) { + if let Some(text) = params.text { + self.on_change(TextDocumentItem { + uri: params.text_document.uri, + text: &text, + version: None, + }) + .await; + let _ = self.client.semantic_tokens_refresh().await; + } + debug!("file saved"); + } + + pub(crate) async fn handle_did_close(&self, params: DidCloseTextDocumentParams) { + self.documents.close(¶ms.text_document.uri); + debug!("file closed: {}", params.text_document.uri); + } + + pub(crate) async fn handle_did_change_configuration( + &self, + params: DidChangeConfigurationParams, + ) { + debug!("configuration changed: {:?}", params.settings); + + let nature_section = params + .settings + .as_object() + .and_then(|o| o.get("nature")) + .cloned() + .unwrap_or(params.settings); + + self.apply_config(&nature_section); + + // Also pull fresh config from the client. + if let Ok(response) = self + .client + .configuration(vec![ConfigurationItem { + scope_uri: None, + section: Some("nature".into()), + }]) + .await + { + if let Some(settings) = response.into_iter().next() { + self.apply_config(&settings); + } + } + } + + pub(crate) async fn handle_did_change_watched_files( + &self, + params: DidChangeWatchedFilesParams, + ) { + debug!("watched files changed"); + + for change in params.changes { + let file_path = change.uri.path(); + + // .n file created/deleted → re-scan workspace index. + if file_path.ends_with(".n") { + if let Some(project) = self.get_file_project(file_path) { + if let Ok(mut ws_index) = project.workspace_index.lock() { + ws_index.scan_workspace(&project.root, &project.nature_root); + debug!("workspace re-indexed after .n file change: {}", file_path); + } + } + continue; + } + + // package.toml changes → re-parse package config. + if !file_path.ends_with("package.toml") { + continue; + } + + let Some(project) = self.get_file_project(file_path) else { + debug!("no project for watched file: {}", file_path); + continue; + }; + + match parse_package(file_path) { + Ok(pkg) => { + *project.package_config.lock().unwrap() = Some(pkg); + self.client + .publish_diagnostics(change.uri.clone(), vec![], None) + .await; + } + Err(e) => { + if let Ok(content) = std::fs::read_to_string(file_path) { + let rope = ropey::Rope::from_str(&content); + let start = offset_to_position(e.start, &rope) + .unwrap_or(Position::new(0, 0)); + let end = + offset_to_position(e.end, &rope).unwrap_or(Position::new(0, 0)); + let diag = Diagnostic::new_simple( + Range::new(start, end), + format!("package.toml parse error: {}", e.message), + ); + self.client + .publish_diagnostics(change.uri.clone(), vec![diag], None) + .await; + } + } + } + } + } + + // ── Core rebuild trigger ──────────────────────────────────────────── + + /// Called after every meaningful document change. Runs the analysis + /// pipeline and publishes diagnostics. + pub(crate) async fn on_change(&self, params: TextDocumentItem<'_>) { + debug!("on_change: {}", params.uri); + + let file_path = params.uri.path(); + let Some(mut project) = self.get_file_project(file_path) else { + debug!("no project for file: {}, skipping", file_path); + return; + }; + + if file_path.ends_with("package.toml") { + debug!("skipping package.toml in on_change"); + return; + } + + let module_ident = module_unique_ident(&project.root, file_path); + debug!("building module: {}", module_ident); + + let module_index = project + .build(file_path, &module_ident, Some(params.text.to_string())) + .await; + debug!("build complete"); + + // Collect diagnostics for the changed module and its dependents. + let all_diagnostics = { + let module_db = project.module_db.lock().unwrap(); + let mut result: Vec<(String, Vec)> = Vec::new(); + + if let Some(m) = module_db.get(module_index) { + result.push((m.path.clone(), Self::build_diagnostics(m))); + for &ref_idx in &m.references { + if let Some(dep) = module_db.get(ref_idx) { + result.push((dep.path.clone(), Self::build_diagnostics(dep))); + } + } + } + + result + }; + + // Publish diagnostics. + for (path, diagnostics) in all_diagnostics { + if path == file_path { + self.client + .publish_diagnostics(params.uri.clone(), diagnostics, params.version) + .await; + } else if let Ok(uri) = Url::from_file_path(&path) { + self.client + .publish_diagnostics(uri, diagnostics, None) + .await; + } + } + } + + /// Convert a module's analyzer errors into LSP diagnostics. + /// Also detects unused imports and adds HINT-level diagnostics for them. + pub fn build_diagnostics(m: &crate::project::Module) -> Vec { + let mut seen = std::collections::HashSet::new(); + + let mut diagnostics: Vec = m.analyzer_errors + .iter() + .filter(|e| e.end > 0) + .filter(|e| seen.insert((e.start, e.end))) + .filter_map(|e| { + let start = offset_to_position(e.start, &m.rope)?; + let end = offset_to_position(e.end, &m.rope)?; + let severity = if e.is_warning { + DiagnosticSeverity::HINT + } else { + DiagnosticSeverity::ERROR + }; + Some(Diagnostic { + range: Range::new(start, end), + severity: Some(severity), + message: e.message.clone(), + tags: if e.is_warning { + Some(vec![DiagnosticTag::UNNECESSARY]) + } else { + None + }, + ..Default::default() + }) + }) + .collect(); + + // ── Unused import diagnostics ─────────────────────────────────── + for dep in &m.dependencies { + if dep.as_name == "*" { + continue; + } + if dep.as_name.is_empty() && !dep.is_selective { + continue; + } + if dep.start == 0 && dep.end == 0 { + continue; + } + + let is_used = if dep.is_selective { + if let Some(items) = &dep.select_items { + items.iter().any(|item| { + let name = item.alias.as_deref().unwrap_or(&item.ident); + is_identifier_used_in_source(&m.source, name, dep.start, dep.end) + }) + } else { + true + } + } else { + is_identifier_used_in_source(&m.source, &dep.as_name, dep.start, dep.end) + }; + + if is_used { + continue; + } + + // Build a human-readable label for the import. + let import_label = if dep.is_selective { + if let Some(items) = &dep.select_items { + let names: Vec<&str> = items + .iter() + .map(|i| i.alias.as_deref().unwrap_or(i.ident.as_str())) + .collect(); + format!("{}.{{{}}}", dep.module_ident, names.join(", ")) + } else { + dep.module_ident.clone() + } + } else { + dep.as_name.clone() + }; + + let start = match offset_to_position(dep.start, &m.rope) { + Some(p) => p, + None => continue, + }; + let end = match offset_to_position(dep.end, &m.rope) { + Some(p) => p, + None => continue, + }; + + diagnostics.push(Diagnostic { + range: Range::new(start, end), + severity: Some(DiagnosticSeverity::HINT), + message: format!("'{}' is imported but never used", import_label), + tags: Some(vec![DiagnosticTag::UNNECESSARY]), + source: Some("nls".into()), + ..Default::default() + }); + } + + diagnostics + } +} + +/// Check whether `name` appears as an identifier in `source`, ignoring the +/// region between `skip_start..skip_end` (the import statement itself). +fn is_identifier_used_in_source(source: &str, name: &str, skip_start: usize, skip_end: usize) -> bool { + if name.is_empty() { + return true; + } + + let bytes = source.as_bytes(); + let name_len = name.len(); + + let mut pos = 0; + while pos + name_len <= bytes.len() { + if let Some(found) = source[pos..].find(name) { + let abs_pos = pos + found; + + // Skip if inside the import statement range + if abs_pos >= skip_start && abs_pos < skip_end { + pos = abs_pos + name_len; + continue; + } + + // Check word boundaries + let before_ok = abs_pos == 0 || !is_ident_char(bytes[abs_pos - 1]); + let after_ok = abs_pos + name_len >= bytes.len() + || !is_ident_char(bytes[abs_pos + name_len]); + + if before_ok && after_ok { + return true; + } + + pos = abs_pos + name_len; + } else { + break; + } + } + + false +} + +fn is_ident_char(b: u8) -> bool { + b.is_ascii_alphanumeric() || b == b'_' +} diff --git a/nls/src/server/hover.rs b/nls/src/server/hover.rs new file mode 100644 index 00000000..0b3505c1 --- /dev/null +++ b/nls/src/server/hover.rs @@ -0,0 +1,491 @@ +//! Hover request handler: show type signatures and documentation on hover. + +use tower_lsp::lsp_types::*; + +use crate::analyzer::common::{AstFnDef, SelfKind, TypeKind}; +use crate::analyzer::lexer::{Token, TokenType}; +use crate::analyzer::symbol::SymbolKind; +use crate::project::Project; + +use super::navigation::{resolve_cursor, resolve_symbol}; +use super::Backend; + +// ─── Handler wiring ───────────────────────────────────────────────────────────── + +impl Backend { + pub(crate) async fn handle_hover( + &self, + params: HoverParams, + ) -> Option { + let uri = ¶ms.text_document_position_params.text_document.uri; + let position = params.text_document_position_params.position; + let file_path = uri.path(); + + let project = self.get_file_project(file_path)?; + build_hover(&project, file_path, position) + } +} + +// ─── Hover construction ───────────────────────────────────────────────────────── + +/// Build a `Hover` response for the symbol under the cursor. +fn build_hover(project: &Project, file_path: &str, position: Position) -> Option { + let cursor = resolve_cursor(project, file_path, position)?; + let symbol = resolve_symbol(project, &cursor)?; + + // Extract doc comment only for globally declared symbols. + // Global symbols have a module prefix ("module.ident") in their ident; + // local variables/constants inside function bodies do not. + let is_global = match &symbol.kind { + SymbolKind::Fn(_) | SymbolKind::Type(_) => true, + SymbolKind::Var(_) | SymbolKind::Const(_) => symbol.ident.contains('.'), + }; + let doc_comment = if is_global { + if let Some(module_path) = symbol.module_path(project) { + extract_doc_comment_for_symbol(project, &module_path, &symbol.kind) + } else { + None + } + } else { + None + }; + + let content = format_hover_content(&symbol.kind, &cursor.word, doc_comment.as_deref()); + + Some(Hover { + contents: HoverContents::Markup(MarkupContent { + kind: MarkupKind::Markdown, + value: content, + }), + range: None, + }) +} + +// ─── Formatting ───────────────────────────────────────────────────────────────── + +/// Produce markdown hover text for a resolved symbol. +fn format_hover_content(kind: &SymbolKind, word: &str, doc: Option<&str>) -> String { + let signature = match kind { + SymbolKind::Fn(fndef_mutex) => { + let fndef = fndef_mutex.lock().unwrap(); + format_fn_hover(&fndef) + } + SymbolKind::Var(var_mutex) => { + let var = var_mutex.lock().unwrap(); + format!("```n\nvar {}: {}\n```", display_name(&var.ident, word), type_display_name(&var.type_)) + } + SymbolKind::Const(const_mutex) => { + let c = const_mutex.lock().unwrap(); + format!("```n\nconst {}: {}\n```", display_name(&c.ident, word), type_display_name(&c.type_)) + } + SymbolKind::Type(typedef_mutex) => { + let typedef = typedef_mutex.lock().unwrap(); + format_type_hover(&typedef, word) + } + }; + + match doc { + Some(d) if !d.is_empty() => format!("{}\n\n---\n\n{}", signature, d), + _ => signature, + } +} + +/// Format a function definition for hover display. +fn format_fn_hover(fndef: &AstFnDef) -> String { + let name = if fndef.fn_name.is_empty() { + &fndef.symbol_name + } else { + &fndef.fn_name + }; + + let params = format_params(fndef); + let ret = type_display_name(&fndef.return_type); + + let mut sig = format!("fn {}({}): {}", name, params, ret); + + if fndef.is_errable { + sig.push('!'); + } + + format!("```n\n{}\n```", sig) +} + +/// Format function parameters as a comma-separated string. +fn format_params(fndef: &AstFnDef) -> String { + let mut parts: Vec = Vec::new(); + + for param in &fndef.params { + let p = param.lock().unwrap(); + + if p.ident == "self" { + let self_str = match fndef.self_kind { + SelfKind::SelfRefT => "&self", + SelfKind::SelfPtrT => "*self", + _ => "self", + }; + parts.push(self_str.to_string()); + continue; + } + + let type_str = type_display_name(&p.type_); + parts.push(format!("{}: {}", p.ident, type_str)); + } + + if fndef.rest_param { + parts.push("...".to_string()); + } + + parts.join(", ") +} + +/// Format a type definition for hover display. +fn format_type_hover( + typedef: &crate::analyzer::common::TypedefStmt, + word: &str, +) -> String { + let name = display_name(&typedef.ident, word); + let keyword = if typedef.is_interface { + "interface" + } else if typedef.is_enum { + "enum" + } else if typedef.is_tagged_union { + "union" + } else { + "type" + }; + + let generics = if typedef.params.is_empty() { + String::new() + } else { + let gs: Vec<&str> = typedef.params.iter().map(|p| p.ident.as_str()).collect(); + format!("<{}>", gs.join(", ")) + }; + + let body = format_type_body(&typedef.type_expr.kind); + + format!("```n\n{} {}{} = {}\n```", keyword, name, generics, body) +} + +/// Produce a concise representation of a type body for hover. +fn format_type_body(kind: &TypeKind) -> String { + match kind { + TypeKind::Struct(_, _, props) => { + if props.is_empty() { + return "{}".to_string(); + } + let fields: Vec = props + .iter() + .take(8) + .map(|p| format!(" {}: {}", p.name, type_display_name(&p.type_))) + .collect(); + let ellipsis = if props.len() > 8 { "\n ..." } else { "" }; + format!("{{\n{}{}\n}}", fields.join("\n"), ellipsis) + } + TypeKind::Enum(_, variants) => { + let vs: Vec<&str> = variants.iter().take(12).map(|v| v.name.as_str()).collect(); + let ellipsis = if variants.len() > 12 { ", ..." } else { "" }; + format!("{{{}{}}}", vs.join(", "), ellipsis) + } + TypeKind::TaggedUnion(_, elements) => { + let vs: Vec = elements.iter().take(8).map(|e| e.tag.clone()).collect(); + let ellipsis = if elements.len() > 8 { " | ..." } else { "" }; + vs.join(" | ") + ellipsis + } + other => other.to_string(), + } +} + +/// Return a user-friendly type display name, preferring `ident` over the kind string. +/// Strips module-qualified prefixes (e.g. "forest example main.MyFn" → "MyFn"). +fn type_display_name(t: &crate::analyzer::common::Type) -> String { + use crate::analyzer::common::TypeKind; + + // For Ref/Ptr types, display as ref/ptr using the inner type's name. + match &t.kind { + TypeKind::Ref(inner) => { + return format!("ref<{}>", type_display_name(inner)); + } + TypeKind::Ptr(inner) => { + return format!("ptr<{}>", type_display_name(inner)); + } + _ => {} + } + + if !t.ident.is_empty() { + // Strip module prefix: take everything after the last '.' + return t.ident.rsplit('.').next().unwrap_or(&t.ident).to_string(); + } + t.to_string() +} + +/// Strip module prefix from an ident, falling back to `word` if needed. +fn display_name<'a>(qualified: &'a str, word: &'a str) -> &'a str { + qualified.rsplit('.').next().unwrap_or(word) +} + +// ─── Doc comment extraction ───────────────────────────────────────────────────── + +/// Look up the defining module for a symbol and extract any doc comment above it. +fn extract_doc_comment_for_symbol( + project: &Project, + module_path: &str, + kind: &SymbolKind, +) -> Option { + let symbol_pos = symbol_def_start(kind); + + let db = project.module_db.lock().ok()?; + let module = db.iter().find(|m| m.path == module_path)?; + + extract_doc_comment(&module.token_db, symbol_pos) +} + +/// Get the `symbol_start` char offset from a `SymbolKind`. +fn symbol_def_start(kind: &SymbolKind) -> usize { + match kind { + SymbolKind::Fn(f) => f.lock().unwrap().symbol_start, + SymbolKind::Var(v) => v.lock().unwrap().symbol_start, + SymbolKind::Const(c) => c.lock().unwrap().symbol_start, + SymbolKind::Type(t) => t.lock().unwrap().symbol_start, + } +} + +/// Scan `token_db` backwards from `symbol_pos` to collect contiguous doc-comment +/// lines immediately preceding the definition. +fn extract_doc_comment(token_db: &[Token], symbol_pos: usize) -> Option { + // Find the token at or just before the symbol position. + let token_idx = token_db.iter().rposition(|t| t.start <= symbol_pos)?; + let def_line = token_db[token_idx].line; + + let mut comments: Vec = Vec::new(); + let mut expected_line = def_line; // first comment must be on expected_line - 1 + + for i in (0..=token_idx).rev() { + let token = &token_db[i]; + + // Skip tokens on the definition line itself (keywords, ident, etc.). + if token.line >= expected_line && !comments.is_empty() { + continue; + } + if token.line == def_line { + continue; + } + + match token.token_type { + TokenType::LineComment | TokenType::BlockComment => { + if token.line == expected_line - 1 { + expected_line = token.line; + comments.push(token.literal.clone()); + } else { + break; + } + } + _ => break, + } + } + + if comments.is_empty() { + return None; + } + + // Collected bottom-up, reverse to top-down order. + comments.reverse(); + + let cleaned: Vec = comments + .iter() + .map(|c| strip_comment_marker(c)) + .collect(); + + Some(cleaned.join("\n")) +} + +/// Remove `//`, `/* */`, or leading `*` markers from a comment token literal. +fn strip_comment_marker(comment: &str) -> String { + let trimmed = comment.trim(); + if let Some(rest) = trimmed.strip_prefix("//") { + // Line comment: strip `//` and optional leading space. + rest.strip_prefix(' ').unwrap_or(rest).to_string() + } else if let Some(rest) = trimmed.strip_prefix("/*") { + // First line of a block comment. + let rest = rest.strip_suffix("*/").unwrap_or(rest); + rest.trim().to_string() + } else if let Some(rest) = trimmed.strip_suffix("*/") { + // Last line of a block comment. + let rest = rest.trim(); + let rest = rest.strip_prefix('*').unwrap_or(rest); + rest.trim_start().to_string() + } else { + // Middle line of a block comment — strip optional leading `*`. + let rest = trimmed.strip_prefix('*').unwrap_or(trimmed); + rest.strip_prefix(' ').unwrap_or(rest).to_string() + } +} + +// ─── Tests ────────────────────────────────────────────────────────────────────── + +#[cfg(test)] +mod tests { + use super::*; + use crate::analyzer::common::{AstFnDef, Type, VarDeclExpr}; + use std::sync::{Arc, Mutex}; + + #[test] + fn hover_var() { + let var = Arc::new(Mutex::new(VarDeclExpr { + ident: "mymod.count".into(), + symbol_id: 0, + symbol_start: 0, + symbol_end: 5, + type_: Type::new(TypeKind::Int64), + be_capture: false, + heap_ident: None, + })); + let content = format_hover_content(&SymbolKind::Var(var), "count", None); + assert!(content.contains("var count: i64"), "got: {}", content); + } + + #[test] + fn hover_fn_simple() { + let fndef = Arc::new(Mutex::new(AstFnDef { + fn_name: "add".into(), + return_type: Type::new(TypeKind::Int64), + ..AstFnDef::default() + })); + let content = format_hover_content(&SymbolKind::Fn(fndef), "add", None); + assert!(content.contains("fn add(): i64"), "got: {}", content); + } + + #[test] + fn hover_fn_with_params() { + let param_a = Arc::new(Mutex::new(VarDeclExpr { + ident: "a".into(), + symbol_id: 0, + symbol_start: 0, + symbol_end: 0, + type_: Type::new(TypeKind::Int64), + be_capture: false, + heap_ident: None, + })); + let param_b = Arc::new(Mutex::new(VarDeclExpr { + ident: "b".into(), + symbol_id: 0, + symbol_start: 0, + symbol_end: 0, + type_: Type::new(TypeKind::String), + be_capture: false, + heap_ident: None, + })); + let fndef = Arc::new(Mutex::new(AstFnDef { + fn_name: "greet".into(), + params: vec![param_a, param_b], + return_type: Type::new(TypeKind::Void), + ..AstFnDef::default() + })); + let content = format_hover_content(&SymbolKind::Fn(fndef), "greet", None); + assert!(content.contains("a: i64, b: string"), "got: {}", content); + } + + #[test] + fn display_name_strips_prefix() { + assert_eq!(display_name("mymod.hello", "hello"), "hello"); + assert_eq!(display_name("hello", "hello"), "hello"); + } + + #[test] + fn hover_with_doc_comment() { + let var = Arc::new(Mutex::new(VarDeclExpr { + ident: "mymod.count".into(), + symbol_id: 0, + symbol_start: 0, + symbol_end: 5, + type_: Type::new(TypeKind::Int64), + be_capture: false, + heap_ident: None, + })); + let content = format_hover_content( + &SymbolKind::Var(var), + "count", + Some("The number of items"), + ); + assert!(content.contains("var count: i64"), "got: {}", content); + assert!(content.contains("---"), "should have separator: {}", content); + assert!(content.contains("The number of items"), "should have doc: {}", content); + } + + #[test] + fn extract_line_comments() { + let tokens = vec![ + Token::new(TokenType::LineComment, "// hello world".into(), 0, 14, 1), + Token::new(TokenType::LineComment, "// second line".into(), 15, 29, 2), + Token::new(TokenType::Ident, "fn".into(), 30, 32, 3), + Token::new(TokenType::Ident, "foo".into(), 33, 36, 3), + ]; + let doc = extract_doc_comment(&tokens, 30); + assert_eq!(doc.as_deref(), Some("hello world\nsecond line")); + } + + #[test] + fn extract_no_comment_when_gap() { + let tokens = vec![ + Token::new(TokenType::LineComment, "// orphan".into(), 0, 9, 1), + // line 2 is blank (no tokens) + Token::new(TokenType::Ident, "fn".into(), 20, 22, 3), + Token::new(TokenType::Ident, "foo".into(), 23, 26, 3), + ]; + let doc = extract_doc_comment(&tokens, 20); + assert_eq!(doc, None); + } + + #[test] + fn strip_line_comment_markers() { + assert_eq!(strip_comment_marker("// hello"), "hello"); + assert_eq!(strip_comment_marker("//hello"), "hello"); + assert_eq!(strip_comment_marker("/* single */"), "single"); + assert_eq!(strip_comment_marker(" * middle"), "middle"); + } + + #[test] + fn type_display_simple_ident() { + let t = crate::analyzer::common::Type { + ident: "forest.example.main.MyStruct".into(), + ..Default::default() + }; + assert_eq!(super::type_display_name(&t), "MyStruct"); + } + + #[test] + fn type_display_ref_wrapping() { + use crate::analyzer::common::{Type, TypeKind}; + let inner = Type { + ident: "forest.app.configuration.app".into(), + kind: TypeKind::Ident, + ..Default::default() + }; + let t = Type { + kind: TypeKind::Ref(Box::new(inner)), + ..Default::default() + }; + assert_eq!(super::type_display_name(&t), "ref"); + } + + #[test] + fn type_display_ptr_wrapping() { + use crate::analyzer::common::{Type, TypeKind}; + let inner = Type { + ident: "module.MyType".into(), + kind: TypeKind::Ident, + ..Default::default() + }; + let t = Type { + kind: TypeKind::Ptr(Box::new(inner)), + ..Default::default() + }; + assert_eq!(super::type_display_name(&t), "ptr"); + } + + #[test] + fn type_display_plain_builtin() { + let t = crate::analyzer::common::Type::new(crate::analyzer::common::TypeKind::Int); + // Int maps to "i64" internally + assert_eq!(super::type_display_name(&t), "i64"); + } +} diff --git a/nls/src/server/inlay_hints.rs b/nls/src/server/inlay_hints.rs new file mode 100644 index 00000000..f4d1f18d --- /dev/null +++ b/nls/src/server/inlay_hints.rs @@ -0,0 +1,432 @@ +//! Inlay hints: type annotations for variable declarations and parameter +//! labels at call sites. + +use tower_lsp::lsp_types::*; + +use crate::analyzer::common::{AstCall, AstNode, Expr, Stmt, TypeKind}; +use crate::analyzer::symbol::SymbolKind; +use crate::project::{Module, Project}; +use crate::server::config::{cfg_bool, CFG_INLAY_TYPE_HINTS, CFG_INLAY_PARAM_HINTS}; +use crate::utils::{offset_to_position, position_to_char_offset}; + +use super::Backend; + +// ─── Handler wiring ───────────────────────────────────────────────────────────── + +impl Backend { + pub(crate) async fn handle_inlay_hint( + &self, + params: InlayHintParams, + ) -> Option> { + let type_hints = cfg_bool(&self.config, CFG_INLAY_TYPE_HINTS, false); + let param_hints = cfg_bool(&self.config, CFG_INLAY_PARAM_HINTS, false); + + // Nothing to do if both kinds are disabled. + if !type_hints && !param_hints { + return Some(vec![]); + } + + let file_path = params.text_document.uri.path(); + let project = self.get_file_project(file_path)?; + build_inlay_hints(&project, file_path, params.range, type_hints, param_hints) + } +} + +// ─── Hint collection ──────────────────────────────────────────────────────────── + +/// Build inlay hints for the visible `range` in the given file. +fn build_inlay_hints( + project: &Project, + file_path: &str, + range: Range, + type_hints: bool, + param_hints: bool, +) -> Option> { + let mh = project.module_handled.lock().ok()?; + let &idx = mh.get(file_path)?; + drop(mh); + + let db = project.module_db.lock().ok()?; + let module = db.get(idx)?; + + let range_start = position_to_char_offset(range.start, &module.rope)?; + let range_end = position_to_char_offset(range.end, &module.rope)?; + + let mut hints = Vec::new(); + + let opts = HintOpts { type_hints, param_hints }; + + // Walk top-level statements. + collect_hints_from_stmts(&module.stmts, module, project, range_start, range_end, &opts, &mut hints); + + // Walk all function bodies. + for fndef_mutex in &module.all_fndefs { + let fndef = fndef_mutex.lock().unwrap(); + collect_hints_from_stmts( + &fndef.body.stmts, + module, + project, + range_start, + range_end, + &opts, + &mut hints, + ); + } + + Some(hints) +} + +/// Which hint kinds are enabled. +struct HintOpts { + type_hints: bool, + param_hints: bool, +} + +/// Collect inlay hints from a list of statements. +fn collect_hints_from_stmts( + stmts: &[Box], + module: &Module, + project: &Project, + range_start: usize, + range_end: usize, + opts: &HintOpts, + hints: &mut Vec, +) { + for stmt in stmts { + // Quick range check: skip statements entirely outside the visible range. + if stmt.end < range_start || stmt.start > range_end { + continue; + } + + match &stmt.node { + // ── Type hints for variable declarations ───────────────── + AstNode::VarDef(var_mutex, _right) if opts.type_hints => { + let var = var_mutex.lock().unwrap(); + if let Some(hint) = make_type_hint(&var.type_, var.symbol_end, &module.rope) { + hints.push(hint); + } + } + + // ── Recursively check if/for/etc. bodies ───────────────── + AstNode::If(_, consequent, alternate) => { + collect_hints_from_stmts(&consequent.stmts, module, project, range_start, range_end, opts, hints); + collect_hints_from_stmts(&alternate.stmts, module, project, range_start, range_end, opts, hints); + } + AstNode::ForIterator(_, _, _, body) => { + collect_hints_from_stmts(&body.stmts, module, project, range_start, range_end, opts, hints); + } + AstNode::ForCond(_, body) => { + collect_hints_from_stmts(&body.stmts, module, project, range_start, range_end, opts, hints); + } + _ => {} + } + + // ── Parameter hints at call sites ──────────────────────────── + if opts.param_hints { + collect_call_hints_from_expr_in_node(&stmt.node, module, project, range_start, range_end, hints); + } + } +} + +/// Walk an AST node looking for `Call` expressions to generate parameter hints. +fn collect_call_hints_from_expr_in_node( + node: &AstNode, + module: &Module, + project: &Project, + range_start: usize, + range_end: usize, + hints: &mut Vec, +) { + match node { + AstNode::Call(call) => { + collect_param_hints(call, module, project, hints); + // Recurse into arguments (they may contain nested calls). + for arg in &call.args { + collect_call_hints_from_expr(arg, module, project, range_start, range_end, hints); + } + // Recurse into the callee expression. + collect_call_hints_from_expr(&call.left, module, project, range_start, range_end, hints); + } + AstNode::VarDef(_, right) => { + collect_call_hints_from_expr(right, module, project, range_start, range_end, hints); + } + AstNode::Assign(left, right) => { + collect_call_hints_from_expr(left, module, project, range_start, range_end, hints); + collect_call_hints_from_expr(right, module, project, range_start, range_end, hints); + } + AstNode::Return(Some(expr)) => { + collect_call_hints_from_expr(expr, module, project, range_start, range_end, hints); + } + AstNode::Ret(expr) => { + collect_call_hints_from_expr(expr, module, project, range_start, range_end, hints); + } + AstNode::Fake(expr) => { + collect_call_hints_from_expr(expr, module, project, range_start, range_end, hints); + } + _ => {} + } +} + +/// Walk an expression tree looking for `Call` nodes. +fn collect_call_hints_from_expr( + expr: &Expr, + module: &Module, + project: &Project, + range_start: usize, + range_end: usize, + hints: &mut Vec, +) { + if expr.end < range_start || expr.start > range_end { + return; + } + collect_call_hints_from_expr_in_node(&expr.node, module, project, range_start, range_end, hints); +} + +// ─── Type hints ───────────────────────────────────────────────────────────────── + +/// Create a type-annotation inlay hint placed after the variable name +/// (right after `symbol_end`). +fn make_type_hint( + type_: &crate::analyzer::common::Type, + symbol_end: usize, + rope: &ropey::Rope, +) -> Option { + // Don't show hints for unknown / void types. + if matches!(type_.kind, TypeKind::Unknown | TypeKind::Void) { + return None; + } + + let position = offset_to_position(symbol_end, rope)?; + let label = format!(": {}", type_display(type_)); + + Some(InlayHint { + position, + label: InlayHintLabel::String(label), + kind: Some(InlayHintKind::TYPE), + text_edits: None, + tooltip: None, + padding_left: None, + padding_right: None, + data: None, + }) +} + +/// Prefer the type's `ident` for display (e.g., `MyStruct` instead of `struct {...}`). +fn type_display(t: &crate::analyzer::common::Type) -> String { + if !t.ident.is_empty() { + t.ident.clone() + } else { + t.to_string() + } +} + +// ─── Parameter hints ──────────────────────────────────────────────────────────── + +/// Generate parameter-name hints for a function call's arguments. +fn collect_param_hints( + call: &AstCall, + module: &Module, + project: &Project, + hints: &mut Vec, +) { + // Resolve the callee to its function definition to get param names. + let param_names = resolve_call_param_names(call, project); + let param_names = match param_names { + Some(names) if !names.is_empty() => names, + _ => return, + }; + + for (i, arg) in call.args.iter().enumerate() { + let Some(name) = param_names.get(i) else { + break; + }; + + // Skip if the argument already looks like the parameter name + // (e.g., passing `name` to a param called `name`). + if arg_matches_param(arg, name) { + continue; + } + + let Some(position) = offset_to_position(arg.start, &module.rope) else { + continue; + }; + + hints.push(InlayHint { + position, + label: InlayHintLabel::String(format!("{}:", name)), + kind: Some(InlayHintKind::PARAMETER), + text_edits: None, + tooltip: None, + padding_left: None, + padding_right: Some(true), + data: None, + }); + } +} + +/// Extract parameter names from the function being called. +/// +/// Resolution strategy: +/// 1. If `left` is an `Ident(_, symbol_id)`, look up the symbol directly. +/// 2. If the left expression's type is `Fn(TypeFn)`, we only have types, not +/// names — return `None` (no parameter hints for indirect calls). +fn resolve_call_param_names(call: &AstCall, project: &Project) -> Option> { + // Direct call via ident: `foo(a, b)` + if let AstNode::Ident(_, symbol_id) = &call.left.node { + return param_names_from_symbol(*symbol_id, project); + } + + // Method call via select: `obj.method(a, b)` — StructSelect carries the + // property type which may be a function. Fall back to the left expr type. + if let AstNode::StructSelect(_, _, prop) = &call.left.node { + if let TypeKind::Fn(ref fn_type) = prop.type_.kind { + // TypeFn only has types, not names. Try to find the fndef via name. + return param_names_from_fn_name(&fn_type.name, project); + } + } + + None +} + +/// Look up a symbol by id and extract param names if it's a function. +fn param_names_from_symbol(symbol_id: usize, project: &Project) -> Option> { + let st = project.symbol_table.lock().ok()?; + let symbol = st.get_symbol_ref(symbol_id)?; + + match &symbol.kind { + SymbolKind::Fn(fndef_mutex) => { + let fndef = fndef_mutex.lock().unwrap(); + Some(extract_param_names(&fndef)) + } + _ => None, + } +} + +/// Try to find a function by name in all modules' global fndefs. +fn param_names_from_fn_name(name: &str, project: &Project) -> Option> { + if name.is_empty() { + return None; + } + let db = project.module_db.lock().ok()?; + for module in db.iter() { + for fndef_mutex in &module.all_fndefs { + let fndef = fndef_mutex.lock().unwrap(); + if fndef.fn_name == name || fndef.symbol_name == name { + return Some(extract_param_names(&fndef)); + } + } + } + None +} + +/// Extract non-self parameter names from a function definition. +fn extract_param_names(fndef: &crate::analyzer::common::AstFnDef) -> Vec { + fndef + .params + .iter() + .filter_map(|p| { + let p = p.lock().unwrap(); + if p.ident == "self" { + None + } else { + Some(p.ident.clone()) + } + }) + .collect() +} + +/// Check whether an argument expression is just an ident matching the param name. +fn arg_matches_param(arg: &Expr, param_name: &str) -> bool { + if let AstNode::Ident(name, _) = &arg.node { + name == param_name + } else { + false + } +} + +// ─── Tests ────────────────────────────────────────────────────────────────────── + +#[cfg(test)] +mod tests { + use super::*; + use crate::analyzer::common::{Type, VarDeclExpr}; + use ropey::Rope; + + #[test] + fn type_hint_for_i64() { + let t = Type::new(TypeKind::Int64); + let rope = Rope::from_str("var x = 42"); + // symbol_end = 5 (after "var x") + let hint = make_type_hint(&t, 5, &rope).expect("should produce hint"); + match &hint.label { + InlayHintLabel::String(s) => assert!(s.contains("i64"), "got: {}", s), + _ => panic!("expected string label"), + } + assert_eq!(hint.kind, Some(InlayHintKind::TYPE)); + } + + #[test] + fn no_type_hint_for_unknown() { + let t = Type::new(TypeKind::Unknown); + let rope = Rope::from_str("var x = ???"); + assert!(make_type_hint(&t, 5, &rope).is_none()); + } + + #[test] + fn no_type_hint_for_void() { + let t = Type::new(TypeKind::Void); + let rope = Rope::from_str("var x = noop()"); + assert!(make_type_hint(&t, 5, &rope).is_none()); + } + + #[test] + fn type_display_prefers_ident() { + let mut t = Type::new(TypeKind::Struct("MyStruct".into(), 0, vec![])); + t.ident = "MyStruct".into(); + assert_eq!(type_display(&t), "MyStruct"); + } + + #[test] + fn arg_matches_param_ident() { + let expr = Expr { + start: 0, + end: 3, + type_: Type::default(), + target_type: Type::default(), + node: AstNode::Ident("foo".into(), 0), + }; + assert!(arg_matches_param(&expr, "foo")); + assert!(!arg_matches_param(&expr, "bar")); + } + + #[test] + fn extract_param_names_skips_self() { + use crate::analyzer::common::AstFnDef; + use std::sync::{Arc, Mutex}; + + let self_param = Arc::new(Mutex::new(VarDeclExpr { + ident: "self".into(), + symbol_id: 0, + symbol_start: 0, + symbol_end: 0, + type_: Type::default(), + be_capture: false, + heap_ident: None, + })); + let x_param = Arc::new(Mutex::new(VarDeclExpr { + ident: "x".into(), + symbol_id: 0, + symbol_start: 0, + symbol_end: 0, + type_: Type::new(TypeKind::Int64), + be_capture: false, + heap_ident: None, + })); + let fndef = AstFnDef { + params: vec![self_param, x_param], + ..AstFnDef::default() + }; + let names = extract_param_names(&fndef); + assert_eq!(names, vec!["x"]); + } +} diff --git a/nls/src/server/mod.rs b/nls/src/server/mod.rs new file mode 100644 index 00000000..57c37b0e --- /dev/null +++ b/nls/src/server/mod.rs @@ -0,0 +1,224 @@ +//! LSP backend: shared state and `LanguageServer` trait implementation. +//! +//! Actual handler logic lives in focused submodules; this file only wires the +//! trait methods to those handlers and holds the shared [`Backend`] struct. + +pub mod capabilities; +pub mod code_actions; +pub mod completion; +pub mod config; +pub mod dispatch; +pub mod hover; +pub mod inlay_hints; +pub mod navigation; +pub mod semantic_tokens; +pub mod signature_help; +pub mod symbols; + +use std::sync::atomic::AtomicU64; +use std::sync::Arc; + +use dashmap::DashMap; +use serde_json::Value; +use tower_lsp::jsonrpc::Result; +use tower_lsp::lsp_types::*; +use tower_lsp::{Client, LanguageServer}; + +use crate::document::DocumentStore; +use crate::project::Project; + +/// The central LSP backend shared across all handlers. +#[derive(Debug)] +pub struct Backend { + /// LSP client handle for sending notifications (diagnostics, logs, etc.). + pub client: Client, + /// Open documents with incremental sync. + pub documents: DocumentStore, + /// Active projects keyed by root path. + pub projects: DashMap, + /// Per-file monotonic counter for debouncing `did_change`. + pub debounce_versions: DashMap>, + /// User / workspace configuration settings (flat dot-separated keys). + pub config: DashMap, +} + +impl Backend { + /// Find the project whose root best matches `file_path` (longest prefix). + pub(crate) fn get_file_project(&self, file_path: &str) -> Option { + let mut best: Option<(usize, Project)> = None; + for entry in self.projects.iter() { + let root = entry.key(); + if file_path.starts_with(root.as_str()) { + let len = root.len(); + if best.as_ref().map_or(true, |(best_len, _)| len > *best_len) { + best = Some((len, entry.value().clone())); + } + } + } + best.map(|(_, p)| p) + } + + /// Walk up from `start_dir` looking for `package.toml`. + pub(crate) fn find_package_dir(&self, start_dir: &std::path::Path) -> Option { + let mut current = Some(start_dir.to_path_buf()); + while let Some(dir) = current { + if dir.join("package.toml").exists() { + return Some(dir); + } + current = dir.parent().map(|p| p.to_path_buf()); + } + None + } +} + +// ─── LanguageServer trait ─────────────────────────────────────────────────────── + +#[tower_lsp::async_trait] +impl LanguageServer for Backend { + async fn initialize(&self, params: InitializeParams) -> Result { + let capabilities = self.handle_initialize(params).await; + Ok(InitializeResult { + server_info: Some(ServerInfo { + name: "nls".into(), + version: Some("0.1.0".into()), + }), + capabilities, + ..Default::default() + }) + } + + async fn initialized(&self, _: InitializedParams) { + self.handle_initialized().await; + } + + async fn shutdown(&self) -> Result<()> { + Ok(()) + } + + // ── Document sync ─────────────────────────────────────────────────── + + async fn did_open(&self, params: DidOpenTextDocumentParams) { + self.handle_did_open(params).await; + } + + async fn did_change(&self, params: DidChangeTextDocumentParams) { + self.handle_did_change(params).await; + } + + async fn did_save(&self, params: DidSaveTextDocumentParams) { + self.handle_did_save(params).await; + } + + async fn did_close(&self, params: DidCloseTextDocumentParams) { + self.handle_did_close(params).await; + } + + async fn did_change_configuration(&self, params: DidChangeConfigurationParams) { + self.handle_did_change_configuration(params).await; + } + + async fn did_change_watched_files(&self, params: DidChangeWatchedFilesParams) { + self.handle_did_change_watched_files(params).await; + } + + // ── Navigation ────────────────────────────────────────────────── + + async fn goto_definition( + &self, + params: GotoDefinitionParams, + ) -> Result> { + Ok(self.handle_goto_definition(params).await) + } + + async fn references( + &self, + params: ReferenceParams, + ) -> Result>> { + Ok(self.handle_references(params).await) + } + + async fn goto_type_definition( + &self, + params: GotoDefinitionParams, + ) -> Result> { + Ok(self.handle_goto_type_definition(params).await) + } + + async fn goto_implementation( + &self, + params: GotoDefinitionParams, + ) -> Result> { + Ok(self.handle_goto_implementation(params).await) + } + + async fn document_symbol( + &self, + params: DocumentSymbolParams, + ) -> Result> { + Ok(self.handle_document_symbol(params).await) + } + + async fn symbol( + &self, + params: WorkspaceSymbolParams, + ) -> Result>> { + Ok(self.handle_workspace_symbol(params).await) + } + + // ── Inline information (Tier 4) ────────────────────────────────── + + async fn hover( + &self, + params: HoverParams, + ) -> Result> { + Ok(self.handle_hover(params).await) + } + + async fn inlay_hint( + &self, + params: InlayHintParams, + ) -> Result>> { + Ok(self.handle_inlay_hint(params).await) + } + + async fn signature_help( + &self, + params: SignatureHelpParams, + ) -> Result> { + Ok(self.handle_signature_help(params).await) + } + + // ── Completions (Tier 5) ───────────────────────────────────────── + + async fn completion( + &self, + params: CompletionParams, + ) -> Result> { + Ok(self.handle_completion(params).await) + } + + // ── Code actions (Tier 6) ──────────────────────────────────────── + + async fn code_action( + &self, + params: CodeActionParams, + ) -> Result> { + Ok(self.handle_code_action(params).await) + } + + // ── Semantic tokens ───────────────────────────────────────────── + + async fn semantic_tokens_full( + &self, + params: SemanticTokensParams, + ) -> Result> { + Ok(self.handle_semantic_tokens_full(params).await) + } + + async fn semantic_tokens_range( + &self, + params: SemanticTokensRangeParams, + ) -> Result> { + Ok(self.handle_semantic_tokens_range(params).await) + } +} diff --git a/nls/src/server/navigation.rs b/nls/src/server/navigation.rs new file mode 100644 index 00000000..eea8d13f --- /dev/null +++ b/nls/src/server/navigation.rs @@ -0,0 +1,990 @@ +//! Navigation request handlers: go-to-definition, find-all-references, +//! go-to-type-definition, and go-to-implementation. + +use tower_lsp::lsp_types::*; + +use crate::analyzer::common::Type; +use crate::analyzer::symbol::{NodeId, Scope, ScopeKind, Symbol, SymbolKind, SymbolTable}; +use crate::project::{Module, Project}; +use crate::utils::{ + extract_word_at_offset_rope, format_global_ident, is_ident_char, offset_to_position, + position_to_char_offset, +}; + +use super::Backend; + +// ─── Handler wiring ───────────────────────────────────────────────────────────── + +impl Backend { + pub(crate) async fn handle_goto_definition( + &self, + params: GotoDefinitionParams, + ) -> Option { + let uri = ¶ms.text_document_position_params.text_document.uri; + let position = params.text_document_position_params.position; + let file_path = uri.path(); + + let project = self.get_file_project(file_path)?; + let location = find_definition(&project, file_path, position)?; + + Some(GotoDefinitionResponse::Scalar(location)) + } + + pub(crate) async fn handle_references( + &self, + params: ReferenceParams, + ) -> Option> { + let uri = ¶ms.text_document_position.text_document.uri; + let position = params.text_document_position.position; + let file_path = uri.path(); + + let project = self.get_file_project(file_path)?; + + find_references(&project, file_path, position, params.context.include_declaration) + } + + pub(crate) async fn handle_goto_type_definition( + &self, + params: GotoDefinitionParams, + ) -> Option { + let uri = ¶ms.text_document_position_params.text_document.uri; + let position = params.text_document_position_params.position; + let file_path = uri.path(); + + let project = self.get_file_project(file_path)?; + let location = find_type_definition(&project, file_path, position)?; + + Some(GotoDefinitionResponse::Scalar(location)) + } + + pub(crate) async fn handle_goto_implementation( + &self, + params: GotoDefinitionParams, + ) -> Option { + let uri = ¶ms.text_document_position_params.text_document.uri; + let position = params.text_document_position_params.position; + let file_path = uri.path(); + + let project = self.get_file_project(file_path)?; + let locations = find_implementations(&project, file_path, position)?; + + if locations.len() == 1 { + Some(GotoDefinitionResponse::Scalar(locations.into_iter().next().unwrap())) + } else { + Some(GotoDefinitionResponse::Array(locations)) + } + } +} + +// ─── Go to definition ─────────────────────────────────────────────────────────── + +/// Resolve the definition location for the symbol under the cursor. +fn find_definition(project: &Project, file_path: &str, position: Position) -> Option { + let cursor = resolve_cursor(project, file_path, position)?; + let symbol = resolve_symbol(project, &cursor)?; + + symbol_to_location(project, &symbol) +} + +// ─── Find all references ──────────────────────────────────────────────────────── + +/// Collect every location that references the same symbol as the one under the cursor. +fn find_references( + project: &Project, + file_path: &str, + position: Position, + include_declaration: bool, +) -> Option> { + let cursor = resolve_cursor(project, file_path, position)?; + let target = resolve_symbol(project, &cursor)?; + let def_offset = symbol_def_range(&target.kind); + + let db = project.module_db.lock().ok()?; + let st = project.symbol_table.lock().ok()?; + + let mut locations: Vec = Vec::new(); + + for module in db.iter() { + let uri = match Url::from_file_path(&module.path) { + Ok(u) => u, + Err(_) => continue, + }; + + collect_references_in_module( + &st, + module, + &target, + def_offset, + include_declaration, + &uri, + &mut locations, + ); + } + + if locations.is_empty() { + return None; + } + + Some(locations) +} + +/// Scan a single module's `token_db` for tokens that resolve to the same symbol. +fn collect_references_in_module( + st: &SymbolTable, + module: &Module, + target: &ResolvedSymbol, + def_offset: (usize, usize), + include_declaration: bool, + uri: &Url, + locations: &mut Vec, +) { + let raw_name = target.raw_name(); + + for token in &module.token_db { + if token.literal != raw_name { + continue; + } + + // Skip the declaration site unless requested. + if !include_declaration && module.ident == target.module_ident { + if token.start == def_offset.0 { + continue; + } + } + + let scope_id = find_scope_at_offset(st, module.scope_id, token.start); + if !token_resolves_to_same_symbol(st, &token.literal, scope_id, module, target) { + continue; + } + + let Some(start) = offset_to_position(token.start, &module.rope) else { + continue; + }; + let Some(end) = offset_to_position(token.end, &module.rope) else { + continue; + }; + + locations.push(Location { + uri: uri.clone(), + range: Range { start, end }, + }); + } +} + +/// Check whether `ident` in `scope_id` resolves to the same symbol as `target`. +fn token_resolves_to_same_symbol( + st: &SymbolTable, + ident: &str, + scope_id: NodeId, + module: &Module, + target: &ResolvedSymbol, +) -> bool { + // Local/scoped lookup. + if let Some(id) = st.lookup_symbol(ident, scope_id) { + return id == target.symbol_id; + } + + // Module-qualified lookup. + if let Some(id) = st.find_module_symbol_id(&module.ident, ident) { + return id == target.symbol_id; + } + + // Selective imports. + for import in &module.dependencies { + if !import.is_selective { + continue; + } + let Some(ref items) = import.select_items else { + continue; + }; + for item in items { + let local_name = item.alias.as_deref().unwrap_or(&item.ident); + if local_name != ident { + continue; + } + let global = format_global_ident(import.module_ident.clone(), item.ident.clone()); + if let Some(id) = st.find_symbol_id(&global, st.global_scope_id) { + return id == target.symbol_id; + } + } + } + + // Wildcard imports. + for import in &module.dependencies { + if import.as_name != "*" { + continue; + } + if let Some(id) = st.find_module_symbol_id(&import.module_ident, ident) { + return id == target.symbol_id; + } + } + + false +} + +// ─── Go to type definition ────────────────────────────────────────────────────── + +/// Navigate from a variable/constant to the definition of its type. +/// +/// For example, if the cursor is on `x` where `var x: MyStruct = ...`, this +/// navigates to the `type MyStruct = struct { ... }` definition. +fn find_type_definition( + project: &Project, + file_path: &str, + position: Position, +) -> Option { + let cursor = resolve_cursor(project, file_path, position)?; + let symbol = resolve_symbol(project, &cursor)?; + + // Extract the type from the symbol. + let type_ = extract_symbol_type(&symbol.kind)?; + + // Look up the type's definition via its symbol_id. + type_to_location(project, &type_) +} + +/// Extract the `Type` associated with a symbol (variable's type, function's +/// return type, constant's type). +fn extract_symbol_type(kind: &SymbolKind) -> Option { + match kind { + SymbolKind::Var(v) => { + let v = v.lock().unwrap(); + Some(v.type_.clone()) + } + SymbolKind::Const(c) => { + let c = c.lock().unwrap(); + Some(c.type_.clone()) + } + SymbolKind::Fn(f) => { + let f = f.lock().unwrap(); + Some(f.return_type.clone()) + } + // If the cursor is already on a type, go to it directly. + SymbolKind::Type(t) => { + let t = t.lock().unwrap(); + // For aliases, navigate to the aliased type. + if t.is_alias { + Some(t.type_expr.clone()) + } else { + None // already at the type itself + } + } + } +} + +/// Convert a `Type` to a `Location` by looking up its `symbol_id` in the +/// symbol table to find the `TypedefStmt` definition site. +fn type_to_location(project: &Project, type_: &Type) -> Option { + // Only user-defined types have navigable definitions. + if type_.symbol_id == 0 { + return None; + } + + let st = project.symbol_table.lock().ok()?; + let symbol = st.get_symbol_ref(type_.symbol_id)?; + + // Must be a Type symbol. + let SymbolKind::Type(typedef_mutex) = &symbol.kind else { + return None; + }; + + let typedef = typedef_mutex.lock().unwrap(); + let def_start = typedef.symbol_start; + let def_end = typedef.symbol_end; + drop(typedef); + + let module_ident = module_ident_for_scope(&st, symbol.defined_in)?; + drop(st); + + let db = project.module_db.lock().ok()?; + let module = db.iter().find(|m| m.ident == module_ident)?; + + let start = offset_to_position(def_start, &module.rope)?; + let end = offset_to_position(def_end, &module.rope)?; + let uri = Url::from_file_path(&module.path).ok()?; + + Some(Location { + uri, + range: Range { start, end }, + }) +} + +// ─── Go to implementation ─────────────────────────────────────────────────────── + +/// Find all types that implement a given interface. +/// +/// When the cursor is on an interface type, this collects all `type X = struct { ... }` +/// definitions that have `impl InterfaceName` in their `impl_interfaces` list. +fn find_implementations( + project: &Project, + file_path: &str, + position: Position, +) -> Option> { + let cursor = resolve_cursor(project, file_path, position)?; + let symbol = resolve_symbol(project, &cursor)?; + + // Must be a type symbol, ideally an interface. + let SymbolKind::Type(typedef_mutex) = &symbol.kind else { + return None; + }; + + let typedef = typedef_mutex.lock().unwrap(); + let target_ident = typedef.ident.clone(); + let is_interface = typedef.is_interface; + drop(typedef); + + if !is_interface { + return None; + } + + let db = project.module_db.lock().ok()?; + let st = project.symbol_table.lock().ok()?; + let mut locations: Vec = Vec::new(); + + // Scan all symbols for types that implement this interface. + for module in db.iter() { + let Some(scope) = st.get_scope(module.scope_id) else { + continue; + }; + + for &sym_id in &scope.symbols { + let Some(sym) = st.get_symbol_ref(sym_id) else { + continue; + }; + + let SymbolKind::Type(td_mutex) = &sym.kind else { + continue; + }; + + let td = td_mutex.lock().unwrap(); + + // Check if this type implements the target interface. + let implements = td.impl_interfaces.iter().any(|iface| { + iface.ident == target_ident || iface.symbol_id == symbol.symbol_id + }); + + if !implements { + continue; + } + + let Some(start) = offset_to_position(td.symbol_start, &module.rope) else { + continue; + }; + let Some(end) = offset_to_position(td.symbol_end, &module.rope) else { + continue; + }; + let Ok(uri) = Url::from_file_path(&module.path) else { + continue; + }; + + locations.push(Location { + uri, + range: Range { start, end }, + }); + } + } + + if locations.is_empty() { + return None; + } + + Some(locations) +} + +// ─── Cursor resolution ────────────────────────────────────────────────────────── + +/// Intermediate: identifies the word and scope under the cursor. +pub(crate) struct CursorContext { + pub(crate) word: String, + /// If the cursor is on the right side of a dot (e.g. `bootstrap.app`), + /// this holds the left-side identifier (`"bootstrap"`). + pub(crate) prefix: Option, + pub(crate) char_offset: usize, + pub(crate) module_ident: String, + pub(crate) module_path: String, + pub(crate) scope_id: NodeId, + pub(crate) dependencies: Vec, +} + +/// Convert an LSP position into a `CursorContext` (word + scope). +pub(crate) fn resolve_cursor( + project: &Project, + file_path: &str, + position: Position, +) -> Option { + let mh = project.module_handled.lock().ok()?; + let &idx = mh.get(file_path)?; + drop(mh); + + let db = project.module_db.lock().ok()?; + let module = db.get(idx)?; + + let char_offset = position_to_char_offset(position, &module.rope)?; + let (word, word_start, _) = extract_word_at_offset_rope(&module.rope, char_offset)?; + + // Check for a dot-prefix: if there's a `.` immediately before the word, + // extract the identifier to the left of the dot. + let prefix = if word_start >= 2 && module.rope.char(word_start - 1) == '.' { + // Walk left from the dot to find the prefix identifier. + let dot_pos = word_start - 1; + let mut prefix_start = dot_pos; + while prefix_start > 0 && is_ident_char(module.rope.char(prefix_start - 1)) { + prefix_start -= 1; + } + if prefix_start < dot_pos { + let prefix_word: String = module.rope.slice(prefix_start..dot_pos).chars().collect(); + Some(prefix_word) + } else { + None + } + } else { + None + }; + + let st = project.symbol_table.lock().ok()?; + let scope_id = find_scope_at_offset(&st, module.scope_id, char_offset); + + Some(CursorContext { + word, + prefix, + char_offset, + module_ident: module.ident.clone(), + module_path: module.path.clone(), + scope_id, + dependencies: module.dependencies.clone(), + }) +} + +// ─── Symbol resolution ────────────────────────────────────────────────────────── + +/// A fully resolved symbol: its id, kind, owning module ident, and position. +pub(crate) struct ResolvedSymbol { + pub(crate) symbol_id: NodeId, + pub(crate) kind: SymbolKind, + /// The global ident (may include module prefix). + pub(crate) ident: String, + /// Module ident of the module that defines this symbol. + /// Stored instead of module_path to avoid locking module_db while + /// symbol_table is held (which would invert the canonical lock order). + pub(crate) module_ident: String, +} + +impl ResolvedSymbol { + /// The raw (unprefixed) name for token matching. + pub(crate) fn raw_name(&self) -> &str { + self.ident.rsplit('.').next().unwrap_or(&self.ident) + } + + /// Lazily resolve the module file path from `module_ident`. + /// Locks `module_db` — call only when `symbol_table` is NOT held. + pub(crate) fn module_path(&self, project: &Project) -> Option { + let db = project.module_db.lock().ok()?; + db.iter() + .find(|m| m.ident == self.module_ident) + .map(|m| m.path.clone()) + } +} + +/// Resolve a cursor context into a `ResolvedSymbol`. +/// +/// Follows the same resolution order as the semantic pass: +/// local scopes → same-module global → selective imports → wildcard imports → builtins. +pub(crate) fn resolve_symbol(project: &Project, ctx: &CursorContext) -> Option { + let st = project.symbol_table.lock().ok()?; + + // 1. Local / parent-scope walk. + if let Some(id) = st.lookup_symbol(&ctx.word, ctx.scope_id) { + return resolved_from_id(&st, id, &ctx.word); + } + + // 2. Same-module global (symbols are stored as "module_ident.name"). + if let Some(id) = st.find_module_symbol_id(&ctx.module_ident, &ctx.word) { + let global = format_global_ident(ctx.module_ident.clone(), ctx.word.clone()); + return resolved_from_id(&st, id, &global); + } + + // 3. Selective imports. + for import in &ctx.dependencies { + if !import.is_selective { + continue; + } + let Some(ref items) = import.select_items else { + continue; + }; + for item in items { + let local_name = item.alias.as_deref().unwrap_or(&item.ident); + if local_name != ctx.word { + continue; + } + let global = format_global_ident(import.module_ident.clone(), item.ident.clone()); + if let Some(id) = st.find_symbol_id(&global, st.global_scope_id) { + return resolved_from_id(&st, id, &global); + } + } + } + + // 4. Wildcard imports. + for import in &ctx.dependencies { + if import.as_name != "*" { + continue; + } + if let Some(id) = st.find_module_symbol_id(&import.module_ident, &ctx.word) { + let global = format_global_ident(import.module_ident.clone(), ctx.word.clone()); + return resolved_from_id(&st, id, &global); + } + } + + // 5. Builtins (global scope, no prefix). + if let Some(id) = st.find_symbol_id(&ctx.word, st.global_scope_id) { + return resolved_from_id(&st, id, &ctx.word); + } + + // 6. Dotted access: prefix.word (e.g. bootstrap.app or myVar.field). + if let Some(ref prefix) = ctx.prefix { + log::debug!("resolve_symbol step 6: prefix={:?}, word={}", prefix, ctx.word); + + // 6a. Module member access: prefix is an import as_name. + for import in &ctx.dependencies { + if import.as_name == *prefix { + let global = format_global_ident(import.module_ident.clone(), ctx.word.clone()); + log::debug!("step 6a: trying import global={}", global); + if let Some(id) = st.find_symbol_id(&global, st.global_scope_id) { + return resolved_from_id(&st, id, &global); + } + } + } + + // 6b. Method/field access: prefix is a variable/const — resolve its type. + if let Some(prefix_symbol) = resolve_prefix_symbol(&st, ctx, prefix) { + log::debug!("step 6b: prefix_symbol kind={:?}", std::mem::discriminant(&prefix_symbol.kind)); + let prefix_type = extract_type_from_kind(&prefix_symbol.kind); + if let Some(ref prefix_type) = prefix_type { + log::debug!("step 6b: prefix_type kind={}, ident={}, symbol_id={}", prefix_type.kind, prefix_type.ident, prefix_type.symbol_id); + // Look up the typedef to find methods or struct fields. + if let Some(result) = resolve_member_on_type(&st, &prefix_type, &ctx.word) { + return Some(result); + } + log::debug!("step 6b: resolve_member_on_type returned None"); + } else { + log::debug!("step 6b: prefix_type is None"); + } + } else { + log::debug!("step 6b: resolve_prefix_symbol returned None for prefix={}", prefix); + } + } + + None +} + +/// Build a `ResolvedSymbol` from a symbol id. +/// +/// Only uses `st` (no `module_db` lock), so it is safe to call while +/// `symbol_table` is held. +fn resolved_from_id( + st: &SymbolTable, + symbol_id: NodeId, + ident: &str, +) -> Option { + let symbol = st.get_symbol_ref(symbol_id)?; + let module_ident = module_ident_for_scope(st, symbol.defined_in) + .unwrap_or_default(); + + Some(ResolvedSymbol { + symbol_id, + kind: symbol.kind.clone(), + ident: ident.to_string(), + module_ident, + }) +} + +// ─── Dotted access helpers ────────────────────────────────────────────────────── + +/// Resolve the prefix identifier (left of the dot) to a symbol, using the same +/// lookup chain as `resolve_symbol` but for a single word. +fn resolve_prefix_symbol<'a>( + st: &'a SymbolTable, + ctx: &CursorContext, + prefix: &str, +) -> Option { + // Local scope walk. + if let Some(id) = st.lookup_symbol(prefix, ctx.scope_id) { + return st.get_symbol_ref(id).cloned(); + } + // Same-module global. + if let Some(id) = st.find_module_symbol_id(&ctx.module_ident, prefix) { + return st.get_symbol_ref(id).cloned(); + } + // Selective imports. + for import in &ctx.dependencies { + if !import.is_selective { + continue; + } + if let Some(ref items) = import.select_items { + for item in items { + let local_name = item.alias.as_deref().unwrap_or(&item.ident); + if local_name == prefix { + let global = format_global_ident(import.module_ident.clone(), item.ident.clone()); + if let Some(id) = st.find_symbol_id(&global, st.global_scope_id) { + return st.get_symbol_ref(id).cloned(); + } + } + } + } + } + // Wildcard imports. + for import in &ctx.dependencies { + if import.as_name != "*" { + continue; + } + if let Some(id) = st.find_module_symbol_id(&import.module_ident, prefix) { + return st.get_symbol_ref(id).cloned(); + } + } + // Builtins. + if let Some(sym) = st.find_global_symbol(prefix) { + return Some(sym.clone()); + } + None +} + +/// Extract the type from a symbol kind (Var → type_, Const → type_, Fn → return_type). +fn extract_type_from_kind(kind: &SymbolKind) -> Option { + match kind { + SymbolKind::Var(v) => Some(v.lock().unwrap().type_.clone()), + SymbolKind::Const(c) => Some(c.lock().unwrap().type_.clone()), + SymbolKind::Fn(f) => Some(f.lock().unwrap().return_type.clone()), + SymbolKind::Type(_) => None, + } +} + +/// Resolve a member (method or struct field) on a given type. +fn resolve_member_on_type( + st: &SymbolTable, + owner_type: &Type, + member: &str, +) -> Option { + // Dereference pointer/ref types to get the underlying type. + let base_type = match &owner_type.kind { + crate::analyzer::common::TypeKind::Ptr(inner) + | crate::analyzer::common::TypeKind::Ref(inner) => inner.as_ref(), + // Handle unreduced ref/ptr stored as TypeKind::Ident with args. + crate::analyzer::common::TypeKind::Ident + if (owner_type.ident == "ref" || owner_type.ident == "ptr") + && !owner_type.args.is_empty() => + { + &owner_type.args[0] + } + _ => owner_type, + }; + + // Find the typedef by symbol_id or ident. + log::debug!("resolve_member_on_type: member={}, base_type.kind={}, base_type.ident={}, base_type.symbol_id={}", member, base_type.kind, base_type.ident, base_type.symbol_id); + let (typedef_symbol, typedef_symbol_id) = if base_type.symbol_id > 0 { + log::debug!("resolve_member_on_type: looking up by symbol_id={}", base_type.symbol_id); + (st.get_symbol_ref(base_type.symbol_id), base_type.symbol_id) + } else if !base_type.ident.is_empty() { + // Try direct lookup (module-qualified ident). + let sym = st.find_global_symbol(&base_type.ident); + log::debug!("resolve_member_on_type: find_global_symbol({}) = {}", base_type.ident, sym.is_some()); + if sym.is_some() { + // Look up the symbol_id from the global scope + let scope = st.get_scope(st.global_scope_id); + let sid = scope.and_then(|s| s.symbol_map.get(&base_type.ident).copied()).unwrap_or(0); + (sym, sid) + } else { + // Fallback: search all global symbols for a matching suffix. + let scope = st.get_scope(st.global_scope_id)?; + let found = scope.symbol_map.iter() + .find(|(k, _)| { + k.rsplit('.').next() == Some(&base_type.ident) + }); + if let Some((_, &id)) = found { + (st.get_symbol_ref(id), id) + } else { + log::debug!("resolve_member_on_type: suffix fallback = false"); + (None, 0) + } + } + } else { + // Type has no ident and no symbol_id — check for inline struct. + if let crate::analyzer::common::TypeKind::Struct(_, _, ref props) = base_type.kind { + for prop in props { + if prop.name == member { + let var = crate::analyzer::common::VarDeclExpr { + ident: prop.name.clone(), + symbol_id: 0, + symbol_start: prop.start, + symbol_end: prop.end, + type_: prop.type_.clone(), + be_capture: false, + heap_ident: None, + is_private: false, + }; + return Some(ResolvedSymbol { + symbol_id: 0, + kind: SymbolKind::Var(std::sync::Arc::new(std::sync::Mutex::new(var))), + ident: prop.name.clone(), + module_ident: String::new(), + }); + } + } + } + (None, 0) + }; + + if let Some(sym) = typedef_symbol { + if let SymbolKind::Type(typedef_mutex) = &sym.kind { + let typedef = typedef_mutex.lock().unwrap(); + let owner_module_ident = module_ident_for_scope(st, sym.defined_in) + .unwrap_or_default(); + + // Check method_table: try exact key first, then suffix match. + // Keys may be fully-qualified (e.g. "module.Type.method") while + // `member` is just the short name ("method"). + let method_fndef = typedef.method_table.get(member).cloned().or_else(|| { + typedef.method_table.iter() + .find(|(k, _)| k.rsplit('.').next() == Some(member)) + .map(|(_, v)| v.clone()) + }); + if let Some(fndef) = method_fndef { + return Some(ResolvedSymbol { + symbol_id: 0, // method is in method_table, not symbol table + kind: SymbolKind::Fn(fndef), + ident: member.to_string(), + module_ident: owner_module_ident, + }); + } + + // Check struct fields. + if let crate::analyzer::common::TypeKind::Struct(_, _, ref props) = typedef.type_expr.kind { + for prop in props { + if prop.name == member { + // Represent struct field as a Var for hover display. + let var = crate::analyzer::common::VarDeclExpr { + ident: prop.name.clone(), + symbol_id: 0, + symbol_start: prop.start, + symbol_end: prop.end, + type_: prop.type_.clone(), + be_capture: false, + heap_ident: None, + is_private: false, + }; + return Some(ResolvedSymbol { + symbol_id: 0, + kind: SymbolKind::Var(std::sync::Arc::new(std::sync::Mutex::new(var))), + ident: prop.name.clone(), + module_ident: owner_module_ident, + }); + } + } + } + } + } + + // Fallback: scan global scope for impl method symbols. + // Method functions are registered as separate symbols with + // `fndef.impl_type.symbol_id` matching the typedef. Cross-module + // re-resolution may produce a different symbol_id, so also match + // by ident (full or short name). + let type_ident = &base_type.ident; + let type_short_name = type_ident.rsplit('.').next().unwrap_or(type_ident); + + if let Some(scope) = st.get_scope(st.global_scope_id) { + for (_, &sym_id) in &scope.symbol_map { + if let Some(sym) = st.get_symbol_ref(sym_id) { + if let SymbolKind::Fn(fndef_mutex) = &sym.kind { + let fndef = fndef_mutex.lock().unwrap(); + if fndef.fn_name != member { + continue; + } + // Match by symbol_id or by ident (full or suffix). + let id_match = typedef_symbol_id > 0 + && fndef.impl_type.symbol_id == typedef_symbol_id; + let ident_match = !fndef.impl_type.ident.is_empty() + && (fndef.impl_type.ident == *type_ident + || fndef.impl_type.ident.rsplit('.').next() == Some(type_short_name)); + if id_match || ident_match { + let owner_module_ident = module_ident_for_scope(st, sym.defined_in) + .unwrap_or_default(); + return Some(ResolvedSymbol { + symbol_id: sym_id, + kind: SymbolKind::Fn(fndef_mutex.clone()), + ident: member.to_string(), + module_ident: owner_module_ident, + }); + } + } + } + } + } + + None +} + +// ─── Scope helpers ────────────────────────────────────────────────────────────── + +/// Find the deepest scope that contains `offset`. +fn find_scope_at_offset(st: &SymbolTable, root: NodeId, offset: usize) -> NodeId { + let mut best = root; + let mut stack = vec![root]; + + while let Some(id) = stack.pop() { + let Some(scope) = st.get_scope(id) else { + continue; + }; + + if id != root && !scope_contains(scope, offset) { + continue; + } + + best = id; + stack.extend_from_slice(&scope.children); + } + + best +} + +/// Whether a scope's range covers `offset`. +fn scope_contains(scope: &Scope, offset: usize) -> bool { + let (start, end) = scope.range; + if start == 0 && end == 0 { + return true; // unbounded (module-level) + } + offset >= start && offset < end +} + +/// Walk up from a scope to find the owning module ident. +fn module_ident_for_scope(st: &SymbolTable, scope_id: NodeId) -> Option { + let mut current = scope_id; + while current > 0 { + let scope = st.get_scope(current)?; + match &scope.kind { + ScopeKind::Module(ident) => return Some(ident.clone()), + ScopeKind::Global => { + // Symbol defined in the global scope (builtin). + // Map it to whichever module — builtins are synthetic; + // returning None means "no navigable source". + return None; + } + _ => current = scope.parent, + } + } + None +} + +// ─── Location helpers ─────────────────────────────────────────────────────────── + +/// Get the char-offset range `(start, end)` of a symbol's definition site. +fn symbol_def_range(kind: &SymbolKind) -> (usize, usize) { + match kind { + SymbolKind::Var(v) => { + let v = v.lock().unwrap(); + (v.symbol_start, v.symbol_end) + } + SymbolKind::Fn(f) => { + let f = f.lock().unwrap(); + (f.symbol_start, f.symbol_end) + } + SymbolKind::Type(t) => { + let t = t.lock().unwrap(); + (t.symbol_start, t.symbol_end) + } + SymbolKind::Const(c) => { + let c = c.lock().unwrap(); + (c.symbol_start, c.symbol_end) + } + } +} + +/// Convert a resolved symbol into an LSP `Location`. +fn symbol_to_location(project: &Project, symbol: &ResolvedSymbol) -> Option { + let (def_start, def_end) = symbol_def_range(&symbol.kind); + + let db = project.module_db.lock().ok()?; + let module = db.iter().find(|m| m.ident == symbol.module_ident)?; + + let start = offset_to_position(def_start, &module.rope)?; + let end = offset_to_position(def_end, &module.rope)?; + let uri = Url::from_file_path(&module.path).ok()?; + + Some(Location { + uri, + range: Range { start, end }, + }) +} + +// ─── Tests ────────────────────────────────────────────────────────────────────── + +#[cfg(test)] +mod tests { + use super::*; + use crate::analyzer::symbol::{Scope, ScopeKind, SymbolTable}; + + #[test] + fn scope_contains_unbounded() { + let scope = Scope { + parent: 0, + symbols: vec![], + children: vec![], + symbol_map: Default::default(), + range: (0, 0), + kind: ScopeKind::Global, + frees: Default::default(), + }; + assert!(scope_contains(&scope, 0)); + assert!(scope_contains(&scope, 999)); + } + + #[test] + fn scope_contains_bounded() { + let scope = Scope { + parent: 0, + symbols: vec![], + children: vec![], + symbol_map: Default::default(), + range: (10, 50), + kind: ScopeKind::Local, + frees: Default::default(), + }; + assert!(!scope_contains(&scope, 9)); + assert!(scope_contains(&scope, 10)); + assert!(scope_contains(&scope, 49)); + assert!(!scope_contains(&scope, 50)); + } + + #[test] + fn find_scope_picks_deepest() { + let mut st = SymbolTable::new(); + + // Create a module scope (root). + let module_id = st.create_scope(ScopeKind::Module("test".into()), st.global_scope_id, 0, 0); + // Create a child scope at [10, 50). + let _child = st.create_scope(ScopeKind::Local, module_id, 10, 50); + // Create a nested child at [20, 40). + let nested = st.create_scope(ScopeKind::Local, _child, 20, 40); + + assert_eq!(find_scope_at_offset(&st, module_id, 5), module_id); + assert_eq!(find_scope_at_offset(&st, module_id, 15), _child); + assert_eq!(find_scope_at_offset(&st, module_id, 25), nested); + assert_eq!(find_scope_at_offset(&st, module_id, 45), _child); + assert_eq!(find_scope_at_offset(&st, module_id, 55), module_id); + } + + #[test] + fn symbol_def_range_var() { + use crate::analyzer::common::VarDeclExpr; + use std::sync::{Arc, Mutex}; + + let var = Arc::new(Mutex::new(VarDeclExpr { + ident: "x".into(), + symbol_id: 0, + symbol_start: 10, + symbol_end: 11, + type_: crate::analyzer::common::Type::default(), + be_capture: false, + heap_ident: None, + })); + assert_eq!(symbol_def_range(&SymbolKind::Var(var)), (10, 11)); + } +} diff --git a/nls/src/server/semantic_tokens.rs b/nls/src/server/semantic_tokens.rs new file mode 100644 index 00000000..16acd0c7 --- /dev/null +++ b/nls/src/server/semantic_tokens.rs @@ -0,0 +1,260 @@ +//! Semantic token request handlers (`textDocument/semanticTokens/*`). + +use tower_lsp::lsp_types::*; + +use crate::analyzer::lexer::Token; +use crate::project::Project; + +use super::Backend; + +impl Backend { + pub(crate) async fn handle_semantic_tokens_full( + &self, + params: SemanticTokensParams, + ) -> Option { + let uri = params.text_document.uri; + let file_path = uri.path(); + let project = self.get_file_project(file_path)?; + let (rope, tokens) = module_semantic_tokens(&project, file_path)?; + + let data = encode_semantic_tokens(&tokens, &rope, None); + Some(SemanticTokensResult::Tokens(SemanticTokens { + result_id: None, + data, + })) + } + + pub(crate) async fn handle_semantic_tokens_range( + &self, + params: SemanticTokensRangeParams, + ) -> Option { + let uri = params.text_document.uri; + let file_path = uri.path(); + let project = self.get_file_project(file_path)?; + let (rope, tokens) = module_semantic_tokens(&project, file_path)?; + + let data = encode_semantic_tokens(&tokens, &rope, Some(params.range)); + Some(SemanticTokensRangeResult::Tokens(SemanticTokens { + result_id: None, + data, + })) + } +} + +fn module_semantic_tokens(project: &Project, file_path: &str) -> Option<(ropey::Rope, Vec)> { + let module_index = { + let module_handled = project.module_handled.lock().ok()?; + module_handled.get(file_path).copied()? + }; + + let module_db = project.module_db.lock().ok()?; + let module = module_db.get(module_index)?; + + // sem_token_db carries parser-resolved token kinds. If unavailable, + // fall back to lexer token types so highlighting still works. + let tokens = if module.sem_token_db.is_empty() { + module.token_db.clone() + } else { + module.sem_token_db.clone() + }; + + Some((module.rope.clone(), tokens)) +} + +fn position_to_char_offset(position: Position, rope: &ropey::Rope) -> Option { + let line_start = rope.try_line_to_char(position.line as usize).ok()?; + let offset = line_start + position.character as usize; + if offset > rope.len_chars() { + return None; + } + Some(offset) +} + +fn token_in_range(token: &Token, range: Range, rope: &ropey::Rope) -> bool { + let Some(range_start) = position_to_char_offset(range.start, rope) else { + return false; + }; + let Some(range_end) = position_to_char_offset(range.end, rope) else { + return false; + }; + token.start < range_end && token.end > range_start +} + +fn encode_semantic_tokens( + tokens: &[Token], + rope: &ropey::Rope, + range: Option, +) -> Vec { + // Index of VARIABLE in LEGEND_TYPE (the default for unresolved idents). + let variable_idx = crate::analyzer::lexer::semantic_token_type_index( + tower_lsp::lsp_types::SemanticTokenType::VARIABLE, + ) as u32; + + let mut sorted: Vec<&Token> = tokens + .iter() + // Only emit semantic tokens for identifiers where we add value + // beyond the TextMate grammar (resolved kind: function, type, property, + // namespace, macro, label). Let TM grammar handle keywords, comments, + // strings, numbers, and operators — this avoids color flickering + // during typing when stale semantic tokens don't match changed text. + .filter(|token| { + matches!( + token.token_type, + crate::analyzer::lexer::TokenType::Ident + | crate::analyzer::lexer::TokenType::MacroIdent + | crate::analyzer::lexer::TokenType::Label + ) + }) + // Skip idents that are still at their default VARIABLE type — let TM + // grammar color them so unresolved identifiers don't flicker. + .filter(|token| token.semantic_token_type as u32 != variable_idx) + .filter(|token| token.length > 0) + .filter(|token| range.map(|r| token_in_range(token, r, rope)).unwrap_or(true)) + .collect(); + + sorted.sort_by_key(|token| (token.line, token.start)); + + let mut prev_line: u32 = 0; + let mut prev_start: u32 = 0; + let mut encoded: Vec = Vec::with_capacity(sorted.len()); + + for token in sorted { + // Lexer uses 1-based line numbering. + let line = token.line.saturating_sub(1); + let Some(line_start) = rope.try_line_to_char(line).ok() else { + continue; + }; + + let start = token.start.saturating_sub(line_start); + let line_u32 = line as u32; + let start_u32 = start as u32; + + let (delta_line, delta_start) = if encoded.is_empty() { + (line_u32, start_u32) + } else if line_u32 == prev_line { + (0, start_u32.saturating_sub(prev_start)) + } else { + (line_u32.saturating_sub(prev_line), start_u32) + }; + + encoded.push(SemanticToken { + delta_line, + delta_start, + length: token.length as u32, + token_type: token.semantic_token_type as u32, + token_modifiers_bitset: 0, + }); + + prev_line = line_u32; + prev_start = start_u32; + } + + encoded +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::analyzer::lexer::{semantic_token_type_index, TokenType}; + use tower_lsp::lsp_types::SemanticTokenType; + + fn tok( + token_type: TokenType, + semantic: SemanticTokenType, + literal: &str, + start: usize, + end: usize, + line: usize, + ) -> Token { + let mut token = Token::new(token_type, literal.to_string(), start, end, line); + token.semantic_token_type = semantic_token_type_index(semantic); + token + } + + #[test] + fn encode_full_orders_and_deltas_tokens() { + let rope = ropey::Rope::from_str("ab cd\nef"); + let tokens = vec![ + tok( + TokenType::Ident, + SemanticTokenType::NAMESPACE, + "ef", + 6, + 8, + 2, + ), + tok( + TokenType::Ident, + SemanticTokenType::FUNCTION, + "ab", + 0, + 2, + 1, + ), + tok( + TokenType::Ident, + SemanticTokenType::TYPE, + "cd", + 3, + 5, + 1, + ), + ]; + + let encoded = encode_semantic_tokens(&tokens, &rope, None); + assert_eq!(encoded.len(), 3); + + // first token (line 0, col 0) + assert_eq!(encoded[0].delta_line, 0); + assert_eq!(encoded[0].delta_start, 0); + assert_eq!(encoded[0].length, 2); + + // second token same line, starts at col 3 => delta_start 3 + assert_eq!(encoded[1].delta_line, 0); + assert_eq!(encoded[1].delta_start, 3); + assert_eq!(encoded[1].length, 2); + + // third token next line, starts at col 0 + assert_eq!(encoded[2].delta_line, 1); + assert_eq!(encoded[2].delta_start, 0); + assert_eq!(encoded[2].length, 2); + } + + #[test] + fn encode_range_filters_tokens() { + let rope = ropey::Rope::from_str("ab cd\nef"); + let tokens = vec![ + tok( + TokenType::Ident, + SemanticTokenType::FUNCTION, + "ab", + 0, + 2, + 1, + ), + tok( + TokenType::Ident, + SemanticTokenType::TYPE, + "cd", + 3, + 5, + 1, + ), + tok( + TokenType::Ident, + SemanticTokenType::PROPERTY, + "ef", + 6, + 8, + 2, + ), + ]; + + // only second line (line index 1) + let range = Range::new(Position::new(1, 0), Position::new(1, 2)); + let encoded = encode_semantic_tokens(&tokens, &rope, Some(range)); + assert_eq!(encoded.len(), 1); + assert_eq!(encoded[0].delta_line, 1); + assert_eq!(encoded[0].delta_start, 0); + } +} \ No newline at end of file diff --git a/nls/src/server/signature_help.rs b/nls/src/server/signature_help.rs new file mode 100644 index 00000000..0c631258 --- /dev/null +++ b/nls/src/server/signature_help.rs @@ -0,0 +1,458 @@ +//! Signature help: show the function signature and highlight the active +//! parameter while typing inside a call expression `fn_name(…|…)`. + +use tower_lsp::lsp_types::*; + +use crate::analyzer::common::{AstCall, AstNode, Expr, Stmt, TypeKind}; +use crate::analyzer::symbol::SymbolKind; +use crate::project::{Module, Project}; +use crate::utils::position_to_char_offset; + +use super::Backend; + +// ─── Handler wiring ───────────────────────────────────────────────────────────── + +impl Backend { + pub(crate) async fn handle_signature_help( + &self, + params: SignatureHelpParams, + ) -> Option { + let uri = ¶ms.text_document_position_params.text_document.uri; + let position = params.text_document_position_params.position; + let file_path = uri.path(); + + let project = self.get_file_project(file_path)?; + build_signature_help(&project, file_path, position) + } +} + +// ─── Signature help construction ──────────────────────────────────────────────── + +/// Build a `SignatureHelp` for the call expression surrounding the cursor. +fn build_signature_help( + project: &Project, + file_path: &str, + position: Position, +) -> Option { + let mh = project.module_handled.lock().ok()?; + let &idx = mh.get(file_path)?; + drop(mh); + + let db = project.module_db.lock().ok()?; + let module = db.get(idx)?; + + let char_offset = position_to_char_offset(position, &module.rope)?; + + // Find the innermost call that contains the cursor. + let call_ctx = find_call_at_offset(&module.stmts, module, char_offset)?; + + // Resolve the callee to get parameter information. + let info = resolve_signature_info(&call_ctx.call, project)?; + + let active_param = compute_active_parameter(&call_ctx.call, char_offset); + + Some(SignatureHelp { + signatures: vec![info], + active_signature: Some(0), + active_parameter: Some(active_param), + }) +} + +// ─── Call context ─────────────────────────────────────────────────────────────── + +/// A call expression found at the cursor position. +struct CallContext<'a> { + call: &'a AstCall, +} + +/// Search the AST for the innermost `Call` expression containing `offset`. +fn find_call_at_offset<'a>( + stmts: &'a [Box], + module: &'a Module, + offset: usize, +) -> Option> { + // First scan top-level statements. + if let Some(ctx) = find_call_in_stmts(stmts, offset) { + return Some(ctx); + } + + // Then scan function bodies. + for fndef_mutex in &module.all_fndefs { + let fndef = fndef_mutex.lock().unwrap(); + // Quick bounds check. + if offset < fndef.body.start || offset > fndef.body.end { + continue; + } + // Safety: We're reading the body stmts while holding the lock. + // The stmts reference is valid for the duration of this block. + // We need to use unsafe to extend the lifetime since the borrow + // checker can't see that the data outlives the lock. + let stmts_ptr = &fndef.body.stmts as *const Vec>; + drop(fndef); + // SAFETY: module_db is borrowed immutably and all_fndefs are Arc>. + // The underlying data won't be mutated while we hold the module_db lock. + let stmts = unsafe { &*stmts_ptr }; + if let Some(ctx) = find_call_in_stmts(stmts, offset) { + return Some(ctx); + } + } + + None +} + +/// Walk a list of statements looking for the innermost call at `offset`. +fn find_call_in_stmts<'a>(stmts: &'a [Box], offset: usize) -> Option> { + for stmt in stmts { + if offset < stmt.start || offset > stmt.end { + continue; + } + if let Some(ctx) = find_call_in_node(&stmt.node, offset) { + return Some(ctx); + } + } + None +} + +/// Walk an AST node looking for the innermost call whose span covers `offset`. +fn find_call_in_node<'a>(node: &'a AstNode, offset: usize) -> Option> { + match node { + AstNode::Call(call) => { + // First check nested calls in arguments (innermost wins). + for arg in &call.args { + if offset >= arg.start && offset <= arg.end { + if let Some(inner) = find_call_in_node(&arg.node, offset) { + return Some(inner); + } + } + } + // This call itself contains the cursor. + Some(CallContext { call }) + } + AstNode::VarDef(_, right) => find_call_in_expr(right, offset), + AstNode::Assign(left, right) => { + find_call_in_expr(left, offset).or_else(|| find_call_in_expr(right, offset)) + } + AstNode::Return(Some(expr)) | AstNode::Ret(expr) | AstNode::Fake(expr) => { + find_call_in_expr(expr, offset) + } + AstNode::If(cond, consequent, alternate) => { + find_call_in_expr(cond, offset) + .or_else(|| find_call_in_stmts(&consequent.stmts, offset)) + .or_else(|| find_call_in_stmts(&alternate.stmts, offset)) + } + AstNode::ForIterator(iter_expr, _, _, body) => { + find_call_in_expr(iter_expr, offset) + .or_else(|| find_call_in_stmts(&body.stmts, offset)) + } + AstNode::ForCond(cond, body) => { + find_call_in_expr(cond, offset) + .or_else(|| find_call_in_stmts(&body.stmts, offset)) + } + _ => None, + } +} + +/// Walk an expression looking for calls. +fn find_call_in_expr<'a>(expr: &'a Expr, offset: usize) -> Option> { + if offset < expr.start || offset > expr.end { + return None; + } + find_call_in_node(&expr.node, offset) +} + +// ─── Signature resolution ─────────────────────────────────────────────────────── + +/// Resolve the callee of a `Call` to a `SignatureInformation`. +fn resolve_signature_info(call: &AstCall, project: &Project) -> Option { + // Direct ident call. + if let AstNode::Ident(name, symbol_id) = &call.left.node { + return signature_from_symbol(*symbol_id, name, project); + } + + // Method call via StructSelect. + if let AstNode::StructSelect(_, key, prop) = &call.left.node { + if let TypeKind::Fn(ref fn_type) = prop.type_.kind { + return signature_from_fn_name(&fn_type.name, key, project); + } + } + + // Fallback: try from the left expression's type. + if let TypeKind::Fn(ref fn_type) = call.left.type_.kind { + let label = format_signature_from_type(fn_type, &fn_type.name); + return Some(SignatureInformation { + label, + documentation: None, + parameters: None, + active_parameter: None, + }); + } + + None +} + +/// Build a `SignatureInformation` from a symbol id (direct call). +fn signature_from_symbol( + symbol_id: usize, + name: &str, + project: &Project, +) -> Option { + let st = project.symbol_table.lock().ok()?; + let symbol = st.get_symbol_ref(symbol_id)?; + + match &symbol.kind { + SymbolKind::Fn(fndef_mutex) => { + let fndef = fndef_mutex.lock().unwrap(); + Some(build_signature_information(&fndef, name)) + } + _ => None, + } +} + +/// Try to find a function by name across all modules. +fn signature_from_fn_name( + fn_name: &str, + display_name: &str, + project: &Project, +) -> Option { + if fn_name.is_empty() { + return None; + } + let db = project.module_db.lock().ok()?; + for module in db.iter() { + for fndef_mutex in &module.all_fndefs { + let fndef = fndef_mutex.lock().unwrap(); + if fndef.fn_name == fn_name || fndef.symbol_name == fn_name { + return Some(build_signature_information(&fndef, display_name)); + } + } + } + None +} + +/// Construct the full `SignatureInformation` including parameter labels. +fn build_signature_information( + fndef: &crate::analyzer::common::AstFnDef, + display_name: &str, +) -> SignatureInformation { + let mut param_parts: Vec = Vec::new(); + let mut parameters: Vec = Vec::new(); + + for param in &fndef.params { + let p = param.lock().unwrap(); + + if p.ident == "self" { + // Don't include self in the parameter list for signature help. + continue; + } + + let type_str = type_display(&p.type_); + let param_label = format!("{}: {}", p.ident, type_str); + param_parts.push(param_label.clone()); + parameters.push(ParameterInformation { + label: ParameterLabel::Simple(param_label), + documentation: None, + }); + } + + if fndef.rest_param { + let rest_label = "...".to_string(); + param_parts.push(rest_label.clone()); + parameters.push(ParameterInformation { + label: ParameterLabel::Simple(rest_label), + documentation: None, + }); + } + + let ret = &fndef.return_type; + let mut label = format!("fn {}({}): {}", display_name, param_parts.join(", "), ret); + if fndef.is_errable { + label.push('!'); + } + + SignatureInformation { + label, + documentation: None, + parameters: Some(parameters), + active_parameter: None, + } +} + +/// Format a signature from a `TypeFn` (no parameter names available). +fn format_signature_from_type( + fn_type: &crate::analyzer::common::TypeFn, + name: &str, +) -> String { + let params: Vec = fn_type + .param_types + .iter() + .map(|t| type_display(t)) + .collect(); + let mut sig = format!("fn {}({}): {}", name, params.join(", "), fn_type.return_type); + if fn_type.errable { + sig.push('!'); + } + sig +} + +/// Prefer the type's ident for display. +fn type_display(t: &crate::analyzer::common::Type) -> String { + if !t.ident.is_empty() { + t.ident.clone() + } else { + t.to_string() + } +} + +// ─── Active parameter ─────────────────────────────────────────────────────────── + +/// Determine which parameter is "active" based on the cursor offset relative +/// to the arguments in the call. +fn compute_active_parameter(call: &AstCall, offset: usize) -> u32 { + if call.args.is_empty() { + return 0; + } + + // Count how many argument separators the cursor has passed. + for (i, arg) in call.args.iter().enumerate().rev() { + if offset >= arg.start { + return i as u32; + } + } + + 0 +} + +// ─── Tests ────────────────────────────────────────────────────────────────────── + +#[cfg(test)] +mod tests { + use super::*; + use crate::analyzer::common::{AstFnDef, Type, TypeKind, VarDeclExpr}; + use std::sync::{Arc, Mutex}; + + #[test] + fn build_signature_info_simple() { + let fndef = AstFnDef { + fn_name: "add".into(), + return_type: Type::new(TypeKind::Int64), + ..AstFnDef::default() + }; + let info = build_signature_information(&fndef, "add"); + assert!(info.label.contains("fn add()"), "got: {}", info.label); + assert!(info.label.contains("i64"), "got: {}", info.label); + assert!(info.parameters.as_ref().unwrap().is_empty()); + } + + #[test] + fn build_signature_info_with_params() { + let param_a = Arc::new(Mutex::new(VarDeclExpr { + ident: "a".into(), + symbol_id: 0, + symbol_start: 0, + symbol_end: 0, + type_: Type::new(TypeKind::Int64), + be_capture: false, + heap_ident: None, + })); + let param_b = Arc::new(Mutex::new(VarDeclExpr { + ident: "b".into(), + symbol_id: 0, + symbol_start: 0, + symbol_end: 0, + type_: Type::new(TypeKind::String), + be_capture: false, + heap_ident: None, + })); + + let fndef = AstFnDef { + fn_name: "greet".into(), + params: vec![param_a, param_b], + return_type: Type::new(TypeKind::Void), + ..AstFnDef::default() + }; + let info = build_signature_information(&fndef, "greet"); + assert!( + info.label.contains("a: i64, b: string"), + "got: {}", + info.label + ); + let params = info.parameters.unwrap(); + assert_eq!(params.len(), 2); + } + + #[test] + fn build_signature_info_skips_self() { + let self_param = Arc::new(Mutex::new(VarDeclExpr { + ident: "self".into(), + symbol_id: 0, + symbol_start: 0, + symbol_end: 0, + type_: Type::default(), + be_capture: false, + heap_ident: None, + })); + let x_param = Arc::new(Mutex::new(VarDeclExpr { + ident: "x".into(), + symbol_id: 0, + symbol_start: 0, + symbol_end: 0, + type_: Type::new(TypeKind::Int64), + be_capture: false, + heap_ident: None, + })); + + let fndef = AstFnDef { + fn_name: "method".into(), + params: vec![self_param, x_param], + return_type: Type::new(TypeKind::Void), + ..AstFnDef::default() + }; + let info = build_signature_information(&fndef, "method"); + assert!(!info.label.contains("self"), "got: {}", info.label); + let params = info.parameters.unwrap(); + assert_eq!(params.len(), 1); + assert!(matches!(¶ms[0].label, ParameterLabel::Simple(s) if s.contains("x"))); + } + + #[test] + fn active_parameter_empty_args() { + let call = AstCall { + return_type: Type::default(), + left: Box::new(Expr::default()), + generics_args: vec![], + args: vec![], + spread: false, + }; + assert_eq!(compute_active_parameter(&call, 10), 0); + } + + #[test] + fn active_parameter_between_args() { + let arg0 = Box::new(Expr { + start: 5, + end: 6, + ..Expr::default() + }); + let arg1 = Box::new(Expr { + start: 8, + end: 10, + ..Expr::default() + }); + let call = AstCall { + return_type: Type::default(), + left: Box::new(Expr::default()), + generics_args: vec![], + args: vec![arg0, arg1], + spread: false, + }; + + // Cursor before first arg. + assert_eq!(compute_active_parameter(&call, 3), 0); + // Cursor at first arg. + assert_eq!(compute_active_parameter(&call, 5), 0); + // Cursor between args (after first, before second). + assert_eq!(compute_active_parameter(&call, 7), 0); + // Cursor at second arg. + assert_eq!(compute_active_parameter(&call, 8), 1); + } +} diff --git a/nls/src/server/symbols.rs b/nls/src/server/symbols.rs new file mode 100644 index 00000000..324d434d --- /dev/null +++ b/nls/src/server/symbols.rs @@ -0,0 +1,289 @@ +//! Document-symbol and workspace-symbol request handlers. + +use tower_lsp::lsp_types::*; + +use crate::analyzer::common::{AstNode, Stmt}; +use crate::analyzer::workspace_index::IndexedSymbolKind; +use crate::project::Project; +use crate::utils::offset_to_position; + +use super::Backend; + +// ─── Handler wiring ───────────────────────────────────────────────────────────── + +impl Backend { + pub(crate) async fn handle_document_symbol( + &self, + params: DocumentSymbolParams, + ) -> Option { + let file_path = params.text_document.uri.path(); + let project = self.get_file_project(file_path)?; + + let symbols = document_symbols(&project, file_path)?; + Some(DocumentSymbolResponse::Flat(symbols)) + } + + pub(crate) async fn handle_workspace_symbol( + &self, + params: WorkspaceSymbolParams, + ) -> Option> { + let query = ¶ms.query; + + let mut results: Vec = Vec::new(); + + for entry in self.projects.iter() { + let project = entry.value(); + collect_workspace_symbols(project, query, &mut results); + } + + if results.is_empty() { + return None; + } + + Some(results) + } +} + +// ─── Document symbols ─────────────────────────────────────────────────────────── + +/// Extract top-level symbols from a single module's AST. +fn document_symbols(project: &Project, file_path: &str) -> Option> { + let mh = project.module_handled.lock().ok()?; + let &idx = mh.get(file_path)?; + drop(mh); + + let db = project.module_db.lock().ok()?; + let module = db.get(idx)?; + + let uri = Url::from_file_path(&module.path).ok()?; + let symbols = symbols_from_stmts(&module.stmts, &module.rope, &uri); + + Some(symbols) +} + +/// Walk top-level statements and produce `SymbolInformation` entries. +fn symbols_from_stmts( + stmts: &[Box], + rope: &ropey::Rope, + uri: &Url, +) -> Vec { + let mut out = Vec::new(); + + for stmt in stmts { + if let Some(sym) = symbol_from_stmt(stmt, rope, uri) { + out.push(sym); + } + } + + out +} + +/// Convert a single top-level statement to a `SymbolInformation`, if applicable. +#[allow(deprecated)] // SymbolInformation::deprecated is deprecated but required by the type +fn symbol_from_stmt( + stmt: &Stmt, + rope: &ropey::Rope, + uri: &Url, +) -> Option { + let (name, kind, start, end) = match &stmt.node { + AstNode::FnDef(fndef_mutex) => { + let fndef = fndef_mutex.lock().unwrap(); + let name = if fndef.fn_name.is_empty() { + fndef.symbol_name.clone() + } else { + fndef.fn_name.clone() + }; + (name, SymbolKind::FUNCTION, fndef.symbol_start, fndef.symbol_end) + } + AstNode::Typedef(typedef_mutex) => { + let typedef = typedef_mutex.lock().unwrap(); + let kind = if typedef.is_interface { + SymbolKind::INTERFACE + } else if typedef.is_enum || typedef.is_tagged_union { + SymbolKind::ENUM + } else { + SymbolKind::STRUCT + }; + (typedef.ident.clone(), kind, typedef.symbol_start, typedef.symbol_end) + } + AstNode::VarDef(var_mutex, _) => { + let var = var_mutex.lock().unwrap(); + (var.ident.clone(), SymbolKind::VARIABLE, var.symbol_start, var.symbol_end) + } + AstNode::ConstDef(const_mutex) => { + let c = const_mutex.lock().unwrap(); + (c.ident.clone(), SymbolKind::CONSTANT, c.symbol_start, c.symbol_end) + } + _ => return None, + }; + + let start_pos = offset_to_position(start, rope)?; + let end_pos = offset_to_position(end, rope)?; + + Some(SymbolInformation { + name, + kind, + tags: None, + deprecated: None, + location: Location { + uri: uri.clone(), + range: Range { + start: start_pos, + end: end_pos, + }, + }, + container_name: None, + }) +} + +// ─── Workspace symbols ────────────────────────────────────────────────────────── + +/// Collect workspace symbols matching `query` from a project's index. +#[allow(deprecated)] +fn collect_workspace_symbols( + project: &Project, + query: &str, + out: &mut Vec, +) { + let ws = match project.workspace_index.lock() { + Ok(ws) => ws, + Err(_) => return, + }; + + let indexed = if query.is_empty() { + // Return all symbols (capped to avoid flooding the client). + ws.symbols + .values() + .flat_map(|v| v.iter()) + .take(500) + .collect::>() + } else { + ws.find_symbols_by_prefix_case_insensitive(query) + }; + + for sym in indexed { + let kind = match sym.kind { + IndexedSymbolKind::Type => SymbolKind::STRUCT, + IndexedSymbolKind::Function => SymbolKind::FUNCTION, + IndexedSymbolKind::Variable => SymbolKind::VARIABLE, + IndexedSymbolKind::Constant => SymbolKind::CONSTANT, + }; + + let uri = match Url::from_file_path(&sym.file_path) { + Ok(u) => u, + Err(_) => continue, + }; + + // WorkspaceIndex doesn't store line position — default to file start. + // The client will still jump to the file; go-to-def from there refines. + out.push(SymbolInformation { + name: sym.name.clone(), + kind, + tags: None, + deprecated: None, + location: Location { + uri, + range: Range { + start: Position::new(0, 0), + end: Position::new(0, 0), + }, + }, + container_name: None, + }); + } +} + +// ─── Tests ────────────────────────────────────────────────────────────────────── + +#[cfg(test)] +mod tests { + use super::*; + use crate::analyzer::common::{AstFnDef, Type, VarDeclExpr}; + use ropey::Rope; + use std::sync::{Arc, Mutex}; + + fn make_fndef_stmt(name: &str, start: usize, end: usize) -> Box { + let mut fndef = AstFnDef::default(); + fndef.fn_name = name.to_string(); + fndef.symbol_start = start; + fndef.symbol_end = end; + Box::new(Stmt { + start, + end, + node: AstNode::FnDef(Arc::new(Mutex::new(fndef))), + }) + } + + fn make_vardef_stmt(name: &str, start: usize, end: usize) -> Box { + let var = Arc::new(Mutex::new(VarDeclExpr { + ident: name.to_string(), + symbol_id: 0, + symbol_start: start, + symbol_end: end, + type_: Type::default(), + be_capture: false, + heap_ident: None, + })); + let right = Box::new(crate::analyzer::common::Expr { + start, + end, + type_: Type::default(), + target_type: Type::default(), + node: AstNode::None, + }); + Box::new(Stmt { + start, + end, + node: AstNode::VarDef(var, right), + }) + } + + #[test] + fn symbols_from_stmts_collects_fn_and_var() { + let source = "fn hello() {}\nvar x = 42\n"; + let rope = Rope::from_str(source); + let uri = Url::parse("file:///test.n").unwrap(); + + let stmts = vec![ + make_fndef_stmt("hello", 3, 8), + make_vardef_stmt("x", 18, 19), + ]; + + let result = symbols_from_stmts(&stmts, &rope, &uri); + assert_eq!(result.len(), 2); + assert_eq!(result[0].name, "hello"); + assert_eq!(result[0].kind, SymbolKind::FUNCTION); + assert_eq!(result[1].name, "x"); + assert_eq!(result[1].kind, SymbolKind::VARIABLE); + } + + #[test] + fn symbols_from_stmts_skips_non_declarations() { + let source = "import 'math.n'\n"; + let rope = Rope::from_str(source); + let uri = Url::parse("file:///test.n").unwrap(); + + let import = crate::analyzer::common::ImportStmt::default(); + let stmts = vec![Box::new(Stmt { + start: 0, + end: 15, + node: AstNode::Import(import), + })]; + + let result = symbols_from_stmts(&stmts, &rope, &uri); + assert!(result.is_empty()); + } + + #[test] + fn indexed_kind_to_symbol_kind() { + assert_eq!( + match IndexedSymbolKind::Function { + IndexedSymbolKind::Type => SymbolKind::STRUCT, + IndexedSymbolKind::Function => SymbolKind::FUNCTION, + IndexedSymbolKind::Variable => SymbolKind::VARIABLE, + IndexedSymbolKind::Constant => SymbolKind::CONSTANT, + }, + SymbolKind::FUNCTION + ); + } +} diff --git a/nls/src/utils.rs b/nls/src/utils.rs index c25351b1..3ec5714b 100644 --- a/nls/src/utils.rs +++ b/nls/src/utils.rs @@ -1,29 +1,133 @@ +//! Position / offset conversion and identifier extraction helpers. +//! +//! All functions in this module are **pure** — they operate on a [`Rope`] or +//! `&str` and carry no other state, making them trivial to unit-test. + use crate::analyzer::common::AnalyzerError; -use crate::project::Module; use ropey::Rope; -use std::collections::hash_map::DefaultHasher; -use std::hash::{Hash, Hasher}; use tower_lsp::lsp_types::Position; +// ─── Offset ↔ Position ───────────────────────────────────────────────────────── + +/// Convert a char offset in a rope to an LSP `Position` (0-based line/col). +/// +/// Returns `None` if the offset is out of range. pub fn offset_to_position(offset: usize, rope: &Rope) -> Option { let line = rope.try_char_to_line(offset).ok()?; - let first_char_of_line = rope.try_line_to_char(line).ok()?; - let column = offset - first_char_of_line; + let line_start = rope.try_line_to_char(line).ok()?; + let column = offset - line_start; Some(Position::new(line as u32, column as u32)) } -pub fn position_to_offset(position: Position, rope: &Rope) -> Option { - let line_char_offset = rope.try_line_to_char(position.line as usize).ok()?; - let slice = rope.slice(0..line_char_offset + position.character as usize); +/// Convert an LSP `Position` to a *byte* offset in the rope. +/// +/// This matches the old behaviour used by the analyzer: the result is a byte +/// offset suitable for slicing into `source: String`. +pub fn position_to_byte_offset(position: Position, rope: &Rope) -> Option { + let line_char = rope.try_line_to_char(position.line as usize).ok()?; + let slice = rope.slice(0..line_char + position.character as usize); Some(slice.len_bytes()) } +/// Convert an LSP `Position` to a *char* offset in the rope. +/// +/// Returns `None` if the position is out of range. +pub fn position_to_char_offset(position: Position, rope: &Rope) -> Option { + let line_start = rope.try_line_to_char(position.line as usize).ok()?; + let offset = line_start + position.character as usize; + if offset > rope.len_chars() { + return None; + } + Some(offset) +} + +// ─── Identifier extraction ────────────────────────────────────────────────────── + +/// Returns `true` if `c` can appear in a Nature identifier. +pub fn is_ident_char(c: char) -> bool { + c.is_alphanumeric() || c == '_' +} + +/// Extract the word (identifier) at `offset` in a `&str`. +/// +/// Returns `(word, start, end)` or `None` when the cursor isn't on an ident. +pub fn extract_word_at_offset(text: &str, offset: usize) -> Option<(String, usize, usize)> { + let chars: Vec = text.chars().collect(); + if offset > chars.len() { + return None; + } + + let mut start = offset; + while start > 0 && is_ident_char(chars[start - 1]) { + start -= 1; + } + + let mut end = offset; + while end < chars.len() && is_ident_char(chars[end]) { + end += 1; + } + + if start == end { + return None; + } + + let word: String = chars[start..end].iter().collect(); + Some((word, start, end)) +} + +/// Like [`extract_word_at_offset`] but operates directly on a [`Rope`], +/// avoiding an O(n) `to_string()` copy. +pub fn extract_word_at_offset_rope(rope: &Rope, offset: usize) -> Option<(String, usize, usize)> { + let len = rope.len_chars(); + if offset > len { + return None; + } + + let mut start = offset; + while start > 0 && is_ident_char(rope.char(start - 1)) { + start -= 1; + } + + let mut end = offset; + while end < len && is_ident_char(rope.char(end)) { + end += 1; + } + + if start == end { + return None; + } + + let word: String = rope.slice(start..end).chars().collect(); + Some((word, start, end)) +} + +// ─── Diagnostic helpers ───────────────────────────────────────────────────────── + +/// Extract a symbol name from diagnostic messages like: +/// `"symbol 'foo' not found"` +/// `"type 'Bar' not found"` +/// `"ident 'qux' undeclared"` +pub fn extract_symbol_from_diagnostic(message: &str) -> Option { + let start = message.find('\'')?; + let end = message[start + 1..].find('\'')?; + let symbol = &message[start + 1..start + 1 + end]; + if !symbol.is_empty() + && (message.contains("not found") + || message.contains("undeclared") + || message.contains("not defined")) + { + Some(symbol.to_string()) + } else { + None + } +} + +// ─── Formatting helpers ───────────────────────────────────────────────────────── + pub fn format_global_ident(prefix: String, ident: String) -> String { - // 如果 prefix 为空,则直接返回 ident if prefix.is_empty() { return ident; } - format!("{prefix}.{ident}") } @@ -31,22 +135,19 @@ pub fn format_impl_ident(impl_ident: String, key: String) -> String { format!("{impl_ident}.{key}") } +/// Append a generics hash to an identifier: `"foo"` + `42` → `"foo#42"`. +/// +/// Panics if `hash` is `0`. If the ident already contains `#`, returns it +/// unchanged. pub fn format_generics_ident(ident: String, hash: u64) -> String { assert!(hash != 0, "hash must not be 0"); - if ident.contains('#') { return ident; } - - format!("{ident}#{}", hash) -} - -pub fn calculate_hash(t: &T) -> u64 { - let mut hasher = DefaultHasher::new(); - t.hash(&mut hasher); - hasher.finish() + format!("{ident}#{hash}") } +/// Round `n` up to the next multiple of `align`. pub fn align_up(n: u64, align: u64) -> u64 { if align == 0 { return n; @@ -54,9 +155,151 @@ pub fn align_up(n: u64, align: u64) -> u64 { (n + align - 1) & !(align - 1) } -pub fn errors_push(m: &mut Module, e: AnalyzerError) { - // if m.index == 16 { - // panic!("TODO analyzer error"); - // } +/// Push an analyzer error onto a module's error list. +pub fn errors_push(m: &mut crate::project::Module, e: AnalyzerError) { m.analyzer_errors.push(e); } + +// ─── Tests ────────────────────────────────────────────────────────────────────── + +#[cfg(test)] +mod tests { + use super::*; + + // ── offset_to_position ────────────────────────────────────────────── + + #[test] + fn offset_to_position_first_line() { + let rope = Rope::from_str("hello world"); + assert_eq!(offset_to_position(0, &rope), Some(Position::new(0, 0))); + assert_eq!(offset_to_position(5, &rope), Some(Position::new(0, 5))); + } + + #[test] + fn offset_to_position_multiline() { + let rope = Rope::from_str("abc\ndef\nghi"); + assert_eq!(offset_to_position(0, &rope), Some(Position::new(0, 0))); + assert_eq!(offset_to_position(4, &rope), Some(Position::new(1, 0))); + assert_eq!(offset_to_position(8, &rope), Some(Position::new(2, 0))); + } + + #[test] + fn offset_to_position_out_of_range() { + let rope = Rope::from_str("ab"); + assert_eq!(offset_to_position(999, &rope), None); + } + + // ── is_ident_char ─────────────────────────────────────────────────── + + #[test] + fn ident_chars() { + assert!(is_ident_char('a')); + assert!(is_ident_char('Z')); + assert!(is_ident_char('0')); + assert!(is_ident_char('_')); + assert!(!is_ident_char(' ')); + assert!(!is_ident_char('.')); + assert!(!is_ident_char('\n')); + } + + // ── extract_word_at_offset ────────────────────────────────────────── + + #[test] + fn word_at_offset_simple() { + let text = "fn hello_world() {}"; + let result = extract_word_at_offset(text, 3); + assert_eq!(result, Some(("hello_world".to_string(), 3, 14))); + } + + #[test] + fn word_at_offset_beginning() { + let text = "myVar = 42"; + let result = extract_word_at_offset(text, 0); + assert_eq!(result, Some(("myVar".to_string(), 0, 5))); + } + + #[test] + fn word_at_offset_out_of_range() { + assert_eq!(extract_word_at_offset("hi", 100), None); + } + + #[test] + fn word_at_offset_empty() { + assert_eq!(extract_word_at_offset("", 0), None); + } + + // ── extract_word_at_offset_rope ───────────────────────────────────── + + #[test] + fn rope_word_matches_str_word() { + let text = "fn hello_world() {}"; + let rope = Rope::from_str(text); + assert_eq!( + extract_word_at_offset(text, 5), + extract_word_at_offset_rope(&rope, 5) + ); + } + + #[test] + fn rope_word_multiline() { + let rope = Rope::from_str("var x = 10\nvar y = 20"); + let result = extract_word_at_offset_rope(&rope, 15); + assert_eq!(result, Some(("y".to_string(), 15, 16))); + } + + // ── extract_symbol_from_diagnostic ────────────────────────────────── + + #[test] + fn diagnostic_symbol_not_found() { + assert_eq!( + extract_symbol_from_diagnostic("symbol 'foo' not found"), + Some("foo".to_string()) + ); + } + + #[test] + fn diagnostic_type_not_found() { + assert_eq!( + extract_symbol_from_diagnostic("type 'MyStruct' not found"), + Some("MyStruct".to_string()) + ); + } + + #[test] + fn diagnostic_no_match() { + assert_eq!( + extract_symbol_from_diagnostic("syntax error: unexpected token"), + None + ); + } + + // ── format helpers ────────────────────────────────────────────────── + + #[test] + fn global_ident_with_prefix() { + assert_eq!(format_global_ident("pkg".into(), "func".into()), "pkg.func"); + } + + #[test] + fn global_ident_empty_prefix() { + assert_eq!(format_global_ident("".into(), "func".into()), "func"); + } + + #[test] + fn generics_ident() { + assert_eq!(format_generics_ident("foo".into(), 42), "foo#42"); + } + + #[test] + fn generics_ident_already_has_hash() { + assert_eq!(format_generics_ident("foo#7".into(), 42), "foo#7"); + } + + #[test] + fn align_up_basic() { + assert_eq!(align_up(5, 8), 8); + assert_eq!(align_up(8, 8), 8); + assert_eq!(align_up(0, 8), 0); + assert_eq!(align_up(5, 0), 5); + } +} diff --git a/nls/language-configuration.json b/nls/syntaxes/language-configuration.json similarity index 100% rename from nls/language-configuration.json rename to nls/syntaxes/language-configuration.json diff --git a/nls/syntaxes/n.tmLanguage.json b/nls/syntaxes/n.tmLanguage.json new file mode 100644 index 00000000..95cc37a4 --- /dev/null +++ b/nls/syntaxes/n.tmLanguage.json @@ -0,0 +1,1008 @@ +{ + "$schema": "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json", + "name": "Nature", + "scopeName": "source.n", + "patterns": [ + { + "include": "#comments" + }, + { + "include": "#strings" + }, + { + "include": "#import-statement" + }, + { + "include": "#type-declaration" + }, + { + "include": "#function-definition" + }, + { + "include": "#variable-declaration" + }, + { + "include": "#numbers" + }, + { + "include": "#constants" + }, + { + "include": "#annotation" + }, + { + "include": "#macro-ident" + }, + { + "include": "#new-expression" + }, + { + "include": "#keywords" + }, + { + "include": "#generic-container" + }, + { + "include": "#array-type" + }, + { + "include": "#builtin-types" + }, + { + "include": "#generic-types" + }, + { + "include": "#fn-type" + }, + { + "include": "#function-call" + }, + { + "include": "#method-call" + }, + { + "include": "#type-instantiation" + }, + { + "include": "#operators" + }, + { + "include": "#punctuation" + }, + { + "include": "#identifiers" + } + ], + "repository": { + "comments": { + "patterns": [ + { + "name": "comment.line.double-slash.n", + "match": "//.*$" + }, + { + "name": "comment.block.n", + "begin": "/\\*", + "end": "\\*/", + "patterns": [ + { + "name": "comment.block.n", + "match": "." + } + ] + } + ] + }, + "strings": { + "patterns": [ + { + "name": "string.quoted.double.n", + "begin": "\"", + "end": "\"", + "beginCaptures": { + "0": { + "name": "punctuation.definition.string.begin.n" + } + }, + "endCaptures": { + "0": { + "name": "punctuation.definition.string.end.n" + } + }, + "patterns": [ + { + "name": "constant.character.escape.n", + "match": "\\\\(?:[abfnrtv\\\\\"']|x[0-9a-fA-F]{2}|u[0-9a-fA-F]{4}|[0-7]{1,3})" + }, + { + "name": "constant.character.escape.n", + "match": "\\\\." + } + ] + }, + { + "name": "string.quoted.single.n", + "begin": "'", + "end": "'", + "beginCaptures": { + "0": { + "name": "punctuation.definition.string.begin.n" + } + }, + "endCaptures": { + "0": { + "name": "punctuation.definition.string.end.n" + } + }, + "patterns": [ + { + "name": "constant.character.escape.n", + "match": "\\\\(?:[abfnrtv\\\\\"']|x[0-9a-fA-F]{2}|u[0-9a-fA-F]{4}|[0-7]{1,3})" + }, + { + "name": "constant.character.escape.n", + "match": "\\\\." + } + ] + }, + { + "name": "string.quoted.backtick.n", + "begin": "`", + "end": "`", + "beginCaptures": { + "0": { + "name": "punctuation.definition.string.begin.n" + } + }, + "endCaptures": { + "0": { + "name": "punctuation.definition.string.end.n" + } + } + } + ] + }, + "import-statement": { + "patterns": [ + { + "comment": "import 'path.n'.{ident, ident as alias}", + "begin": "\\b(import)\\s+", + "beginCaptures": { + "1": { + "name": "keyword.control.n" + } + }, + "end": "$", + "patterns": [ + { + "include": "#comments" + }, + { + "include": "#strings" + }, + { + "name": "keyword.control.n", + "match": "\\b(as)\\b" + }, + { + "name": "keyword.operator.n", + "match": "\\*" + }, + { + "include": "#punctuation" + }, + { + "name": "variable.other.n", + "match": "\\b[a-zA-Z_][a-zA-Z0-9_]*\\b" + } + ] + } + ] + }, + "type-declaration": { + "patterns": [ + { + "comment": "type name:Interface1.Interface2 = ...", + "begin": "\\b(type)\\s+([a-zA-Z_][a-zA-Z0-9_]*)", + "beginCaptures": { + "1": { + "name": "keyword.other.n" + }, + "2": { + "name": "entity.name.type.n" + } + }, + "end": "(?==)|(?=\\{)|$", + "patterns": [ + { + "comment": "Generic params ", + "begin": "<", + "beginCaptures": { + "0": { + "name": "punctuation.definition.generic.begin.n" + } + }, + "end": ">", + "endCaptures": { + "0": { + "name": "punctuation.definition.generic.end.n" + } + }, + "patterns": [ + { + "include": "#generic-params" + } + ] + }, + { + "match": ":", + "name": "punctuation.separator.n" + }, + { + "match": "\\.", + "name": "punctuation.accessor.n" + }, + { + "include": "#builtin-types" + }, + { + "match": "\\b([a-zA-Z_][a-zA-Z0-9_]*)\\b", + "name": "entity.name.type.n" + } + ] + } + ] + }, + "generic-params": { + "patterns": [ + { + "comment": "Type parameter with constraint: T: int|float or T: Readable&Writable", + "begin": "\\b([a-zA-Z_][a-zA-Z0-9_]*)\\s*(:)", + "beginCaptures": { + "1": { + "name": "storage.type.n" + }, + "2": { + "name": "punctuation.separator.colon.n" + } + }, + "end": "(?=[,>])", + "patterns": [ + { + "include": "#builtin-types" + }, + { + "include": "#generic-types" + }, + { + "name": "keyword.operator.n", + "match": "[|&]" + }, + { + "name": "storage.type.n", + "match": "\\b[a-zA-Z_][a-zA-Z0-9_]*\\b" + } + ] + }, + { + "comment": "Type parameter without constraint: T", + "match": "\\b([a-zA-Z_][a-zA-Z0-9_]*)\\b", + "captures": { + "1": { + "name": "storage.type.n" + } + } + }, + { + "include": "#punctuation" + } + ] + }, + "function-definition": { + "patterns": [ + { + "comment": "Method: fn Type.name(... or fn Type.name(...", + "begin": "\\b(fn)\\s+([a-zA-Z_][a-zA-Z0-9_]*(?:<[^>]*>)?)\\s*\\.\\s*([a-zA-Z_][a-zA-Z0-9_]*)\\s*(\\()", + "beginCaptures": { + "1": { + "name": "storage.type.function.n" + }, + "2": { + "name": "entity.name.type.n" + }, + "3": { + "name": "entity.name.function.n" + }, + "4": { + "name": "punctuation.section.parens.begin.n" + } + }, + "end": "\\)", + "endCaptures": { + "0": { + "name": "punctuation.section.parens.end.n" + } + }, + "patterns": [ + { + "include": "#function-params" + } + ] + }, + { + "comment": "Function with generics: fn name(...)", + "begin": "\\b(fn)\\s+([a-zA-Z_][a-zA-Z0-9_]*)\\s*(<)", + "beginCaptures": { + "1": { + "name": "storage.type.function.n" + }, + "2": { + "name": "entity.name.function.n" + }, + "3": { + "name": "punctuation.definition.generic.begin.n" + } + }, + "end": "\\)", + "endCaptures": { + "0": { + "name": "punctuation.section.parens.end.n" + } + }, + "patterns": [ + { + "comment": "Generic params section between < and >", + "begin": "(?<=<)", + "end": ">", + "endCaptures": { + "0": { + "name": "punctuation.definition.generic.end.n" + } + }, + "patterns": [ + { + "include": "#generic-params" + } + ] + }, + { + "comment": "Function params section (...)", + "begin": "\\(", + "beginCaptures": { + "0": { + "name": "punctuation.section.parens.begin.n" + } + }, + "end": "(?=\\))", + "patterns": [ + { + "include": "#function-params" + } + ] + } + ] + }, + { + "comment": "Function without generics: fn name(", + "begin": "\\b(fn)\\s+([a-zA-Z_][a-zA-Z0-9_]*)\\s*(\\()", + "beginCaptures": { + "1": { + "name": "storage.type.function.n" + }, + "2": { + "name": "entity.name.function.n" + }, + "3": { + "name": "punctuation.section.parens.begin.n" + } + }, + "end": "\\)", + "endCaptures": { + "0": { + "name": "punctuation.section.parens.end.n" + } + }, + "patterns": [ + { + "include": "#function-params" + } + ] + }, + { + "comment": "Anonymous fn: fn(", + "begin": "\\b(fn)\\s*(\\()", + "beginCaptures": { + "1": { + "name": "storage.type.function.n" + }, + "2": { + "name": "punctuation.section.parens.begin.n" + } + }, + "end": "\\)", + "endCaptures": { + "0": { + "name": "punctuation.section.parens.end.n" + } + }, + "patterns": [ + { + "include": "#function-params" + } + ] + }, + { + "comment": "Partial fn declaration while typing: fn name (no parens yet). Colors the name as function immediately to avoid red→blue flicker.", + "match": "\\b(fn)\\s+([a-zA-Z_][a-zA-Z0-9_]*)", + "captures": { + "1": { + "name": "storage.type.function.n" + }, + "2": { + "name": "entity.name.function.n" + } + } + } + ] + }, + "function-params": { + "patterns": [ + { + "include": "#comments" + }, + { + "comment": "Parameter with type: type name or type ...name", + "match": "\\b([a-zA-Z_][a-zA-Z0-9_<>\\[\\]\\?\\*]*)\\s+(\\.\\.\\.)?([a-zA-Z_][a-zA-Z0-9_]*)\\b", + "captures": { + "1": { + "name": "storage.type.n" + }, + "2": { + "name": "keyword.operator.n" + }, + "3": { + "name": "variable.parameter.n" + } + } + }, + { + "include": "#builtin-types" + }, + { + "include": "#generic-types" + }, + { + "include": "#punctuation" + } + ] + }, + "variable-declaration": { + "patterns": [ + { + "comment": "var/let/const name = ...", + "match": "\\b(var|let|const)\\s+([a-zA-Z_][a-zA-Z0-9_]*)\\b", + "captures": { + "1": { + "name": "keyword.other.n" + }, + "2": { + "name": "variable.other.n" + } + } + } + ] + }, + "numbers": { + "patterns": [ + { + "name": "constant.numeric.hex.n", + "match": "\\b0[xX][0-9a-fA-F_]+\\b" + }, + { + "name": "constant.numeric.octal.n", + "match": "\\b0[oO][0-7_]+\\b" + }, + { + "name": "constant.numeric.binary.n", + "match": "\\b0[bB][01_]+\\b" + }, + { + "name": "constant.numeric.float.n", + "match": "\\b[0-9][0-9_]*\\.[0-9][0-9_]*([eE][+-]?[0-9_]+)?\\b" + }, + { + "name": "constant.numeric.integer.n", + "match": "\\b[0-9][0-9_]*\\b" + } + ] + }, + "constants": { + "patterns": [ + { + "name": "constant.language.boolean.true.n", + "match": "\\btrue\\b" + }, + { + "name": "constant.language.boolean.false.n", + "match": "\\bfalse\\b" + }, + { + "name": "constant.language.null.n", + "match": "\\bnull\\b" + } + ] + }, + "annotation": { + "patterns": [ + { + "comment": "#linkid, #where, #local, etc.", + "match": "(#[a-zA-Z_][a-zA-Z0-9_]*)\\b(.*)?$", + "captures": { + "1": { + "name": "entity.name.tag.n" + }, + "2": { + "name": "string.unquoted.n" + } + } + } + ] + }, + "macro-ident": { + "patterns": [ + { + "name": "entity.name.tag.n", + "match": "@[a-zA-Z_][a-zA-Z0-9_]*" + } + ] + }, + "keywords": { + "patterns": [ + { + "name": "keyword.control.n", + "match": "\\b(if|else|for|in|break|continue|return|match|select)\\b" + }, + { + "name": "keyword.control.n", + "match": "\\b(try|catch|throw)\\b" + }, + { + "name": "keyword.control.n", + "match": "\\b(go)\\b" + }, + { + "name": "storage.type.n", + "match": "\\b(fn|import|test)\\b" + }, + { + "name": "keyword.other.n", + "match": "\\b(var|let|const|type)\\b" + }, + { + "comment": "struct/enum/union keywords", + "name": "keyword.control.n", + "match": "\\b(struct|enum|union)\\b" + }, + { + "comment": "interface keyword (default foreground/white)", + "name": "meta.interface.n", + "match": "\\b(interface)\\b" + }, + { + "name": "keyword.operator.n", + "match": "\\b(sizeof|reflect_hash)\\b" + }, + { + "name": "keyword.control.n", + "match": "\\b(is|as)\\b" + }, + { + "name": "keyword.control.n", + "match": "\\b(new)\\b" + }, + { + "name": "variable.language.n", + "match": "\\b(self)\\b" + } + ] + }, + "builtin-types": { + "patterns": [ + { + "name": "storage.type.builtin.n", + "match": "\\b(int|uint|float|bool|string|void|any|anyptr|chan|u8|u16|u32|u64|i8|i16|i32|i64|f32|f64)\\b" + }, + { + "comment": "ptr and ref as standalone (without <)", + "name": "keyword.control.n", + "match": "\\b(ptr|ref)\\b" + }, + { + "comment": "vec/map/set as standalone (without <)", + "name": "storage.type.builtin.n", + "match": "\\b(vec|map|set)\\b" + } + ] + }, + "generic-types": { + "patterns": [ + { + "comment": "PascalCase identifiers or single uppercase letters used as types (e.g. T, Point, Color, MyStruct) — excludes multi-char ALL_CAPS constants like PI, MAX_SIZE", + "name": "entity.name.type.n", + "match": "\\b(?:[A-Z]|[A-Z][a-zA-Z0-9_]*[a-z][a-zA-Z0-9_]*)\\b" + } + ] + }, + "function-call": { + "patterns": [ + { + "comment": "name( or name(", + "match": "\\b([a-zA-Z_][a-zA-Z0-9_]*)\\s*(?=\\(|<[^<>]*>\\s*\\()", + "captures": { + "1": { + "name": "entity.name.function.n" + } + } + } + ] + }, + "new-expression": { + "patterns": [ + { + "comment": "new Type(args) — heap allocation with named arguments", + "begin": "\\b(new)\\s+([a-zA-Z_][a-zA-Z0-9_]*)\\s*(\\()", + "beginCaptures": { + "1": { + "name": "keyword.control.n" + }, + "2": { + "name": "entity.name.type.n" + }, + "3": { + "name": "punctuation.section.parens.begin.n" + } + }, + "end": "\\)", + "endCaptures": { + "0": { + "name": "punctuation.section.parens.end.n" + } + }, + "patterns": [ + { + "include": "#comments" + }, + { + "comment": "Named argument: name = value", + "match": "\\b([a-zA-Z_][a-zA-Z0-9_]*)\\s*(?=\\s*=(?!=))", + "captures": { + "1": { + "name": "variable.parameter.n" + } + } + }, + { + "include": "#strings" + }, + { + "include": "#numbers" + }, + { + "include": "#constants" + }, + { + "include": "#builtin-types" + }, + { + "include": "#generic-types" + }, + { + "include": "#operators" + }, + { + "include": "#punctuation" + }, + { + "include": "#identifiers" + } + ] + } + ] + }, + "type-instantiation": { + "patterns": [ + { + "comment": "Type instantiation: name{ — struct literal (after = or , or ( or [, but not keywords)", + "match": "(?<==|,|\\(|\\[)\\s*\\b(?!struct\\b|enum\\b|union\\b|interface\\b|if\\b|else\\b|for\\b|fn\\b|match\\b|select\\b)([a-zA-Z_][a-zA-Z0-9_]*)\\s*(?=\\{)", + "captures": { + "1": { + "name": "entity.name.type.n" + } + } + } + ] + }, + "array-type": { + "patterns": [ + { + "comment": "Array type: [type] — colors the type inside brackets", + "begin": "\\[(?=[a-zA-Z_])", + "beginCaptures": { + "0": { + "name": "punctuation.section.brackets.begin.n" + } + }, + "end": "\\]", + "endCaptures": { + "0": { + "name": "punctuation.section.brackets.end.n" + } + }, + "patterns": [ + { + "include": "#builtin-types" + }, + { + "include": "#generic-types" + }, + { + "name": "storage.type.n", + "match": "\\b[a-zA-Z_][a-zA-Z0-9_]*\\b" + } + ] + } + ] + }, + "generic-container": { + "patterns": [ + { + "comment": "ptr and ref — pointer/reference types (yellow)", + "begin": "\\b(ptr|ref)\\s*(<)", + "beginCaptures": { + "1": { + "name": "keyword.control.n" + }, + "2": { + "name": "punctuation.definition.generic.begin.n" + } + }, + "end": ">", + "endCaptures": { + "0": { + "name": "punctuation.definition.generic.end.n" + } + }, + "patterns": [ + { + "include": "#generic-container" + }, + { + "include": "#builtin-types" + }, + { + "include": "#generic-types" + }, + { + "name": "storage.type.n", + "match": "\\b[a-zA-Z_][a-zA-Z0-9_]*\\b" + }, + { + "include": "#punctuation" + } + ] + }, + { + "comment": "vec, map, set — container types (purple)", + "begin": "\\b(vec|map|set)\\s*(<)", + "beginCaptures": { + "1": { + "name": "storage.type.builtin.n" + }, + "2": { + "name": "punctuation.definition.generic.begin.n" + } + }, + "end": ">", + "endCaptures": { + "0": { + "name": "punctuation.definition.generic.end.n" + } + }, + "patterns": [ + { + "include": "#generic-container" + }, + { + "include": "#builtin-types" + }, + { + "include": "#generic-types" + }, + { + "name": "storage.type.n", + "match": "\\b[a-zA-Z_][a-zA-Z0-9_]*\\b" + }, + { + "include": "#punctuation" + } + ] + } + ] + }, + "fn-type": { + "patterns": [ + { + "comment": "Function type: fn(type, type):returntype", + "begin": "\\b(fn)\\s*(\\()", + "beginCaptures": { + "1": { + "name": "storage.type.function.n" + }, + "2": { + "name": "punctuation.section.parens.begin.n" + } + }, + "end": "\\)", + "endCaptures": { + "0": { + "name": "punctuation.section.parens.end.n" + } + }, + "patterns": [ + { + "include": "#builtin-types" + }, + { + "include": "#generic-types" + }, + { + "name": "storage.type.n", + "match": "\\b[a-zA-Z_][a-zA-Z0-9_]*\\b" + }, + { + "include": "#punctuation" + } + ] + } + ] + }, + "method-call": { + "patterns": [ + { + "comment": ".method(", + "match": "\\.\\s*([a-zA-Z_][a-zA-Z0-9_]*)\\s*(?=\\()", + "captures": { + "1": { + "name": "entity.name.function.n" + } + } + }, + { + "comment": ".property access", + "match": "\\.\\s*([a-zA-Z_][a-zA-Z0-9_]*)\\b(?!\\s*\\()", + "captures": { + "1": { + "name": "variable.other.n" + } + } + } + ] + }, + "operators": { + "patterns": [ + { + "name": "keyword.operator.n", + "match": "\\.\\.\\." + }, + { + "name": "keyword.operator.n", + "match": "\\.\\." + }, + { + "name": "keyword.operator.n", + "match": "->" + }, + { + "name": "keyword.operator.n", + "match": "\\+=|-=|\\*=|/=|%=|&=|\\|=|\\^=|<<=|>>=" + }, + { + "name": "keyword.operator.n", + "match": "==|!=|<=|>=" + }, + { + "name": "keyword.operator.n", + "match": "&&|\\|\\|" + }, + { + "name": "keyword.operator.n", + "match": "<<|>>" + }, + { + "name": "keyword.operator.n", + "match": "=" + }, + { + "name": "keyword.operator.n", + "match": "!" + }, + { + "name": "keyword.operator.n", + "match": "[&|^~]" + }, + { + "name": "keyword.operator.n", + "match": "[<>]" + }, + { + "name": "keyword.operator.n", + "match": "[+\\-*/%]" + } + ] + }, + "punctuation": { + "patterns": [ + { + "name": "punctuation.separator.comma.n", + "match": "," + }, + { + "name": "punctuation.terminator.semicolon.n", + "match": ";" + }, + { + "name": "punctuation.separator.colon.n", + "match": ":" + }, + { + "name": "punctuation.accessor.dot.n", + "match": "\\." + }, + { + "name": "punctuation.section.braces.begin.n", + "match": "\\{" + }, + { + "name": "punctuation.section.braces.end.n", + "match": "\\}" + }, + { + "name": "punctuation.section.parens.begin.n", + "match": "\\(" + }, + { + "name": "punctuation.section.parens.end.n", + "match": "\\)" + }, + { + "name": "punctuation.section.brackets.begin.n", + "match": "\\[" + }, + { + "name": "punctuation.section.brackets.end.n", + "match": "\\]" + }, + { + "name": "punctuation.definition.generic.begin.n", + "match": "<" + }, + { + "name": "punctuation.definition.generic.end.n", + "match": ">" + }, + { + "name": "punctuation.nullable.n", + "match": "\\?" + } + ] + }, + "identifiers": { + "patterns": [ + { + "name": "variable.other.n", + "match": "\\b[a-zA-Z_][a-zA-Z0-9_]*\\b" + } + ] + } + } +} \ No newline at end of file diff --git a/nls/tests/analyzer_test.rs b/nls/tests/analyzer_test.rs index 263bb468..d4bbfb96 100644 --- a/nls/tests/analyzer_test.rs +++ b/nls/tests/analyzer_test.rs @@ -1,6 +1,21 @@ use log::debug; +use nls::analyzer::module_unique_ident; use nls::project::Project; use ropey::Rope; +use std::fs; +use std::path::PathBuf; +use std::time::{SystemTime, UNIX_EPOCH}; + +/// Create a unique temp directory for a test case. +fn temp_project(name: &str) -> PathBuf { + let nanos = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos(); + let dir = std::env::temp_dir().join(format!("nls_analyzer_test_{}_{}", name, nanos)); + fs::create_dir_all(&dir).unwrap(); + dir +} #[test] fn test_rope() { @@ -14,45 +29,43 @@ fn test_rope() { #[tokio::test] async fn test_project() { - // Initialize logger with error handling let _ = env_logger::builder().filter_level(log::LevelFilter::Debug).try_init(); - debug!("start test"); - let project_root = "/Users/weiwenhao/Code/nature-test"; + let root = temp_project("test_project"); + let file = root.join("main.n"); + let code = "fn main() {}\n"; + fs::write(&file, code).unwrap(); - let mut project = Project::new(project_root.to_string()).await; - project.backend_handle_queue(); + let mut project = Project::new(root.to_string_lossy().to_string()).await; + project.start_queue_worker(); - let module_ident = "nature-test.main"; - let file_path = "/Users/weiwenhao/Code/nature-test/main.n"; - - // 使用 None 自动从文件读取内容进行编译 - let module_index = project.build(&file_path, &module_ident, None).await; + let module_ident = module_unique_ident(&project.root, &file.to_string_lossy()); + let module_index = project + .build(&file.to_string_lossy(), &module_ident, Some(code.to_string())) + .await; dbg!(module_index); - let mut module_db = project.module_db.lock().unwrap(); - let m = &mut module_db[module_index]; + let module_db = project.module_db.lock().unwrap(); + let m = &module_db[module_index]; println!("errors: {:?}", &m.analyzer_errors); - drop(module_db); + assert!(m.analyzer_errors.is_empty(), "empty main should have no errors"); } #[tokio::test] async fn test_stage() { - // Initialize logger with error handling let _ = env_logger::builder().filter_level(log::LevelFilter::Debug).try_init(); - debug!("start test"); - let project_root = "/Users/weiwenhao/Code/nature-test"; + let root = temp_project("test_stage"); + let file = root.join("main.n"); - let mut project = Project::new(project_root.to_string()).await; - project.backend_handle_queue(); + let mut project = Project::new(root.to_string_lossy().to_string()).await; + project.start_queue_worker(); - let module_ident = "nature-test.main"; - let file_path = "/Users/weiwenhao/Code/nature-test/main.n"; + let module_ident = module_unique_ident(&project.root, &file.to_string_lossy()); - // 定义阶段测试数据 + // Stage test data — each phase is built sequentially. let test_codes = vec![ r#" type addr1_t = struct{ @@ -76,20 +89,20 @@ fn main() { "#, ]; - // 将 first phase 写入到 file path 中。 - std::fs::write(&file_path, test_codes[0].as_bytes()).expect("Failed to write first phase to file"); + // Write the first phase to disk so the build can read it. + fs::write(&file, test_codes[0].as_bytes()).expect("Failed to write first phase to file"); - // 循环执行各个阶段的测试 for (index, code) in test_codes.iter().enumerate() { let phase_name = format!("Phase {}", index + 1); - println!("{} build:", phase_name); + println!("{phase_name} build:"); - let module_index = project.build(&file_path, &module_ident, Some(code.to_string())).await; + let module_index = project + .build(&file.to_string_lossy(), &module_ident, Some(code.to_string())) + .await; dbg!(module_index); - let mut module_db = project.module_db.lock().unwrap(); - let m = &mut module_db[module_index]; - println!("{} errors: {:?}", phase_name, &m.analyzer_errors); - drop(module_db); + let module_db = project.module_db.lock().unwrap(); + let m = &module_db[module_index]; + println!("{phase_name} errors: {:?}", &m.analyzer_errors); } } diff --git a/nls/tests/build_pipeline_test.rs b/nls/tests/build_pipeline_test.rs new file mode 100644 index 00000000..8feb1454 --- /dev/null +++ b/nls/tests/build_pipeline_test.rs @@ -0,0 +1,787 @@ +//! Regression tests for the build pipeline. +//! +//! Each test creates a temp directory with `.n` source files, runs the full +//! `Project::build` pipeline, and asserts on concrete outcomes (error count, +//! error messages, module count, etc.). If the analyzer, lexer, parser, or +//! type system changes behaviour, these tests will fail. + +use nls::analyzer::module_unique_ident; +use nls::project::Project; +use std::fs; +use std::path::PathBuf; +use std::time::{SystemTime, UNIX_EPOCH}; + +/// Create a unique temp directory for a test case. +fn temp_project(name: &str) -> PathBuf { + let nanos = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos(); + let dir = std::env::temp_dir().join(format!("nls_test_{}_{}", name, nanos)); + fs::create_dir_all(&dir).unwrap(); + dir +} + +/// Helper: build a single-file project and return (module_index, errors). +async fn build_single_file( + name: &str, + code: &str, +) -> (usize, Vec) { + let root = temp_project(name); + let file = root.join("main.n"); + fs::write(&file, code).unwrap(); + + let mut project = Project::new(root.to_string_lossy().to_string()).await; + let ident = module_unique_ident(&project.root, &file.to_string_lossy()); + let idx = project + .build(&file.to_string_lossy(), &ident, Some(code.to_string())) + .await; + + let db = project.module_db.lock().unwrap(); + let errors = db[idx].analyzer_errors.clone(); + (idx, errors) +} + +// ─── Valid programs should produce zero errors ────────────────────────────────── + +#[tokio::test] +async fn valid_empty_main() { + let (_, errors) = build_single_file( + "empty_main", + r#" +fn main() { +} +"#, + ) + .await; + assert!( + errors.is_empty(), + "expected no errors for empty main, got: {:?}", + errors.iter().map(|e| &e.message).collect::>() + ); +} + +#[tokio::test] +async fn valid_variable_declaration() { + let (_, errors) = build_single_file( + "var_decl", + r#" +fn main() { + int x = 42 + var y = 10 +} +"#, + ) + .await; + assert!( + errors.is_empty(), + "expected no errors, got: {:?}", + errors.iter().map(|e| &e.message).collect::>() + ); +} + +#[tokio::test] +async fn valid_function_with_return() { + let (_, errors) = build_single_file( + "fn_return", + r#" +fn add(int a, int b):int { + return a + b +} + +fn main() { + int result = add(1, 2) +} +"#, + ) + .await; + assert!( + errors.is_empty(), + "expected no errors, got: {:?}", + errors.iter().map(|e| &e.message).collect::>() + ); +} + +#[tokio::test] +async fn valid_struct_definition() { + let (_, errors) = build_single_file( + "struct_def", + r#" +type point = struct { + int x + int y +} + +fn main() { + var p = point{x: 1, y: 2} +} +"#, + ) + .await; + assert!( + errors.is_empty(), + "expected no errors, got: {:?}", + errors.iter().map(|e| &e.message).collect::>() + ); +} + +#[tokio::test] +async fn valid_if_else() { + let (_, errors) = build_single_file( + "if_else", + r#" +fn main() { + int x = 10 + if x > 5 { + var y = 1 + } else { + var z = 2 + } +} +"#, + ) + .await; + assert!( + errors.is_empty(), + "expected no errors, got: {:?}", + errors.iter().map(|e| &e.message).collect::>() + ); +} + +#[tokio::test] +async fn valid_for_loop() { + let (_, errors) = build_single_file( + "for_loop", + r#" +fn main() { + for int i = 0; i < 10; i += 1 { + var x = i + } +} +"#, + ) + .await; + assert!( + errors.is_empty(), + "expected no errors, got: {:?}", + errors.iter().map(|e| &e.message).collect::>() + ); +} + +// ─── Invalid programs should produce specific errors ──────────────────────────── + +#[tokio::test] +async fn error_global_non_empty_string() { + let (_, errors) = build_single_file( + "global_str_err", + r#" +string s = 'hello world' + +fn main() { +} +"#, + ) + .await; + assert!( + !errors.is_empty(), + "expected errors for non-empty global string initializer" + ); + let has_global_str_error = errors + .iter() + .any(|e| e.message.contains("global string initializer must be empty")); + assert!( + has_global_str_error, + "expected 'global string initializer must be empty' error, got: {:?}", + errors.iter().map(|e| &e.message).collect::>() + ); +} + +#[tokio::test] +async fn error_undeclared_variable() { + let (_, errors) = build_single_file( + "undeclared_var", + r#" +fn main() { + int x = undefined_var +} +"#, + ) + .await; + assert!( + !errors.is_empty(), + "expected error for undeclared variable" + ); + // The analyzer should report *something* about an undefined symbol. + let has_relevant_error = errors.iter().any(|e| { + e.message.contains("not found") + || e.message.contains("undeclared") + || e.message.contains("undefined") + || e.message.contains("not defined") + }); + assert!( + has_relevant_error, + "expected an 'undefined/not found' error, got: {:?}", + errors.iter().map(|e| &e.message).collect::>() + ); +} + +#[tokio::test] +async fn error_type_mismatch() { + let (_, errors) = build_single_file( + "type_mismatch", + r#" +fn main() { + int x = 'hello' +} +"#, + ) + .await; + assert!( + !errors.is_empty(), + "expected type mismatch error" + ); +} + +#[tokio::test] +async fn error_missing_return() { + let (_, errors) = build_single_file( + "missing_return", + r#" +fn foo():int { + var x = 42 +} + +fn main() { +} +"#, + ) + .await; + assert!( + !errors.is_empty(), + "expected missing return error" + ); + let has_return_error = errors + .iter() + .any(|e| e.message.contains("return")); + assert!( + has_return_error, + "expected a return-related error, got: {:?}", + errors.iter().map(|e| &e.message).collect::>() + ); +} + +// ─── Build pipeline structure ─────────────────────────────────────────────────── + +#[tokio::test] +async fn build_creates_module_entry() { + let root = temp_project("module_entry"); + let file = root.join("main.n"); + fs::write(&file, "fn main() {}").unwrap(); + + let mut project = Project::new(root.to_string_lossy().to_string()).await; + let ident = module_unique_ident(&project.root, &file.to_string_lossy()); + let idx = project + .build(&file.to_string_lossy(), &ident, Some("fn main() {}".into())) + .await; + + // Module should be registered in the handled map. + let handled = project.module_handled.lock().unwrap(); + assert!(handled.contains_key(&*file.to_string_lossy())); + + // Module DB should contain it. + let db = project.module_db.lock().unwrap(); + assert!(idx < db.len()); + assert_eq!(db[idx].path, file.to_string_lossy().to_string()); +} + +#[tokio::test] +async fn rebuild_updates_source() { + let root = temp_project("rebuild"); + let file = root.join("main.n"); + let code_v1 = "fn main() { int x = 1 }"; + let code_v2 = "fn main() { int x = 2 }"; + fs::write(&file, code_v1).unwrap(); + + let mut project = Project::new(root.to_string_lossy().to_string()).await; + let ident = module_unique_ident(&project.root, &file.to_string_lossy()); + + // First build. + let idx = project + .build(&file.to_string_lossy(), &ident, Some(code_v1.into())) + .await; + { + let db = project.module_db.lock().unwrap(); + assert_eq!(db[idx].source, code_v1); + } + + // Rebuild with new content. + let idx2 = project + .build(&file.to_string_lossy(), &ident, Some(code_v2.into())) + .await; + { + let db = project.module_db.lock().unwrap(); + assert_eq!(db[idx2].source, code_v2); + } + assert_eq!(idx, idx2, "rebuild should reuse the same module index"); +} + +#[tokio::test] +async fn lexer_produces_tokens() { + let root = temp_project("tokens"); + let file = root.join("main.n"); + let code = "fn main() { int x = 42 }"; + fs::write(&file, code).unwrap(); + + let mut project = Project::new(root.to_string_lossy().to_string()).await; + let ident = module_unique_ident(&project.root, &file.to_string_lossy()); + let idx = project + .build(&file.to_string_lossy(), &ident, Some(code.into())) + .await; + + let db = project.module_db.lock().unwrap(); + let m = &db[idx]; + // The lexer should have produced tokens. + assert!( + !m.token_db.is_empty(), + "expected lexer to produce tokens" + ); + // The parser should have produced statements. + assert!( + !m.stmts.is_empty(), + "expected parser to produce statements" + ); +} + +// ─── Diagnostics builder ──────────────────────────────────────────────────────── + +#[tokio::test] +async fn build_diagnostics_deduplicates() { + use nls::analyzer::common::AnalyzerError; + use nls::project::Module; + + let mut m = Module::default(); + m.source = "fn main() {}".into(); + m.rope = ropey::Rope::from_str(&m.source); + + // Push two identical errors at the same position. + m.analyzer_errors.push(AnalyzerError { + start: 0, + end: 2, + message: "duplicate error".into(), + is_warning: false, + }); + m.analyzer_errors.push(AnalyzerError { + start: 0, + end: 2, + message: "duplicate error".into(), + is_warning: false, + }); + + let diagnostics = nls::server::Backend::build_diagnostics(&m); + assert_eq!( + diagnostics.len(), + 1, + "duplicate errors at the same position should be deduplicated" + ); +} + +#[tokio::test] +async fn build_diagnostics_warning_has_unnecessary_tag() { + use nls::analyzer::common::AnalyzerError; + use nls::project::Module; + use tower_lsp::lsp_types::DiagnosticTag; + + let mut m = Module::default(); + m.source = "fn main() {}".into(); + m.rope = ropey::Rope::from_str(&m.source); + m.analyzer_errors.push(AnalyzerError { + start: 0, + end: 2, + message: "unused variable".into(), + is_warning: true, + }); + + let diagnostics = nls::server::Backend::build_diagnostics(&m); + assert_eq!(diagnostics.len(), 1); + let tags = diagnostics[0].tags.as_ref().unwrap(); + assert!( + tags.contains(&DiagnosticTag::UNNECESSARY), + "warning diagnostics should have the UNNECESSARY tag" + ); +} + +#[tokio::test] +async fn build_diagnostics_skips_zero_end() { + use nls::analyzer::common::AnalyzerError; + use nls::project::Module; + + let mut m = Module::default(); + m.source = "fn main() {}".into(); + m.rope = ropey::Rope::from_str(&m.source); + m.analyzer_errors.push(AnalyzerError { + start: 0, + end: 0, // end == 0 should be filtered out + message: "internal error".into(), + is_warning: false, + }); + + let diagnostics = nls::server::Backend::build_diagnostics(&m); + assert!( + diagnostics.is_empty(), + "errors with end == 0 should be skipped" + ); +} + +// ─── Module helpers ───────────────────────────────────────────────────────────── + +#[test] +fn module_new_sets_dir() { + use nls::project::Module; + + let m = Module::new( + "test".into(), + "fn main() {}".into(), + "/home/user/project/main.n".into(), + 0, + 0, + ); + assert_eq!(m.dir, "/home/user/project"); + assert_eq!(m.path, "/home/user/project/main.n"); + assert_eq!(m.ident, "test"); + assert_eq!(m.source, "fn main() {}"); +} + +#[test] +fn module_default_is_empty() { + use nls::project::Module; + + let m = Module::default(); + assert!(m.source.is_empty()); + assert!(m.path.is_empty()); + assert!(m.token_db.is_empty()); + assert!(m.stmts.is_empty()); + assert!(m.analyzer_errors.is_empty()); + assert!(m.references.is_empty()); + assert!(m.dependencies.is_empty()); +} + +// ─── module_unique_ident ──────────────────────────────────────────────────────── + +#[test] +fn module_unique_ident_strips_root() { + let ident = module_unique_ident("/home/user/project", "/home/user/project/main.n"); + assert!( + !ident.is_empty(), + "module_unique_ident should produce a non-empty ident" + ); + assert!( + !ident.contains("/home/user/project/"), + "ident should not contain the full root path" + ); +} + +// ─── Diagnostic position & message regression ─────────────────────────────────── + +/// Verify that diagnostics for a known error have the correct severity. +#[tokio::test] +async fn diagnostic_severity_error_for_type_mismatch() { + use tower_lsp::lsp_types::DiagnosticSeverity; + + let (_, errors) = build_single_file( + "sev_type_mismatch", + r#" +fn main() { + int x = 'hello' +} +"#, + ) + .await; + assert!(!errors.is_empty(), "should have errors"); + // All type mismatch errors must be non-warnings (errors). + for e in &errors { + assert!(!e.is_warning, "type mismatch should be an error, not warning"); + } + + // Convert to LSP diagnostics and check severity. + let mut m = nls::project::Module::default(); + m.source = "fn main() {\n int x = 'hello'\n}\n".into(); + m.rope = ropey::Rope::from_str(&m.source); + m.analyzer_errors = errors; + let diagnostics = nls::server::Backend::build_diagnostics(&m); + for d in &diagnostics { + assert_eq!( + d.severity, + Some(DiagnosticSeverity::ERROR), + "type mismatch diagnostic should be ERROR severity" + ); + } +} + +/// Diagnostics should have non-zero ranges (start != end). +#[tokio::test] +async fn diagnostic_ranges_are_non_empty() { + let (_, errors) = build_single_file( + "ranges_nonempty", + r#" +fn main() { + int x = undefined_var +} +"#, + ) + .await; + assert!(!errors.is_empty()); + + let mut m = nls::project::Module::default(); + m.source = "fn main() {\n int x = undefined_var\n}\n".into(); + m.rope = ropey::Rope::from_str(&m.source); + m.analyzer_errors = errors; + let diagnostics = nls::server::Backend::build_diagnostics(&m); + assert!(!diagnostics.is_empty(), "should produce diagnostics"); + for d in &diagnostics { + assert_ne!( + d.range.start, d.range.end, + "diagnostic range should not be zero-width: {:?}", + d + ); + } +} + +/// Multiple distinct errors should each appear in the diagnostics list. +#[tokio::test] +async fn diagnostic_multiple_errors_are_preserved() { + let (_, errors) = build_single_file( + "multi_errors", + r#" +fn main() { + int x = undef_a + int y = undef_b +} +"#, + ) + .await; + // Should have at least two errors. + assert!( + errors.len() >= 2, + "expected at least 2 errors, got {}", + errors.len() + ); +} + +/// A valid program must produce zero diagnostics through the full pipeline. +#[tokio::test] +async fn diagnostic_zero_for_valid_program() { + let (idx, errors) = build_single_file( + "zero_diag", + r#" +fn main() { + int x = 42 + int y = x + 1 +} +"#, + ) + .await; + assert!(errors.is_empty(), "valid program should produce zero errors"); + + // Also verify via build_diagnostics. + let mut m = nls::project::Module::default(); + m.source = "fn main() {\n int x = 42\n int y = x + 1\n}\n".into(); + m.rope = ropey::Rope::from_str(&m.source); + m.analyzer_errors = errors; + let diagnostics = nls::server::Backend::build_diagnostics(&m); + assert!(diagnostics.is_empty(), "diagnostics should be empty for valid code"); +} + +// ─── Additional language feature regression ───────────────────────────────────── + +/// Generic type definition and usage should compile clean. +#[tokio::test] +async fn valid_generic_type() { + let (_, errors) = build_single_file( + "generic", + r#" +type box = struct { + t0 value +} + +fn main() { + var b = box{value: 42} +} +"#, + ) + .await; + assert!( + errors.is_empty(), + "generic type usage should produce no errors, got: {:?}", + errors.iter().map(|e| &e.message).collect::>() + ); +} + +/// Array operations should work. +#[tokio::test] +async fn valid_array() { + let (_, errors) = build_single_file( + "array", + r#" +fn main() { + var list = [1, 2, 3] + var first = list[0] +} +"#, + ) + .await; + assert!( + errors.is_empty(), + "array usage should produce no errors, got: {:?}", + errors.iter().map(|e| &e.message).collect::>() + ); +} + +/// Map literal should work. +#[tokio::test] +async fn valid_map() { + let (_, errors) = build_single_file( + "map", + r#" +fn main() { + var m = {1: 'hello', 2: 'world'} +} +"#, + ) + .await; + assert!( + errors.is_empty(), + "map usage should produce no errors, got: {:?}", + errors.iter().map(|e| &e.message).collect::>() + ); +} + +/// Tuple definition and access should work. +#[tokio::test] +async fn valid_tuple() { + let (_, errors) = build_single_file( + "tuple", + r#" +fn main() { + var t = (1, 'hello', true) +} +"#, + ) + .await; + assert!( + errors.is_empty(), + "tuple usage should produce no errors, got: {:?}", + errors.iter().map(|e| &e.message).collect::>() + ); +} + +/// Error handling with try-catch should analyze cleanly. +#[tokio::test] +async fn valid_error_handling() { + let (_, errors) = build_single_file( + "error_handling", + r#" +fn risky():int! { + return 42 +} + +fn main() { + var result = risky() catch err { + return + } +} +"#, + ) + .await; + assert!( + errors.is_empty(), + "error handling should produce no errors, got: {:?}", + errors.iter().map(|e| &e.message).collect::>() + ); +} + +/// Closure / anonymous function should analyze cleanly. +#[tokio::test] +async fn valid_closure() { + let (_, errors) = build_single_file( + "closure", + r#" +fn main() { + var add = fn(int a, int b):int { + return a + b + } + var result = add(1, 2) +} +"#, + ) + .await; + assert!( + errors.is_empty(), + "closure should produce no errors, got: {:?}", + errors.iter().map(|e| &e.message).collect::>() + ); +} + +/// Type alias should work. +#[tokio::test] +async fn valid_type_alias() { + let (_, errors) = build_single_file( + "type_alias", + r#" +type num = int + +fn main() { + num x = 42 +} +"#, + ) + .await; + assert!( + errors.is_empty(), + "type alias should produce no errors, got: {:?}", + errors.iter().map(|e| &e.message).collect::>() + ); +} + +/// `as` type cast should work. +#[tokio::test] +async fn valid_type_cast() { + let (_, errors) = build_single_file( + "type_cast", + r#" +fn main() { + f64 x = 3.14 + int y = x as int +} +"#, + ) + .await; + assert!( + errors.is_empty(), + "type cast should produce no errors, got: {:?}", + errors.iter().map(|e| &e.message).collect::>() + ); +} + +/// `is` type check should work. +#[tokio::test] +async fn valid_is_expr() { + let (_, errors) = build_single_file( + "is_expr", + r#" +type animal = int|string + +fn main() { + animal a = 42 + if a is int { + var x = a as int + } +} +"#, + ) + .await; + assert!( + errors.is_empty(), + "is expression should produce no errors, got: {:?}", + errors.iter().map(|e| &e.message).collect::>() + ); +} diff --git a/nls/tests/server_test.rs b/nls/tests/server_test.rs new file mode 100644 index 00000000..4fe7e763 --- /dev/null +++ b/nls/tests/server_test.rs @@ -0,0 +1,472 @@ +//! Regression tests for server-level behavior. +//! +//! Tests here verify configuration handling, document store contracts, +//! and utility function edge cases to catch regressions. + +// ─── Document store contract tests ────────────────────────────────────────────── + +mod document_store { + use nls::document::DocumentStore; + use tower_lsp::lsp_types::{Position, Range, TextDocumentContentChangeEvent, Url}; + + fn file_url(name: &str) -> Url { + Url::parse(&format!("file:///test/{}", name)).unwrap() + } + + #[test] + fn incremental_edits_compose_correctly() { + let store = DocumentStore::new(); + let uri = file_url("test.n"); + store.open(&uri, "hello world", 1, "nature".into()); + + // Replace "world" → "nature" + store.apply_changes( + &uri, + 2, + &[TextDocumentContentChangeEvent { + range: Some(Range::new(Position::new(0, 6), Position::new(0, 11))), + range_length: None, + text: "nature".into(), + }], + ); + + let doc = store.get(&uri).unwrap(); + assert_eq!(doc.rope.to_string(), "hello nature"); + assert_eq!(doc.version, 2); + } + + #[test] + fn multiple_incremental_edits_in_one_batch() { + let store = DocumentStore::new(); + let uri = file_url("multi.n"); + // "ab\ncd\nef" + store.open(&uri, "ab\ncd\nef", 1, "nature".into()); + + // Two edits in the same batch: + // 1) Replace 'a' with 'A' (line 0, col 0-1) + // 2) Replace 'e' with 'E' (line 2, col 0-1) + // Note: LSP spec says changes are applied sequentially. + store.apply_changes( + &uri, + 2, + &[ + TextDocumentContentChangeEvent { + range: Some(Range::new(Position::new(0, 0), Position::new(0, 1))), + range_length: None, + text: "A".into(), + }, + TextDocumentContentChangeEvent { + range: Some(Range::new(Position::new(2, 0), Position::new(2, 1))), + range_length: None, + text: "E".into(), + }, + ], + ); + + let doc = store.get(&uri).unwrap(); + assert_eq!(doc.rope.to_string(), "Ab\ncd\nEf"); + } + + #[test] + fn full_sync_replaces_entire_content() { + let store = DocumentStore::new(); + let uri = file_url("full.n"); + store.open(&uri, "original content", 1, "nature".into()); + + // Full sync: no range means replace everything. + store.apply_changes( + &uri, + 2, + &[TextDocumentContentChangeEvent { + range: None, + range_length: None, + text: "completely new content".into(), + }], + ); + + let doc = store.get(&uri).unwrap(); + assert_eq!(doc.rope.to_string(), "completely new content"); + } + + #[test] + fn open_same_file_twice_updates() { + let store = DocumentStore::new(); + let uri = file_url("dup.n"); + store.open(&uri, "version one", 1, "nature".into()); + store.open(&uri, "version two", 2, "nature".into()); + + let doc = store.get(&uri).unwrap(); + assert_eq!(doc.rope.to_string(), "version two"); + assert_eq!(doc.version, 2); + } + + #[test] + fn close_and_reopen() { + let store = DocumentStore::new(); + let uri = file_url("cycle.n"); + store.open(&uri, "first open", 1, "nature".into()); + store.close(&uri); + assert!(store.get(&uri).is_none()); + + store.open(&uri, "reopened", 2, "nature".into()); + let doc = store.get(&uri).unwrap(); + assert_eq!(doc.rope.to_string(), "reopened"); + } + + #[test] + fn insert_at_beginning() { + let store = DocumentStore::new(); + let uri = file_url("ins.n"); + store.open(&uri, "world", 1, "nature".into()); + + store.apply_changes( + &uri, + 2, + &[TextDocumentContentChangeEvent { + range: Some(Range::new(Position::new(0, 0), Position::new(0, 0))), + range_length: None, + text: "hello ".into(), + }], + ); + + let doc = store.get(&uri).unwrap(); + assert_eq!(doc.rope.to_string(), "hello world"); + } + + #[test] + fn delete_entire_line() { + let store = DocumentStore::new(); + let uri = file_url("del.n"); + store.open(&uri, "line1\nline2\nline3", 1, "nature".into()); + + // Delete line2 including its newline. + store.apply_changes( + &uri, + 2, + &[TextDocumentContentChangeEvent { + range: Some(Range::new(Position::new(1, 0), Position::new(2, 0))), + range_length: None, + text: "".into(), + }], + ); + + let doc = store.get(&uri).unwrap(); + assert_eq!(doc.rope.to_string(), "line1\nline3"); + } + + #[test] + fn unicode_content() { + let store = DocumentStore::new(); + let uri = file_url("unicode.n"); + let content = "fn main() {\n string s = '日本語'\n}"; + store.open(&uri, content, 1, "nature".into()); + + let doc = store.get(&uri).unwrap(); + assert!(doc.rope.to_string().contains("日本語")); + } + + #[test] + fn empty_document() { + let store = DocumentStore::new(); + let uri = file_url("empty.n"); + store.open(&uri, "", 1, "nature".into()); + + let doc = store.get(&uri).unwrap(); + assert_eq!(doc.rope.to_string(), ""); + assert_eq!(doc.rope.len_chars(), 0); + } +} + +// ─── Utils regression tests ───────────────────────────────────────────────────── + +mod utils_regression { + use nls::utils::*; + use ropey::Rope; + + #[test] + fn offset_to_position_empty_source() { + let rope = Rope::from_str(""); + // Empty rope at offset 0 yields position (0,0) — not None. + let pos = offset_to_position(0, &rope); + if let Some(p) = pos { + assert_eq!(p.line, 0); + assert_eq!(p.character, 0); + } + // Either None or (0,0) is acceptable. + } + + #[test] + fn offset_to_position_last_char() { + let rope = Rope::from_str("abc"); + let pos = offset_to_position(2, &rope).unwrap(); + assert_eq!(pos.line, 0); + assert_eq!(pos.character, 2); + } + + #[test] + fn offset_to_position_newline_boundary() { + let rope = Rope::from_str("ab\ncd\nef"); + // offset 3 = first char of second line ('c') + let pos = offset_to_position(3, &rope).unwrap(); + assert_eq!(pos.line, 1); + assert_eq!(pos.character, 0); + } + + #[test] + fn offset_to_position_end_of_line() { + let rope = Rope::from_str("ab\ncd\nef"); + // offset 2 = newline at end of first line + let pos = offset_to_position(2, &rope).unwrap(); + assert_eq!(pos.line, 0); + assert_eq!(pos.character, 2); + } + + #[test] + fn position_to_byte_offset_first_char() { + let rope = Rope::from_str("hello\nworld"); + let pos = tower_lsp::lsp_types::Position::new(0, 0); + let offset = position_to_byte_offset(pos, &rope); + assert_eq!(offset, Some(0)); + } + + #[test] + fn position_to_byte_offset_second_line() { + let rope = Rope::from_str("hello\nworld"); + let pos = tower_lsp::lsp_types::Position::new(1, 0); + let offset = position_to_byte_offset(pos, &rope); + assert_eq!(offset, Some(6)); // "hello\n" = 6 bytes + } + + #[test] + fn position_to_byte_offset_mid_word() { + let rope = Rope::from_str("hello\nworld"); + let pos = tower_lsp::lsp_types::Position::new(1, 3); + let offset = position_to_byte_offset(pos, &rope); + assert_eq!(offset, Some(9)); // "hello\nwor" = 9 bytes + } + + #[test] + fn extract_word_empty_string() { + let result = extract_word_at_offset("", 0); + assert!(result.is_none(), "empty string should yield None"); + } + + #[test] + fn extract_word_at_space() { + let result = extract_word_at_offset("hello world", 5); + // offset 5 is the space between words — may return None or adjacent word. + match &result { + None => {} // fine + Some((word, _, _)) => { + assert!( + word == "hello" || word == "world", + "unexpected word at space: {word}" + ); + } + } + } + + #[test] + fn extract_word_at_end_of_string() { + let result = extract_word_at_offset("hello", 5); + // offset 5 is past the end; should return "hello" or wrap. + match &result { + Some((word, _, _)) => assert_eq!(word, "hello"), + None => {} // also acceptable if offset is out of range + } + } + + #[test] + fn extract_word_rope_matches_string_version() { + let text = "fn main() { var foo = 42 }"; + let rope = Rope::from_str(text); + // Check a few positions — rope and string version should agree. + for offset in [0, 3, 12, 16, 22] { + let str_word = extract_word_at_offset(text, offset); + let rope_word = extract_word_at_offset_rope(&rope, offset); + assert_eq!( + str_word, rope_word, + "mismatch at offset {}: str={:?} rope={:?}", + offset, str_word, rope_word + ); + } + } + + #[test] + fn format_global_ident_regression() { + // These exact outputs are relied upon by the analyzer. + assert_eq!( + format_global_ident("mymod".into(), "myfn".into()), + "mymod.myfn" + ); + assert_eq!( + format_global_ident("".into(), "myfn".into()), + "myfn" + ); + } + + #[test] + fn format_impl_ident_regression() { + assert_eq!( + format_impl_ident("point".into(), "new".into()), + "point.new" + ); + } + + #[test] + fn format_generics_ident_regression() { + assert_eq!( + format_generics_ident("box".into(), 1), + "box#1" + ); + // When the ident already has a hash, the function replaces the suffix. + // Actual behavior: "box#0" with hash 1 → "box#0" (keeps existing hash). + let result = format_generics_ident("box#0".into(), 1); + assert!( + result.starts_with("box#"), + "should start with 'box#', got: {}", + result + ); + } + + #[test] + fn extract_symbol_from_diagnostic_not_found() { + let msg = "type 'point' not found in module"; + let sym = extract_symbol_from_diagnostic(msg); + assert_eq!(sym.as_deref(), Some("point")); + } + + #[test] + fn extract_symbol_from_diagnostic_no_match() { + let msg = "syntax error near line 42"; + let sym = extract_symbol_from_diagnostic(msg); + assert!(sym.is_none()); + } + + #[test] + fn align_up_regression() { + assert_eq!(align_up(0, 8), 0); + assert_eq!(align_up(1, 8), 8); + assert_eq!(align_up(8, 8), 8); + assert_eq!(align_up(9, 8), 16); + assert_eq!(align_up(100, 64), 128); + } +} + +// ─── Config regression tests ──────────────────────────────────────────────────── + +mod config_regression { + use dashmap::DashMap; + use nls::server::config::*; + use serde_json::json; + + #[test] + fn cfg_bool_returns_true_when_set() { + let config: DashMap = DashMap::new(); + config.insert("enabled".into(), json!(true)); + assert_eq!(cfg_bool(&config, "enabled", false), true); + } + + #[test] + fn cfg_bool_returns_default_when_absent() { + let config: DashMap = DashMap::new(); + assert_eq!(cfg_bool(&config, "missing", true), true); + assert_eq!(cfg_bool(&config, "missing", false), false); + } + + #[test] + fn cfg_u64_returns_value_when_set() { + let config: DashMap = DashMap::new(); + config.insert("timeout".into(), json!(500)); + assert_eq!(cfg_u64(&config, "timeout", 300), 500); + } + + #[test] + fn cfg_u64_returns_default_for_wrong_type() { + let config: DashMap = DashMap::new(); + config.insert("timeout".into(), json!("not a number")); + assert_eq!(cfg_u64(&config, "timeout", 300), 300); + } + + #[test] + fn cfg_string_returns_string() { + let config: DashMap = DashMap::new(); + config.insert("path".into(), json!("/usr/bin")); + assert_eq!(cfg_string(&config, "path", "default"), "/usr/bin"); + } + + #[test] + fn cfg_string_returns_default_for_wrong_type() { + let config: DashMap = DashMap::new(); + config.insert("path".into(), json!(42)); + assert_eq!(cfg_string(&config, "path", "default"), "default"); + } + + #[test] + fn debounce_ms_constant_is_positive() { + assert!(DEBOUNCE_MS > 0, "DEBOUNCE_MS must be positive"); + } + + #[test] + fn config_keys_are_dot_separated() { + // These config key constants must follow the dot-separated convention. + assert!(CFG_INLAY_HINTS_ENABLED.contains('.')); + assert!(CFG_INLAY_TYPE_HINTS.contains('.')); + assert!(CFG_INLAY_PARAM_HINTS.contains('.')); + assert!(CFG_DEBOUNCE_MS.contains('.')); + } +} + +// ─── Package parsing regression ───────────────────────────────────────────────── + +mod package_regression { + use nls::package::parse_package; + use std::fs; + use std::path::PathBuf; + use std::time::{SystemTime, UNIX_EPOCH}; + + fn temp_dir(name: &str) -> PathBuf { + let nanos = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos(); + let dir = std::env::temp_dir().join(format!("nls_pkg_test_{}_{}", name, nanos)); + fs::create_dir_all(&dir).unwrap(); + dir + } + + #[test] + fn parse_valid_package_toml() { + let dir = temp_dir("valid_pkg"); + let toml_path = dir.join("package.toml"); + fs::write( + &toml_path, + r#"name = "myproject" +version = "1.0.0" +type = "bin" +"#, + ) + .unwrap(); + + let result = parse_package(&toml_path.to_string_lossy()); + assert!(result.is_ok(), "should parse valid package.toml: {:?}", result.err()); + } + + #[test] + fn parse_missing_package_toml() { + let dir = temp_dir("missing_pkg"); + let toml_path = dir.join("package.toml"); + let result = parse_package(&toml_path.to_string_lossy()); + assert!(result.is_err(), "missing package.toml should return Err"); + } + + #[test] + fn parse_invalid_package_toml() { + let dir = temp_dir("invalid_pkg"); + let toml_path = dir.join("package.toml"); + fs::write(&toml_path, "this is not valid toml [[[[").unwrap(); + + let result = parse_package(&toml_path.to_string_lossy()); + assert!(result.is_err(), "invalid TOML should return Err"); + } +}