···10 TypePosition,
11 /// Inside constrained { } block
12 ConstraintBlock,
000013 /// Unknown context
14 Unknown,
15}
1617/// Detect the completion context based on the text before the cursor
18pub fn detect_context(text_before_cursor: &str) -> CompletionContext {
0000000000000000000000000000019 // Check if we're in a use statement (check before trimming!)
20 if let Some(last_line) = text_before_cursor.lines().last() {
21 let last_line = last_line.trim_start(); // Only trim left side
···126 fn test_top_level() {
127 assert_eq!(detect_context(""), CompletionContext::TopLevel);
128 assert_eq!(detect_context("rec"), CompletionContext::TopLevel);
0000000000000000000129 }
130}
···10 TypePosition,
11 /// Inside constrained { } block
12 ConstraintBlock,
13+ /// Typing an annotation (after @)
14+ Annotation,
15+ /// Typing annotation selectors (after @rust,)
16+ AnnotationSelector,
17 /// Unknown context
18 Unknown,
19}
2021/// Detect the completion context based on the text before the cursor
22pub fn detect_context(text_before_cursor: &str) -> CompletionContext {
23+ // Check if we're typing an annotation
24+ if let Some(last_line) = text_before_cursor.lines().last() {
25+ let last_line_trimmed = last_line.trim_start();
26+27+ // Check for annotation selector context: @rust, or @rust,typescript,
28+ if let Some(at_pos) = last_line_trimmed.rfind('@') {
29+ let after_at = &last_line_trimmed[at_pos + 1..];
30+31+ // If there's a colon, we're past the selectors
32+ if after_at.contains(':') {
33+ // Check if we're completing the annotation name
34+ let parts: Vec<&str> = after_at.split(':').collect();
35+ if parts.len() >= 2 && !parts[1].contains('(') {
36+ return CompletionContext::Annotation;
37+ }
38+ } else if after_at.ends_with(',') || (after_at.contains(',') && !after_at.contains(':')) {
39+ // We're completing another selector: @rust, or @rust,typescript,
40+ return CompletionContext::AnnotationSelector;
41+ } else if !after_at.is_empty() && !after_at.contains(':') && !after_at.contains('(') {
42+ // We're completing the first part (could be selector or annotation name)
43+ // Default to annotation for simplicity
44+ return CompletionContext::Annotation;
45+ } else if after_at.is_empty() {
46+ // Just typed @ - suggest annotations
47+ return CompletionContext::Annotation;
48+ }
49+ }
50+ }
51+52 // Check if we're in a use statement (check before trimming!)
53 if let Some(last_line) = text_before_cursor.lines().last() {
54 let last_line = last_line.trim_start(); // Only trim left side
···159 fn test_top_level() {
160 assert_eq!(detect_context(""), CompletionContext::TopLevel);
161 assert_eq!(detect_context("rec"), CompletionContext::TopLevel);
162+ }
163+164+ #[test]
165+ fn test_annotation() {
166+ assert_eq!(detect_context("@"), CompletionContext::Annotation);
167+ assert_eq!(detect_context("@dep"), CompletionContext::Annotation);
168+ assert_eq!(detect_context("@deprecated"), CompletionContext::Annotation);
169+ }
170+171+ #[test]
172+ fn test_annotation_selector() {
173+ assert_eq!(detect_context("@rust,"), CompletionContext::AnnotationSelector);
174+ assert_eq!(detect_context("@rust,typescript,"), CompletionContext::AnnotationSelector);
175+ }
176+177+ #[test]
178+ fn test_annotation_after_selector() {
179+ assert_eq!(detect_context("@rust:"), CompletionContext::Annotation);
180+ assert_eq!(detect_context("@rust,typescript:"), CompletionContext::Annotation);
181 }
182}
+154
mlf-lsp/src/server.rs
···792 ".".to_string(),
793 ":".to_string(),
794 " ".to_string(),
00795 ]),
796 ..Default::default()
797 }),
···896 if let Some(lexicon) = &doc_state.lexicon {
897 // Convert position to offset
898 if let Some(offset) = position_to_offset(&doc_state.text, position) {
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000899 // Find the item at this position
900 if let Some(item) = find_item_at_offset(lexicon, offset) {
901 let name = get_item_name(item);
···1260 }
12611262 tracing::debug!("Total completions: {}", completions.len());
0000000000000000000000000000000000000000000001263 }
12641265 MlfCompletionContext::TopLevel => {
···792 ".".to_string(),
793 ":".to_string(),
794 " ".to_string(),
795+ "@".to_string(),
796+ ",".to_string(),
797 ]),
798 ..Default::default()
799 }),
···898 if let Some(lexicon) = &doc_state.lexicon {
899 // Convert position to offset
900 if let Some(offset) = position_to_offset(&doc_state.text, position) {
901+ // Check if we're hovering over an annotation
902+ for item in &lexicon.items {
903+ let annotations_to_check = match item {
904+ Item::Record(r) => Some(&r.annotations),
905+ Item::InlineType(i) => Some(&i.annotations),
906+ Item::DefType(d) => Some(&d.annotations),
907+ Item::Token(t) => Some(&t.annotations),
908+ Item::Query(q) => Some(&q.annotations),
909+ Item::Procedure(p) => Some(&p.annotations),
910+ Item::Subscription(s) => Some(&s.annotations),
911+ _ => None,
912+ };
913+914+ if let Some(annotations) = annotations_to_check {
915+ for annotation in annotations {
916+ if annotation.span.start <= offset && offset <= annotation.span.end {
917+ // Build hover content for the annotation
918+ let mut contents = vec![];
919+920+ // Format annotation name with selectors if present
921+ let annotation_display = if annotation.selectors.is_empty() {
922+ format!("@{}", annotation.name.name)
923+ } else {
924+ let selector_names: Vec<_> = annotation.selectors.iter()
925+ .map(|s| s.name.as_str())
926+ .collect();
927+ format!("@{}:{}", selector_names.join(","), annotation.name.name)
928+ };
929+930+ contents.push(MarkedString::LanguageString(LanguageString {
931+ language: "mlf".to_string(),
932+ value: annotation_display.clone(),
933+ }));
934+935+ // Add description based on annotation name
936+ let description = match annotation.name.name.as_str() {
937+ "deprecated" => "Marks this definition as deprecated",
938+ "main" => "Designates this as the main definition for conflict resolution",
939+ "key" => "Specifies the record key type (e.g., 'tid', 'literal:self')",
940+ "encoding" => "Specifies MIME type encoding for XRPC (e.g., 'application/json', 'application/cbor')",
941+ "since" => "Indicates the version when this was added",
942+ "doc" => "Provides a documentation URL",
943+ "validate" => "Specifies validation rules",
944+ "cache" => "Defines caching strategy",
945+ "indexed" => "Marks this field as indexed",
946+ "sensitive" => "Marks this field as containing sensitive data (e.g., PII)",
947+ _ => "Custom annotation",
948+ };
949+950+ contents.push(MarkedString::String(description.to_string()));
951+952+ // Add information about selectors if present
953+ if !annotation.selectors.is_empty() {
954+ let selector_info = format!(
955+ "This annotation applies to: {}",
956+ annotation.selectors.iter()
957+ .map(|s| s.name.as_str())
958+ .collect::<Vec<_>>()
959+ .join(", ")
960+ );
961+ contents.push(MarkedString::String(selector_info));
962+ } else {
963+ contents.push(MarkedString::String(
964+ "This annotation is visible to all generators".to_string()
965+ ));
966+ }
967+968+ return Ok(Some(Hover {
969+ contents: HoverContents::Array(contents),
970+ range: None,
971+ }));
972+ }
973+ }
974+ }
975+976+ // Also check field annotations
977+ match item {
978+ Item::Record(r) => {
979+ for field in &r.fields {
980+ for annotation in &field.annotations {
981+ if annotation.span.start <= offset && offset <= annotation.span.end {
982+ let annotation_display = if annotation.selectors.is_empty() {
983+ format!("@{}", annotation.name.name)
984+ } else {
985+ let selector_names: Vec<_> = annotation.selectors.iter()
986+ .map(|s| s.name.as_str())
987+ .collect();
988+ format!("@{}:{}", selector_names.join(","), annotation.name.name)
989+ };
990+991+ return Ok(Some(Hover {
992+ contents: HoverContents::Scalar(
993+ MarkedString::LanguageString(LanguageString {
994+ language: "mlf".to_string(),
995+ value: format!("Field annotation: {}", annotation_display),
996+ })
997+ ),
998+ range: None,
999+ }));
1000+ }
1001+ }
1002+ }
1003+ }
1004+ _ => {}
1005+ }
1006+ }
1007+1008 // Find the item at this position
1009 if let Some(item) = find_item_at_offset(lexicon, offset) {
1010 let name = get_item_name(item);
···1369 }
13701371 tracing::debug!("Total completions: {}", completions.len());
1372+ }
1373+1374+ MlfCompletionContext::Annotation => {
1375+ // Suggest common annotation names
1376+ let annotations = vec![
1377+ ("deprecated", "Mark as deprecated"),
1378+ ("main", "Main definition for conflict resolution"),
1379+ ("key", "Specify record key type (e.g., @key(\"literal:self\"))"),
1380+ ("encoding", "Specify MIME type encoding (e.g., @encoding(\"application/cbor\"))"),
1381+ ("since", "Version when added (e.g., @since(1, 2, 0))"),
1382+ ("doc", "Documentation URL (e.g., @doc(\"https://example.com\"))"),
1383+ ("validate", "Validation rules (e.g., @validate(min: 0, max: 100))"),
1384+ ("cache", "Caching strategy (e.g., @cache(ttl: 3600))"),
1385+ ("indexed", "Mark field as indexed"),
1386+ ("sensitive", "Mark field as containing sensitive data"),
1387+ ];
1388+1389+ for (label, detail) in annotations {
1390+ completions.push(CompletionItem {
1391+ label: label.to_string(),
1392+ kind: Some(CompletionItemKind::FUNCTION),
1393+ detail: Some(detail.to_string()),
1394+ ..Default::default()
1395+ });
1396+ }
1397+ }
1398+1399+ MlfCompletionContext::AnnotationSelector => {
1400+ // Suggest generator selector names
1401+ let generators = vec![
1402+ ("rust", "Rust code generator"),
1403+ ("typescript", "TypeScript code generator"),
1404+ ("go", "Go code generator"),
1405+ ("python", "Python code generator"),
1406+ ("java", "Java code generator"),
1407+ ];
1408+1409+ for (label, detail) in generators {
1410+ completions.push(CompletionItem {
1411+ label: label.to_string(),
1412+ kind: Some(CompletionItemKind::MODULE),
1413+ detail: Some(detail.to_string()),
1414+ ..Default::default()
1415+ });
1416+ }
1417 }
14181419 MlfCompletionContext::TopLevel => {