Check if the cursor is immediately after `/**` with only whitespace before it on the line, and that there is no existing docblock (i.e. the `/**` is not already closed with `*/`). Returns the range covering the `/**` text (to be replaced by the snippet) and the leading indentation string.
(content: &str, position: Position)
| 392 | /// Returns the range covering the `/**` text (to be replaced by the |
| 393 | /// snippet) and the leading indentation string. |
| 394 | fn detect_docblock_trigger(content: &str, position: Position) -> Option<(Range, String)> { |
| 395 | let lines: Vec<&str> = content.lines().collect(); |
| 396 | let line_idx = position.line as usize; |
| 397 | if line_idx >= lines.len() { |
| 398 | return None; |
| 399 | } |
| 400 | |
| 401 | let line = lines[line_idx]; |
| 402 | |
| 403 | // Convert the UTF-16 column offset to a byte offset within the line. |
| 404 | // LSP positions use UTF-16 code units, which diverge from byte offsets |
| 405 | // when the line contains multibyte characters (e.g. "ń" is 2 bytes in |
| 406 | // UTF-8 but 1 UTF-16 code unit). |
| 407 | let col = utf16_col_to_byte_offset(line, position.character); |
| 408 | |
| 409 | // The cursor column must be at least 3 (for `/**`). |
| 410 | if col < 3 { |
| 411 | return None; |
| 412 | } |
| 413 | |
| 414 | // Get the text up to the cursor on this line. |
| 415 | let before_cursor = if col <= line.len() { |
| 416 | &line[..col] |
| 417 | } else { |
| 418 | line |
| 419 | }; |
| 420 | |
| 421 | // Must end with `/**`. |
| 422 | if !before_cursor.ends_with("/**") { |
| 423 | return None; |
| 424 | } |
| 425 | |
| 426 | // Everything before `/**` must be whitespace. |
| 427 | let prefix = &before_cursor[..before_cursor.len() - 3]; |
| 428 | if !prefix.chars().all(|c| c == ' ' || c == '\t') { |
| 429 | return None; |
| 430 | } |
| 431 | |
| 432 | // Check what follows the `/**` on this line. |
| 433 | let after_trigger = if col <= line.len() { &line[col..] } else { "" }; |
| 434 | |
| 435 | // Editors like VS Code auto-close `/**` into `/** */` on the same |
| 436 | // line. We allow this when the only thing after `/**` is optional |
| 437 | // whitespace and `*/` (i.e. an empty auto-closed block). |
| 438 | let after_trimmed = after_trigger.trim(); |
| 439 | let auto_closed = after_trimmed == "*/" || after_trimmed.is_empty(); |
| 440 | |
| 441 | // If there is a `*/` with real content between `/**` and `*/` |
| 442 | // (e.g. `/** @var int */`), this is an existing single-line |
| 443 | // docblock — don't trigger. |
| 444 | if !auto_closed && after_trigger.contains("*/") { |
| 445 | return None; |
| 446 | } |
| 447 | |
| 448 | // Also check that the next few lines don't form an existing |
| 449 | // docblock (i.e. don't generate a new block inside an existing one). |
| 450 | // A simple heuristic: if the next non-empty line starts with `*` or |
| 451 | // contains `*/`, there's already a docblock. |