@@ -23,6 +23,8 @@ struct DocumentState {
2323 local_type_info: HashMap<Span, String>,
2424 /// Span-based type info for literal expressions.
2525 literal_types: Vec<(Span, String)>,
26+ /// Per-declaration effect info (formatted strings).
27+ effect_info: HashMap<String, String>,
2628 knot_diagnostics: Vec<diagnostic::Diagnostic>,
2729}
2830
@@ -48,6 +50,20 @@ fn main() {
4850 trigger_characters: Some(vec![".".into(), "*".into(), "&".into()]),
4951 ..Default::default()
5052 }),
53+ references_provider: Some(OneOf::Left(true)),
54+ rename_provider: Some(OneOf::Right(RenameOptions {
55+ prepare_provider: Some(true),
56+ work_done_progress_options: Default::default(),
57+ })),
58+ inlay_hint_provider: Some(OneOf::Left(true)),
59+ signature_help_provider: Some(SignatureHelpOptions {
60+ trigger_characters: Some(vec![" ".into()]),
61+ retrigger_characters: None,
62+ work_done_progress_options: Default::default(),
63+ }),
64+ code_lens_provider: Some(CodeLensOptions {
65+ resolve_provider: Some(false),
66+ }),
5167 ..Default::default()
5268 })
5369 .unwrap();
@@ -101,6 +117,24 @@ fn handle_request(state: &mut ServerState, conn: &Connection, req: Request) {
101117 } else if let Some(params) = cast_request::<request::Completion>(&req) {
102118 let result = handle_completion(state, ¶ms);
103119 send_response(conn, id, result);
120+ } else if let Some(params) = cast_request::<request::References>(&req) {
121+ let result = handle_references(state, ¶ms);
122+ send_response(conn, id, result);
123+ } else if let Some(params) = cast_request::<request::PrepareRenameRequest>(&req) {
124+ let result = handle_prepare_rename(state, ¶ms);
125+ send_response(conn, id, result);
126+ } else if let Some(params) = cast_request::<request::Rename>(&req) {
127+ let result = handle_rename(state, ¶ms);
128+ send_response(conn, id, result);
129+ } else if let Some(params) = cast_request::<request::InlayHintRequest>(&req) {
130+ let result = handle_inlay_hint(state, ¶ms);
131+ send_response(conn, id, result);
132+ } else if let Some(params) = cast_request::<request::SignatureHelpRequest>(&req) {
133+ let result = handle_signature_help(state, ¶ms);
134+ send_response(conn, id, result);
135+ } else if let Some(params) = cast_request::<request::CodeLensRequest>(&req) {
136+ let result = handle_code_lens(state, ¶ms);
137+ send_response(conn, id, result);
104138 }
105139}
106140
@@ -150,6 +184,7 @@ fn analyze_document(uri: &Uri, source: &str) -> DocumentState {
150184 let mut all_diags = Vec::new();
151185 let mut type_info = HashMap::new();
152186 let mut local_type_info = HashMap::new();
187+ let mut effect_info = HashMap::new();
153188
154189 // Lex
155190 let lexer = knot::lexer::Lexer::new(source);
@@ -190,7 +225,14 @@ fn analyze_document(uri: &Uri, source: &str) -> DocumentState {
190225 local_type_info = local_types;
191226
192227 // Effect inference
193− all_diags.extend(knot_compiler::effects::check(&analysis_module));
228+ let (effect_diags, effects) =
229+ knot_compiler::effects::check_with_effects(&analysis_module);
230+ all_diags.extend(effect_diags);
231+ for (name, eff) in &effects {
232+ if !eff.is_pure() {
233+ effect_info.insert(name.clone(), format!("{eff}"));
234+ }
235+ }
194236
195237 // Stratification
196238 all_diags.extend(knot_compiler::stratify::check(&analysis_module));
@@ -205,6 +247,7 @@ fn analyze_document(uri: &Uri, source: &str) -> DocumentState {
205247 type_info,
206248 local_type_info,
207249 literal_types,
250+ effect_info,
208251 knot_diagnostics: all_diags,
209252 }
210253}
@@ -363,7 +406,7 @@ fn handle_hover(state: &ServerState, params: &HoverParams) -> Option<Hover> {
363406 } else if let Some(d) = doc.details.get(word) {
364407 // If we have an inferred type and the AST detail has no type annotation,
365408 // enhance with the inferred type
366− if let Some(inferred) = doc.type_info.get(word) {
409+ let base = if let Some(inferred) = doc.type_info.get(word) {
367410 if !d.contains(':') {
368411 format!("{d} : {inferred}")
369412 } else {
@@ -371,10 +414,20 @@ fn handle_hover(state: &ServerState, params: &HoverParams) -> Option<Hover> {
371414 }
372415 } else {
373416 d.clone()
417+ };
418+ // Append effect info if available
419+ if let Some(effects) = doc.effect_info.get(word) {
420+ format!("{base}\n{effects}")
421+ } else {
422+ base
374423 }
375424 } else if let Some(inferred) = doc.type_info.get(word) {
376− // No AST detail but have inferred type (e.g. builtins, prelude functions)
377− format!("{word} : {inferred}")
425+ let base = format!("{word} : {inferred}");
426+ if let Some(effects) = doc.effect_info.get(word) {
427+ format!("{base}\n{effects}")
428+ } else {
429+ base
430+ }
378431 } else {
379432 return None;
380433 };
@@ -396,9 +449,92 @@ fn handle_completion(
396449) -> Option<CompletionResponse> {
397450 let uri = ¶ms.text_document_position.text_document.uri;
398451 let doc = state.documents.get(uri)?;
452+ let pos = params.text_document_position.position;
453+
454+ // Detect trigger context
455+ let offset = position_to_offset(&doc.source, pos);
456+ let trigger_char = if offset > 0 {
457+ doc.source.as_bytes().get(offset - 1).copied()
458+ } else {
459+ None
460+ };
399461
400462 let mut items = Vec::new();
401463
464+ // Context-aware: after `*` only suggest source/view names
465+ if trigger_char == Some(b'*') {
466+ for decl in &doc.module.decls {
467+ if let DeclKind::Source { name, .. } | DeclKind::View { name, .. } = &decl.node {
468+ let detail = doc.type_info.get(name.as_str()).cloned();
469+ items.push(CompletionItem {
470+ label: name.clone(),
471+ kind: Some(CompletionItemKind::VARIABLE),
472+ detail,
473+ ..Default::default()
474+ });
475+ }
476+ }
477+ return Some(CompletionResponse::Array(items));
478+ }
479+
480+ // Context-aware: after `&` only suggest derived names
481+ if trigger_char == Some(b'&') {
482+ for decl in &doc.module.decls {
483+ if let DeclKind::Derived { name, .. } = &decl.node {
484+ let detail = doc.type_info.get(name.as_str()).cloned();
485+ items.push(CompletionItem {
486+ label: name.clone(),
487+ kind: Some(CompletionItemKind::VARIABLE),
488+ detail,
489+ ..Default::default()
490+ });
491+ }
492+ }
493+ return Some(CompletionResponse::Array(items));
494+ }
495+
496+ // Context-aware: after `.` suggest record field names from known types
497+ if trigger_char == Some(b'.') {
498+ // Collect all known field names from record types and data constructors
499+ let mut fields = std::collections::HashSet::new();
500+ for decl in &doc.module.decls {
501+ match &decl.node {
502+ DeclKind::TypeAlias { ty, .. } => {
503+ if let TypeKind::Record { fields: fs, .. } = &ty.node {
504+ for f in fs {
505+ fields.insert(f.name.clone());
506+ }
507+ }
508+ }
509+ DeclKind::Source { ty, .. } => {
510+ if let TypeKind::Record { fields: fs, .. } = &ty.node {
511+ for f in fs {
512+ fields.insert(f.name.clone());
513+ }
514+ }
515+ }
516+ DeclKind::Data { constructors, .. } => {
517+ for ctor in constructors {
518+ for f in &ctor.fields {
519+ fields.insert(f.name.clone());
520+ }
521+ }
522+ }
523+ _ => {}
524+ }
525+ }
526+ for name in fields {
527+ items.push(CompletionItem {
528+ label: name,
529+ kind: Some(CompletionItemKind::FIELD),
530+ ..Default::default()
531+ });
532+ }
533+ return Some(CompletionResponse::Array(items));
534+ }
535+
536+ // General completion: keywords + declarations + builtins
537+
402538 // Keywords
403539 for kw in KEYWORDS {
404540 items.push(CompletionItem {
@@ -408,7 +544,7 @@ fn handle_completion(
408544 });
409545 }
410546
411− // Declarations from current document
547+ // Declarations from current document with type details
412548 for decl in &doc.module.decls {
413549 match &decl.node {
414550 DeclKind::Data {
@@ -417,12 +553,14 @@ fn handle_completion(
417553 items.push(CompletionItem {
418554 label: name.clone(),
419555 kind: Some(CompletionItemKind::STRUCT),
556+ detail: doc.details.get(name).cloned(),
420557 ..Default::default()
421558 });
422559 for ctor in constructors {
423560 items.push(CompletionItem {
424561 label: ctor.name.clone(),
425562 kind: Some(CompletionItemKind::ENUM_MEMBER),
563+ detail: doc.details.get(&ctor.name).cloned(),
426564 ..Default::default()
427565 });
428566 }
@@ -431,6 +569,7 @@ fn handle_completion(
431569 items.push(CompletionItem {
432570 label: name.clone(),
433571 kind: Some(CompletionItemKind::STRUCT),
572+ detail: doc.details.get(name).cloned(),
434573 ..Default::default()
435574 });
436575 }
@@ -439,6 +578,7 @@ fn handle_completion(
439578 label: format!("*{name}"),
440579 kind: Some(CompletionItemKind::VARIABLE),
441580 insert_text: Some(format!("*{name}")),
581+ detail: doc.type_info.get(name.as_str()).cloned(),
442582 ..Default::default()
443583 });
444584 }
@@ -447,6 +587,7 @@ fn handle_completion(
447587 label: format!("&{name}"),
448588 kind: Some(CompletionItemKind::VARIABLE),
449589 insert_text: Some(format!("&{name}")),
590+ detail: doc.type_info.get(name.as_str()).cloned(),
450591 ..Default::default()
451592 });
452593 }
@@ -454,6 +595,7 @@ fn handle_completion(
454595 items.push(CompletionItem {
455596 label: name.clone(),
456597 kind: Some(CompletionItemKind::FUNCTION),
598+ detail: doc.type_info.get(name.as_str()).cloned(),
457599 ..Default::default()
458600 });
459601 }
@@ -461,6 +603,7 @@ fn handle_completion(
461603 items.push(CompletionItem {
462604 label: name.clone(),
463605 kind: Some(CompletionItemKind::INTERFACE),
606+ detail: doc.details.get(name).cloned(),
464607 ..Default::default()
465608 });
466609 }
@@ -468,11 +611,12 @@ fn handle_completion(
468611 }
469612 }
470613
471− // Built-in functions
614+ // Built-in functions with type info
472615 for name in BUILTINS {
473616 items.push(CompletionItem {
474617 label: name.to_string(),
475618 kind: Some(CompletionItemKind::FUNCTION),
619+ detail: doc.type_info.get(*name).cloned(),
476620 ..Default::default()
477621 });
478622 }
@@ -480,6 +624,451 @@ fn handle_completion(
480624 Some(CompletionResponse::Array(items))
481625}
482626
627+// ── Find References ─────────────────────────────────────────────────
628+
629+fn handle_references(
630+ state: &ServerState,
631+ params: &ReferenceParams,
632+) -> Option<Vec<Location>> {
633+ let uri = ¶ms.text_document_position.text_document.uri;
634+ let pos = params.text_document_position.position;
635+ let doc = state.documents.get(uri)?;
636+ let offset = position_to_offset(&doc.source, pos);
637+
638+ // Find the definition span for the symbol at cursor
639+ let def_span = doc
640+ .references
641+ .iter()
642+ .find(|(usage, _)| usage.start <= offset && offset < usage.end)
643+ .map(|(_, def)| *def)
644+ .or_else(|| {
645+ // Cursor might be on a definition site itself
646+ let word = word_at_position(&doc.source, pos)?;
647+ doc.definitions.get(word).copied()
648+ })
649+ // Also check: cursor might be directly on a definition span
650+ .or_else(|| {
651+ doc.definitions.values().find(|span| span.start <= offset && offset < span.end).copied()
652+ })?;
653+
654+ let mut locations = Vec::new();
655+
656+ // Include declaration if requested
657+ if params.context.include_declaration {
658+ locations.push(Location {
659+ uri: uri.clone(),
660+ range: span_to_range(def_span, &doc.source),
661+ });
662+ }
663+
664+ // Find all usages that point to this definition
665+ for (usage_span, target_span) in &doc.references {
666+ if *target_span == def_span {
667+ locations.push(Location {
668+ uri: uri.clone(),
669+ range: span_to_range(*usage_span, &doc.source),
670+ });
671+ }
672+ }
673+
674+ if locations.is_empty() {
675+ None
676+ } else {
677+ Some(locations)
678+ }
679+}
680+
681+// ── Rename ──────────────────────────────────────────────────────────
682+
683+fn handle_prepare_rename(
684+ state: &ServerState,
685+ params: &TextDocumentPositionParams,
686+) -> Option<PrepareRenameResponse> {
687+ let doc = state.documents.get(¶ms.text_document.uri)?;
688+ let pos = params.position;
689+ let offset = position_to_offset(&doc.source, pos);
690+
691+ // Check if cursor is on a renameable symbol
692+ let word = word_at_position(&doc.source, pos)?;
693+
694+ // Must be on a known definition or a reference to one
695+ let is_ref = doc
696+ .references
697+ .iter()
698+ .any(|(usage, _)| usage.start <= offset && offset < usage.end);
699+ let is_def = doc.definitions.values().any(|span| span.start <= offset && offset < span.end);
700+
701+ if !is_ref && !is_def {
702+ return None;
703+ }
704+
705+ // Return the word range
706+ let word_offset = position_to_offset(&doc.source, pos);
707+ let bytes = doc.source.as_bytes();
708+ let is_ident = |b: u8| b.is_ascii_alphanumeric() || b == b'_';
709+ let start = (0..word_offset)
710+ .rev()
711+ .find(|&i| !is_ident(bytes[i]))
712+ .map(|i| i + 1)
713+ .unwrap_or(0);
714+ let end = (word_offset..bytes.len())
715+ .find(|&i| !is_ident(bytes[i]))
716+ .unwrap_or(bytes.len());
717+
718+ let range = span_to_range(Span::new(start, end), &doc.source);
719+ Some(PrepareRenameResponse::RangeWithPlaceholder {
720+ range,
721+ placeholder: word.to_string(),
722+ })
723+}
724+
725+fn handle_rename(
726+ state: &ServerState,
727+ params: &RenameParams,
728+) -> Option<WorkspaceEdit> {
729+ let uri = ¶ms.text_document_position.text_document.uri;
730+ let pos = params.text_document_position.position;
731+ let doc = state.documents.get(uri)?;
732+ let offset = position_to_offset(&doc.source, pos);
733+ let new_name = ¶ms.new_name;
734+
735+ // Find the definition span
736+ let def_span = doc
737+ .references
738+ .iter()
739+ .find(|(usage, _)| usage.start <= offset && offset < usage.end)
740+ .map(|(_, def)| *def)
741+ .or_else(|| {
742+ doc.definitions.values().find(|span| span.start <= offset && offset < span.end).copied()
743+ })?;
744+
745+ let mut edits = Vec::new();
746+
747+ // Rename at definition site
748+ let def_range = span_to_range(def_span, &doc.source);
749+ // Find the word within the definition span to rename precisely
750+ let def_text = &doc.source[def_span.start..def_span.end];
751+ let old_name = word_at_position(&doc.source, pos)?;
752+ if let Some(name_start) = def_text.find(old_name) {
753+ let name_span = Span::new(def_span.start + name_start, def_span.start + name_start + old_name.len());
754+ edits.push(TextEdit {
755+ range: span_to_range(name_span, &doc.source),
756+ new_text: new_name.clone(),
757+ });
758+ } else {
759+ edits.push(TextEdit {
760+ range: def_range,
761+ new_text: new_name.clone(),
762+ });
763+ }
764+
765+ // Rename all usage sites
766+ for (usage_span, target_span) in &doc.references {
767+ if *target_span == def_span {
768+ edits.push(TextEdit {
769+ range: span_to_range(*usage_span, &doc.source),
770+ new_text: new_name.clone(),
771+ });
772+ }
773+ }
774+
775+ let mut changes = HashMap::new();
776+ changes.insert(uri.clone(), edits);
777+
778+ Some(WorkspaceEdit {
779+ changes: Some(changes),
780+ ..Default::default()
781+ })
782+}
783+
784+// ── Inlay Hints ─────────────────────────────────────────────────────
785+
786+fn handle_inlay_hint(
787+ state: &ServerState,
788+ params: &InlayHintParams,
789+) -> Option<Vec<InlayHint>> {
790+ let doc = state.documents.get(¶ms.text_document.uri)?;
791+ let mut hints = Vec::new();
792+
793+ let range_start = position_to_offset(&doc.source, params.range.start);
794+ let range_end = position_to_offset(&doc.source, params.range.end);
795+
796+ // Show inferred types for unannotated function declarations
797+ for decl in &doc.module.decls {
798+ // Only show hints within the visible range
799+ if decl.span.end < range_start || decl.span.start > range_end {
800+ continue;
801+ }
802+
803+ match &decl.node {
804+ DeclKind::Fun { name, ty: None, .. } => {
805+ if let Some(inferred) = doc.type_info.get(name) {
806+ let decl_text = &doc.source[decl.span.start..decl.span.end.min(doc.source.len())];
807+ let name_end = decl_text.find(|c: char| !c.is_alphanumeric() && c != '_')
808+ .unwrap_or(name.len());
809+ let hint_offset = decl.span.start + name_end;
810+ let hint_pos = offset_to_position(&doc.source, hint_offset);
811+ hints.push(InlayHint {
812+ position: hint_pos,
813+ label: InlayHintLabel::String(format!(": {inferred}")),
814+ kind: Some(InlayHintKind::TYPE),
815+ text_edits: None,
816+ tooltip: None,
817+ padding_left: Some(true),
818+ padding_right: Some(true),
819+ data: None,
820+ });
821+ }
822+ }
823+ DeclKind::View { name, ty: None, .. } | DeclKind::Derived { name, ty: None, .. } => {
824+ if let Some(inferred) = doc.type_info.get(name) {
825+ let decl_text = &doc.source[decl.span.start..decl.span.end.min(doc.source.len())];
826+ let name_end = decl_text.find('=').unwrap_or(name.len() + 1);
827+ let hint_offset = decl.span.start + name_end;
828+ let hint_pos = offset_to_position(&doc.source, hint_offset);
829+ hints.push(InlayHint {
830+ position: hint_pos,
831+ label: InlayHintLabel::String(format!(": {inferred}")),
832+ kind: Some(InlayHintKind::TYPE),
833+ text_edits: None,
834+ tooltip: None,
835+ padding_left: Some(true),
836+ padding_right: Some(true),
837+ data: None,
838+ });
839+ }
840+ }
841+ _ => {}
842+ }
843+ }
844+
845+ // Show inferred types for local bindings (let/bind in do blocks)
846+ for (span, ty) in &doc.local_type_info {
847+ if span.end < range_start || span.start > range_end {
848+ continue;
849+ }
850+ let hint_pos = offset_to_position(&doc.source, span.end);
851+ hints.push(InlayHint {
852+ position: hint_pos,
853+ label: InlayHintLabel::String(format!(": {ty}")),
854+ kind: Some(InlayHintKind::TYPE),
855+ text_edits: None,
856+ tooltip: None,
857+ padding_left: Some(true),
858+ padding_right: None,
859+ data: None,
860+ });
861+ }
862+
863+ Some(hints)
864+}
865+
866+// ── Signature Help ──────────────────────────────────────────────────
867+
868+fn handle_signature_help(
869+ state: &ServerState,
870+ params: &SignatureHelpParams,
871+) -> Option<SignatureHelp> {
872+ let uri = ¶ms.text_document_position_params.text_document.uri;
873+ let pos = params.text_document_position_params.position;
874+ let doc = state.documents.get(uri)?;
875+ let offset = position_to_offset(&doc.source, pos);
876+
877+ // Walk backwards to find the function name being applied
878+ // In Knot, function application is `f x y z` (juxtaposition)
879+ // Find the start of the current "argument list" by looking backwards for a function name
880+ let bytes = doc.source.as_bytes();
881+ let is_ident = |b: u8| b.is_ascii_alphanumeric() || b == b'_';
882+
883+ // Skip back over whitespace and the current word being typed
884+ let mut i = offset;
885+ // Skip current word
886+ while i > 0 && is_ident(bytes[i - 1]) {
887+ i -= 1;
888+ }
889+ // Skip whitespace
890+ while i > 0 && bytes[i - 1] == b' ' {
891+ i -= 1;
892+ }
893+
894+ // Count how many arguments precede cursor (count spaces between function and cursor)
895+ let mut active_param = 0u32;
896+
897+ // Now find the function name
898+ let end = i;
899+ while i > 0 && is_ident(bytes[i - 1]) {
900+ i -= 1;
901+ }
902+ if i == end {
903+ return None; // no function name found
904+ }
905+ let func_name = &doc.source[i..end];
906+
907+ // Count arguments between function and cursor
908+ let between = &doc.source[end..offset];
909+ for segment in between.split_whitespace() {
910+ if !segment.is_empty() {
911+ active_param += 1;
912+ }
913+ }
914+
915+ // Look up the function type
916+ let type_str = doc.type_info.get(func_name)?;
917+
918+ // Parse arrow-separated parameters from the type string
919+ let params_list = parse_function_params(type_str);
920+ if params_list.is_empty() {
921+ return None;
922+ }
923+
924+ let param_infos: Vec<ParameterInformation> = params_list
925+ .iter()
926+ .map(|p| ParameterInformation {
927+ label: ParameterLabel::Simple(p.clone()),
928+ documentation: None,
929+ })
930+ .collect();
931+
932+ let signature = SignatureInformation {
933+ label: format!("{func_name} : {type_str}"),
934+ documentation: None,
935+ parameters: Some(param_infos),
936+ active_parameter: Some(active_param),
937+ };
938+
939+ Some(SignatureHelp {
940+ signatures: vec![signature],
941+ active_signature: Some(0),
942+ active_parameter: Some(active_param),
943+ })
944+}
945+
946+/// Parse a Knot type string like "Int -> Text -> Bool" into parameter types.
947+fn parse_function_params(type_str: &str) -> Vec<String> {
948+ let mut params = Vec::new();
949+ let mut depth = 0i32;
950+ let mut current = String::new();
951+
952+ let chars: Vec<char> = type_str.chars().collect();
953+ let mut i = 0;
954+
955+ while i < chars.len() {
956+ match chars[i] {
957+ '(' | '[' | '{' | '<' => {
958+ depth += 1;
959+ current.push(chars[i]);
960+ }
961+ ')' | ']' | '}' | '>' => {
962+ depth -= 1;
963+ current.push(chars[i]);
964+ }
965+ '-' if depth == 0 && i + 1 < chars.len() && chars[i + 1] == '>' => {
966+ let trimmed = current.trim().to_string();
967+ if !trimmed.is_empty() {
968+ params.push(trimmed);
969+ }
970+ current.clear();
971+ i += 2; // skip "->"
972+ continue;
973+ }
974+ _ => {
975+ current.push(chars[i]);
976+ }
977+ }
978+ i += 1;
979+ }
980+
981+ // The last segment is the return type, not a parameter
982+ // But we still include all parts as the signature info
983+ // Actually, keep all segments — the last is the return type
984+ let trimmed = current.trim().to_string();
985+ if !trimmed.is_empty() {
986+ params.push(trimmed);
987+ }
988+
989+ params
990+}
991+
992+// ── Code Lens ───────────────────────────────────────────────────────
993+
994+fn handle_code_lens(
995+ state: &ServerState,
996+ params: &CodeLensParams,
997+) -> Option<Vec<CodeLens>> {
998+ let doc = state.documents.get(¶ms.text_document.uri)?;
999+ let mut lenses = Vec::new();
1000+
1001+ for decl in &doc.module.decls {
1002+ match &decl.node {
1003+ DeclKind::Fun { .. }
1004+ | DeclKind::Source { .. }
1005+ | DeclKind::View { .. }
1006+ | DeclKind::Derived { .. }
1007+ | DeclKind::Data { .. }
1008+ | DeclKind::Trait { .. } => {}
1009+ _ => continue,
1010+ }
1011+
1012+ // Count references to this declaration
1013+ let ref_count = doc
1014+ .references
1015+ .iter()
1016+ .filter(|(_, def)| *def == decl.span)
1017+ .count();
1018+
1019+ let range = span_to_range(decl.span, &doc.source);
1020+ let title = if ref_count == 1 {
1021+ "1 reference".to_string()
1022+ } else {
1023+ format!("{ref_count} references")
1024+ };
1025+
1026+ lenses.push(CodeLens {
1027+ range: Range {
1028+ start: range.start,
1029+ end: range.start, // code lens goes on a single line
1030+ },
1031+ command: Some(Command {
1032+ title,
1033+ command: String::new(),
1034+ arguments: None,
1035+ }),
1036+ data: None,
1037+ });
1038+
1039+ // For traits: show number of implementations
1040+ if let DeclKind::Trait { name, .. } = &decl.node {
1041+ let impl_count = doc
1042+ .module
1043+ .decls
1044+ .iter()
1045+ .filter(|d| matches!(&d.node, DeclKind::Impl { trait_name, .. } if trait_name == name))
1046+ .count();
1047+ if impl_count > 0 {
1048+ let title = if impl_count == 1 {
1049+ "1 implementation".to_string()
1050+ } else {
1051+ format!("{impl_count} implementations")
1052+ };
1053+ lenses.push(CodeLens {
1054+ range: Range {
1055+ start: range.start,
1056+ end: range.start,
1057+ },
1058+ command: Some(Command {
1059+ title,
1060+ command: String::new(),
1061+ arguments: None,
1062+ }),
1063+ data: None,
1064+ });
1065+ }
1066+ }
1067+ }
1068+
1069+ Some(lenses)
1070+}
1071+
4831072const KEYWORDS: &[&str] = &[
4841073 "import", "data", "type", "trait", "impl", "route", "migrate", "where", "do", "yield", "set",
4851074 "if", "then", "else", "case", "of", "let", "in", "not", "full", "atomic", "deriving", "with",