endpoint 2.0 dysnomia.ptr.pet
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

fixup and refactor completion, should work properly for blocks and closures now

dawn 15375ed6 6c915e4a

+464 -120
+464 -120
src/completion.rs
··· 55 55 (working_set, shapes, global_offset) 56 56 }; 57 57 58 - // 2. Identify context 59 - let mut is_command_pos = false; 60 - let mut current_span = Span::new(byte_pos, byte_pos); 61 - let mut prefix = "".to_string(); 62 - let mut found_shape = false; 58 + // Initial state logging 59 + web_sys::console::log_1(&JsValue::from_str(&format!( 60 + "[completion] Input: {:?}, JS cursor: {}, byte cursor: {}", 61 + input, js_cursor_pos, byte_pos 62 + ))); 63 + web_sys::console::log_1(&JsValue::from_str(&format!( 64 + "[completion] Found {} shapes, global_offset: {}", 65 + shapes.len(), 66 + global_offset 67 + ))); 68 + for (idx, (span, shape)) in shapes.iter().enumerate() { 69 + let (local_start, local_end) = ( 70 + span.start.saturating_sub(global_offset), 71 + span.end.saturating_sub(global_offset), 72 + ); 73 + web_sys::console::log_1(&JsValue::from_str(&format!( 74 + "[completion] Shape {}: {:?} at [{}, {}] (local: [{}, {}])", 75 + idx, shape, span.start, span.end, local_start, local_end 76 + ))); 77 + } 78 + 79 + // Helper functions 80 + let is_separator_char = |c: char| -> bool { ['|', ';', '(', '{'].contains(&c) }; 81 + 82 + let is_command_separator_char = |c: char| -> bool { ['|', ';'].contains(&c) }; 83 + 84 + let has_separator_between = |start: usize, end: usize| -> bool { 85 + if start < end && start < input.len() { 86 + let text_between = &input[start..std::cmp::min(end, input.len())]; 87 + text_between.chars().any(|c| is_separator_char(c)) 88 + } else { 89 + false 90 + } 91 + }; 92 + 93 + let find_last_separator_pos = |text: &str| -> Option<usize> { 94 + text.rfind(|c| is_command_separator_char(c)).map(|i| i + 1) 95 + }; 63 96 64 - // Check if cursor is inside or touching a shape 65 - for (span, shape) in &shapes { 66 - // Convert global span to local indices 67 - let local_start = span.start.saturating_sub(global_offset); 68 - let local_end = span.end.saturating_sub(global_offset); 69 - let local_span = Span::new(local_start, local_end); 97 + let ends_with_separator = |text: &str| -> bool { 98 + let text = text.trim_end(); 99 + text.ends_with('|') || text.ends_with(';') 100 + }; 70 101 71 - if local_span.contains(byte_pos) || local_span.end == byte_pos { 72 - current_span = local_span; 73 - found_shape = true; 74 - let safe_end = std::cmp::min(local_span.end, byte_pos); 102 + let to_local_span = |span: Span| -> Span { 103 + Span::new( 104 + span.start.saturating_sub(global_offset), 105 + span.end.saturating_sub(global_offset), 106 + ) 107 + }; 108 + 109 + let safe_slice = |span: Span| -> String { 110 + (span.start < input.len()) 111 + .then(|| { 112 + let safe_end = std::cmp::min(span.end, input.len()); 113 + input[span.start..safe_end].to_string() 114 + }) 115 + .unwrap_or_default() 116 + }; 117 + 118 + let is_command_shape = |shape: &FlatShape, local_span: Span| -> bool { 119 + matches!( 120 + shape, 121 + FlatShape::External(_) | FlatShape::InternalCall(_) | FlatShape::Keyword 122 + ) || matches!(shape, FlatShape::Garbage) && { 75 123 if local_span.start < input.len() { 76 - prefix = input[local_span.start..safe_end].to_string(); 124 + let prev_text = &safe_slice(local_span); 125 + !prev_text.trim().starts_with('-') 126 + } else { 127 + false 77 128 } 129 + } 130 + }; 78 131 79 - match shape { 80 - FlatShape::External(_) | FlatShape::InternalCall(_) | FlatShape::Keyword => { 81 - is_command_pos = true; 82 - } 83 - FlatShape::Garbage => { 84 - // Check if it looks like a flag 85 - if prefix.starts_with('-') { 86 - is_command_pos = false; 132 + let handle_block_prefix = |prefix: &str, span: Span| -> Option<(String, Span, bool)> { 133 + let mut block_prefix = prefix; 134 + let mut block_span_start = span.start; 135 + 136 + // Remove leading '{' and whitespace 137 + if block_prefix.starts_with('{') { 138 + block_prefix = &block_prefix[1..]; 139 + block_span_start += 1; 140 + } 141 + let trimmed_block_prefix = block_prefix.trim_start(); 142 + if trimmed_block_prefix != block_prefix { 143 + // Adjust span start to skip whitespace 144 + block_span_start += block_prefix.len() - trimmed_block_prefix.len(); 145 + } 146 + 147 + let is_empty = trimmed_block_prefix.is_empty(); 148 + Some(( 149 + trimmed_block_prefix.to_string(), 150 + Span::new(block_span_start, span.end), 151 + is_empty, 152 + )) 153 + }; 154 + 155 + // Helper function to handle both Block and Closure shapes 156 + let handle_block_or_closure = |prefix: &str, 157 + span: Span, 158 + shape_name: &str| 159 + -> Option<CompletionContext> { 160 + web_sys::console::log_1(&JsValue::from_str(&format!( 161 + "[completion] Processing {} shape with prefix: {:?}", 162 + shape_name, prefix 163 + ))); 164 + 165 + // Check if the content ends with a pipe or semicolon 166 + let prefix_ends_with_separator = ends_with_separator(prefix); 167 + let last_sep_pos_in_prefix = if prefix_ends_with_separator { 168 + find_last_separator_pos(prefix) 169 + } else { 170 + None 171 + }; 172 + web_sys::console::log_1(&JsValue::from_str(&format!( 173 + "[completion] {}: prefix_ends_with_separator={}, last_sep_pos_in_prefix={:?}", 174 + shape_name, prefix_ends_with_separator, last_sep_pos_in_prefix 175 + ))); 176 + 177 + if let Some((trimmed_prefix, adjusted_span, is_empty)) = handle_block_prefix(prefix, span) { 178 + web_sys::console::log_1(&JsValue::from_str(&format!( 179 + "[completion] {}: trimmed_prefix={:?}, is_empty={}", 180 + shape_name, trimmed_prefix, is_empty 181 + ))); 182 + 183 + if is_empty { 184 + // Empty block/closure or just whitespace - command context 185 + web_sys::console::log_1(&JsValue::from_str(&format!( 186 + "[completion] {} is empty, setting Command context", 187 + shape_name 188 + ))); 189 + Some(CompletionContext::Command { 190 + prefix: String::new(), 191 + span: adjusted_span, 192 + }) 193 + } else if let Some(last_sep_pos) = last_sep_pos_in_prefix { 194 + // After a separator - command context 195 + let after_sep = prefix[last_sep_pos..].trim_start(); 196 + web_sys::console::log_1(&JsValue::from_str(&format!( 197 + "[completion] {} has separator at {}, after_sep={:?}, setting Command context", 198 + shape_name, last_sep_pos, after_sep 199 + ))); 200 + Some(CompletionContext::Command { 201 + prefix: after_sep.to_string(), 202 + span: Span::new(span.start + last_sep_pos, span.end), 203 + }) 204 + } else { 205 + web_sys::console::log_1(&JsValue::from_str(&format!( 206 + "[completion] {} has no separator, setting Argument context", 207 + shape_name 208 + ))); 209 + Some(CompletionContext::Argument { 210 + prefix: trimmed_prefix, 211 + span: adjusted_span, 212 + }) 213 + } 214 + } else { 215 + None 216 + } 217 + }; 218 + 219 + // Find what we're completing 220 + #[derive(Debug)] 221 + enum CompletionContext { 222 + Command { prefix: String, span: Span }, 223 + Argument { prefix: String, span: Span }, 224 + } 225 + 226 + let mut context: Option<CompletionContext> = None; 227 + 228 + // Helper function to build full command prefix by looking backwards through shapes 229 + let build_command_prefix = 230 + |current_idx: usize, current_local_span: Span, current_prefix: &str| -> (String, Span) { 231 + let mut span_start = current_local_span.start; 232 + 233 + // Look backwards through shapes to find previous command words 234 + for i in (0..current_idx).rev() { 235 + if let Some((prev_span, prev_shape)) = shapes.get(i) { 236 + let prev_local_span = to_local_span(*prev_span); 237 + 238 + if is_command_shape(prev_shape, prev_local_span) { 239 + // Check if there's a separator between this shape and the next one 240 + let next_shape_start = if i + 1 < shapes.len() { 241 + to_local_span(shapes[i + 1].0).start 242 + } else { 243 + current_local_span.start 244 + }; 245 + 246 + // Check if there's a separator (pipe, semicolon, etc.) between shapes 247 + // Whitespace is fine, but separators indicate a new command 248 + if has_separator_between(prev_local_span.end, next_shape_start) { 249 + break; // Stop at separator 250 + } 251 + 252 + // Update span start to include this command word 253 + span_start = prev_local_span.start; 87 254 } else { 88 - // Assume command if it's garbage but not a flag (e.g. typing a new command) 89 - is_command_pos = true; 255 + // Not a command shape, stop looking backwards 256 + break; 90 257 } 91 258 } 92 - _ => { 93 - is_command_pos = false; 259 + } 260 + 261 + // Extract the full prefix from the input, preserving exact spacing 262 + let span_end = current_local_span.end; 263 + let full_prefix = if span_start < input.len() { 264 + safe_slice(Span::new(span_start, span_end)) 265 + } else { 266 + current_prefix.to_string() 267 + }; 268 + 269 + (full_prefix, Span::new(span_start, span_end)) 270 + }; 271 + 272 + // First, check if cursor is within a shape 273 + for (idx, (span, shape)) in shapes.iter().enumerate() { 274 + let local_span = to_local_span(*span); 275 + 276 + if local_span.start <= byte_pos && byte_pos <= local_span.end { 277 + web_sys::console::log_1(&JsValue::from_str(&format!( 278 + "[completion] Cursor in shape {}: {:?} at {:?}", 279 + idx, shape, local_span 280 + ))); 281 + 282 + // Check if there's a pipe or semicolon between this shape's end and the cursor 283 + // If so, we're starting a new command and should ignore this shape 284 + let has_sep = has_separator_between(local_span.end, byte_pos); 285 + if has_sep { 286 + web_sys::console::log_1(&JsValue::from_str(&format!( 287 + "[completion] Separator found between shape end ({}) and cursor ({}), skipping shape", 288 + local_span.end, byte_pos 289 + ))); 290 + // There's a separator, so we're starting a new command - skip this shape 291 + continue; 292 + } 293 + 294 + let span = Span::new(local_span.start, std::cmp::min(local_span.end, byte_pos)); 295 + let prefix = safe_slice(span); 296 + web_sys::console::log_1(&JsValue::from_str(&format!( 297 + "[completion] Processing shape {} with prefix: {:?}", 298 + idx, prefix 299 + ))); 300 + 301 + // Special case: if prefix is just '{' (possibly with whitespace), 302 + // we're at the start of a block and should complete commands 303 + let trimmed_prefix = prefix.trim(); 304 + if trimmed_prefix == "{" { 305 + // We're right after '{' - command context 306 + if let Some((_, adjusted_span, _)) = handle_block_prefix(&prefix, span) { 307 + context = Some(CompletionContext::Command { 308 + prefix: String::new(), 309 + span: adjusted_span, 310 + }); 311 + } 312 + } else { 313 + match shape { 314 + _ if is_command_shape(shape, local_span) => { 315 + let (full_prefix, full_span) = build_command_prefix(idx, span, &prefix); 316 + context = Some(CompletionContext::Command { 317 + prefix: full_prefix, 318 + span: full_span, 319 + }); 320 + } 321 + FlatShape::Block | FlatShape::Closure => { 322 + if let Some(ctx) = handle_block_or_closure( 323 + &prefix, 324 + span, 325 + shape.as_str().trim_start_matches("shape_"), 326 + ) { 327 + context = Some(ctx); 328 + } 329 + } 330 + _ => { 331 + context = Some(CompletionContext::Argument { prefix, span }); 332 + } 94 333 } 95 334 } 96 335 break; 97 336 } 98 337 } 99 338 100 - let cmds = 101 - working_set.find_commands_by_predicate(|value| value.starts_with(prefix.as_bytes()), true); 102 - drop(working_set); 103 - drop(engine_guard); 339 + // If not in a shape, check what comes before the cursor 340 + if context.is_none() { 341 + web_sys::console::log_1(&JsValue::from_str( 342 + "[completion] Context is None, entering fallback logic", 343 + )); 344 + // Check if there's a command-like shape before us 345 + let mut found_command_before = false; 346 + let mut has_separator_after_command = false; 347 + for (span, shape) in shapes.iter().rev() { 348 + let local_span = to_local_span(*span); 349 + if local_span.end <= byte_pos { 350 + if is_command_shape(shape, local_span) { 351 + // Check if there's a pipe or semicolon between this command and the cursor 352 + has_separator_after_command = has_separator_between(local_span.end, byte_pos); 353 + web_sys::console::log_1(&JsValue::from_str(&format!( 354 + "[completion] Found command shape {:?} at {:?}, has_separator_after_command={}", 355 + shape, local_span, has_separator_after_command 356 + ))); 357 + if !has_separator_after_command { 358 + found_command_before = true; 104 359 105 - // Fallback to Lexer if in whitespace 106 - if !found_shape { 107 - let (tokens, _err) = lex(input.as_bytes(), 0, &[], &[], true); 108 - let last_token = tokens.iter().filter(|t| t.span.end <= byte_pos).last(); 360 + // Extract the command text 361 + let cmd = safe_slice(local_span); 362 + web_sys::console::log_1(&JsValue::from_str(&format!( 363 + "[completion] Set Command context with prefix: {:?}", 364 + cmd 365 + ))); 109 366 110 - if let Some(token) = last_token { 111 - match token.contents { 112 - TokenContents::Pipe 113 - | TokenContents::PipePipe 114 - | TokenContents::Semicolon 115 - | TokenContents::Eol => { 116 - is_command_pos = true; 117 - } 118 - _ => { 119 - let text = &input[token.span.start..token.span.end]; 120 - if text == "{" || text == "(" || text == ";" || text == "&&" || text == "||" { 121 - is_command_pos = true; 122 - } else { 123 - is_command_pos = false; 367 + // We're after a command, complete with that command as prefix 368 + context = Some(CompletionContext::Command { 369 + prefix: cmd, 370 + span: local_span, 371 + }); 124 372 } 125 373 } 374 + break; 126 375 } 127 - } else { 128 - is_command_pos = true; // Start of input 376 + } 377 + 378 + if !found_command_before { 379 + web_sys::console::log_1(&JsValue::from_str( 380 + "[completion] No command found before cursor, checking tokens", 381 + )); 382 + // No command before, check context from tokens 383 + let (tokens, _) = lex(input.as_bytes(), 0, &[], &[], true); 384 + let last_token = tokens.iter().filter(|t| t.span.end <= byte_pos).last(); 385 + 386 + let is_cmd_context = if let Some(token) = last_token { 387 + let matches = matches!( 388 + token.contents, 389 + TokenContents::Pipe 390 + | TokenContents::PipePipe 391 + | TokenContents::Semicolon 392 + | TokenContents::Eol 393 + ); 394 + web_sys::console::log_1(&JsValue::from_str(&format!( 395 + "[completion] Last token: {:?}, is_cmd_context from token={}", 396 + token.contents, matches 397 + ))); 398 + matches 399 + } else { 400 + web_sys::console::log_1(&JsValue::from_str( 401 + "[completion] No last token found, assuming start of input (is_cmd_context=true)", 402 + )); 403 + true // Start of input 404 + }; 405 + 406 + // Look for the last non-whitespace token before cursor 407 + let text_before = &input[..byte_pos]; 408 + 409 + // Also check if we're inside a block - if the last non-whitespace char before cursor is '{' 410 + let text_before_trimmed = text_before.trim_end(); 411 + let is_inside_block = text_before_trimmed.ends_with('{'); 412 + // If we found a separator after a command, we're starting a new command 413 + let is_cmd_context = is_cmd_context || is_inside_block || has_separator_after_command; 414 + web_sys::console::log_1(&JsValue::from_str(&format!( 415 + "[completion] is_inside_block={}, has_separator_after_command={}, final is_cmd_context={}", 416 + is_inside_block, has_separator_after_command, is_cmd_context 417 + ))); 418 + 419 + // Find the last word before cursor 420 + let last_word_start = text_before 421 + .rfind(|c: char| c.is_whitespace() || is_separator_char(c)) 422 + .map(|i| i + 1) 423 + .unwrap_or(0); 424 + 425 + let last_word = text_before[last_word_start..].trim_start(); 426 + web_sys::console::log_1(&JsValue::from_str(&format!( 427 + "[completion] last_word_start={}, last_word={:?}", 428 + last_word_start, last_word 429 + ))); 430 + 431 + if is_cmd_context { 432 + context = Some(CompletionContext::Command { 433 + prefix: last_word.to_string(), 434 + span: Span::new(last_word_start, byte_pos), 435 + }); 436 + web_sys::console::log_1(&JsValue::from_str(&format!( 437 + "[completion] Set Command context with prefix: {:?}", 438 + last_word 439 + ))); 440 + } else { 441 + context = Some(CompletionContext::Argument { 442 + prefix: last_word.to_string(), 443 + span: Span::new(last_word_start, byte_pos), 444 + }); 445 + web_sys::console::log_1(&JsValue::from_str(&format!( 446 + "[completion] Set Argument context with prefix: {:?}", 447 + last_word 448 + ))); 449 + } 129 450 } 130 451 } 131 452 453 + web_sys::console::log_1(&JsValue::from_str(&format!("context: {:?}", context))); 454 + 132 455 let mut suggestions: Vec<Suggestion> = Vec::new(); 133 456 134 457 // Convert byte-spans back to char-spans for JS 135 - let to_char_span = |start: usize, end: usize| -> (usize, usize) { 136 - let char_start = input[..start].chars().count(); 137 - let char_end = input[..end].chars().count(); 138 - (char_start, char_end) 458 + let to_char_span = |span: Span| -> Span { 459 + let char_start = input[..span.start].chars().count(); 460 + let char_end = input[..span.end].chars().count(); 461 + Span::new(char_start, char_end) 139 462 }; 140 463 141 - let (span_start, span_end) = to_char_span(current_span.start, current_span.end); 464 + match context { 465 + Some(CompletionContext::Command { prefix, span }) => { 466 + web_sys::console::log_1(&JsValue::from_str(&format!( 467 + "[completion] Generating Command suggestions with prefix: {:?}", 468 + prefix 469 + ))); 470 + // Command completion 471 + let cmds = working_set 472 + .find_commands_by_predicate(|value| value.starts_with(prefix.as_bytes()), true); 142 473 143 - let mut add_cmd_suggestion = |name: Vec<u8>, desc: Option<String>| { 144 - let name = String::from_utf8_lossy(&name).to_string(); 145 - suggestions.push(Suggestion { 146 - rendered: { 147 - let name = ansi_term::Color::Green.bold().paint(&name); 148 - let desc = desc.as_deref().unwrap_or("<no description>"); 149 - format!("{name} {desc}") 150 - }, 151 - name: name.clone(), // Replacement text is just the name 152 - description: desc, 153 - is_command: true, 154 - span_start, 155 - span_end, 156 - }); 157 - }; 474 + let span = to_char_span(span); 475 + let mut cmd_count = 0; 158 476 159 - if is_command_pos { 160 - for (_, name, desc, _) in cmds { 161 - add_cmd_suggestion(name, desc); 477 + for (_, name, desc, _) in cmds { 478 + let name_str = String::from_utf8_lossy(&name).to_string(); 479 + suggestions.push(Suggestion { 480 + rendered: { 481 + let name_colored = ansi_term::Color::Green.bold().paint(&name_str); 482 + let desc_str = desc.as_deref().unwrap_or("<no description>"); 483 + format!("{name_colored} {desc_str}") 484 + }, 485 + name: name_str, 486 + description: desc, 487 + is_command: true, 488 + span_start: span.start, 489 + span_end: span.end, 490 + }); 491 + cmd_count += 1; 492 + } 493 + web_sys::console::log_1(&JsValue::from_str(&format!( 494 + "[completion] Found {} command suggestions", 495 + cmd_count 496 + ))); 162 497 } 163 - } else { 164 - // File completion 165 - // Split prefix into directory and file part 166 - let (dir, file_prefix) = if let Some(idx) = prefix.rfind('/') { 167 - (&prefix[..idx + 1], &prefix[idx + 1..]) 168 - } else { 169 - ("", prefix.as_str()) 170 - }; 498 + Some(CompletionContext::Argument { prefix, span }) => { 499 + web_sys::console::log_1(&JsValue::from_str(&format!( 500 + "[completion] Generating Argument suggestions with prefix: {:?}", 501 + prefix 502 + ))); 503 + // File completion 504 + let (dir, file_prefix) = prefix 505 + .rfind('/') 506 + .map(|idx| (&prefix[..idx + 1], &prefix[idx + 1..])) 507 + .unwrap_or(("", prefix.as_str())); 171 508 172 - // Fix: Clean up the directory path before joining. 173 - // VFS often fails if 'join' is called with a trailing slash (e.g. "a/") because it interprets it as "a" + "" 174 - let dir_to_join = if dir.len() > 1 && dir.ends_with('/') { 175 - &dir[..dir.len() - 1] 176 - } else { 177 - dir 178 - }; 509 + let dir_to_join = (dir.len() > 1 && dir.ends_with('/')) 510 + .then(|| &dir[..dir.len() - 1]) 511 + .unwrap_or(dir); 179 512 180 - let target_dir = if !dir.is_empty() { 181 - match root.join(dir_to_join) { 182 - Ok(d) => { 183 - if let Ok(true) = d.is_dir() { 184 - Some(d) 185 - } else { 186 - None 187 - } 513 + let target_dir = if !dir.is_empty() { 514 + match root.join(dir_to_join) { 515 + Ok(d) if d.is_dir().unwrap_or(false) => Some(d), 516 + _ => None, 188 517 } 189 - Err(_) => None, 190 - } 191 - } else { 192 - // If prefix is empty, list current directory 193 - Some(root.join("").unwrap()) 194 - }; 518 + } else { 519 + Some(root.join("").unwrap()) 520 + }; 521 + 522 + let mut file_count = 0; 523 + if let Some(d) = target_dir { 524 + if let Ok(iterator) = d.read_dir() { 525 + let span = to_char_span(span); 195 526 196 - if let Some(d) = target_dir { 197 - if let Ok(iterator) = d.read_dir() { 198 - for entry in iterator { 199 - let name = entry.filename(); 200 - if name.starts_with(file_prefix) { 201 - let full_completion = format!("{}{}", dir, name); 202 - suggestions.push(Suggestion { 203 - name: full_completion.clone(), 204 - description: None, 205 - is_command: false, 206 - rendered: full_completion, 207 - span_start, 208 - span_end, 209 - }) 527 + for entry in iterator { 528 + let name = entry.filename(); 529 + if name.starts_with(file_prefix) { 530 + let full_completion = format!("{}{}", dir, name); 531 + suggestions.push(Suggestion { 532 + name: full_completion.clone(), 533 + description: None, 534 + is_command: false, 535 + rendered: full_completion, 536 + span_start: span.start, 537 + span_end: span.end, 538 + }); 539 + file_count += 1; 540 + } 210 541 } 211 542 } 212 543 } 544 + web_sys::console::log_1(&JsValue::from_str(&format!( 545 + "[completion] Found {} file suggestions", 546 + file_count 547 + ))); 548 + } 549 + _ => { 550 + web_sys::console::log_1(&JsValue::from_str( 551 + "[completion] Context is None, no suggestions generated", 552 + )); 213 553 } 214 554 } 215 555 556 + drop(working_set); 557 + drop(engine_guard); 558 + 216 559 suggestions.sort(); 217 - 218 - serde_json::to_string(&suggestions).unwrap_or_else(|_| "[]".to_string()) 560 + let suggestions = serde_json::to_string(&suggestions).unwrap_or_else(|_| "[]".to_string()); 561 + web_sys::console::log_1(&JsValue::from_str(&suggestions)); 562 + suggestions 219 563 }