Enable partial acceptance

This commit is contained in:
Oliver Azevedo Barnes 2025-07-09 21:53:49 +01:00
parent 2942f4aace
commit cb9d2d40b8
No known key found for this signature in database
2 changed files with 189 additions and 1 deletions

View file

@ -373,3 +373,46 @@ impl EditPredictionProvider for FakeInlineCompletionProvider {
self.completion.clone() self.completion.clone()
} }
} }
#[gpui::test]
async fn test_partial_accept_inline_completion(cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {});
let mut cx = EditorTestContext::new(cx).await;
let provider = cx.new(|_| FakeInlineCompletionProvider::default());
assign_editor_completion_provider(provider.clone(), &mut cx);
cx.set_state("let x = ˇ;");
// Propose a completion with multiple words
propose_edits(
&provider,
vec![(Point::new(0, 8)..Point::new(0, 8), "hello world")],
&mut cx,
);
cx.update_editor(|editor, window, cx| editor.update_visible_inline_completion(window, cx));
// Verify the completion is shown
cx.assert_editor_state("let x = ˇ;");
cx.editor(|editor, _, _| {
assert!(editor.has_active_inline_completion());
});
// Accept partial completion - should accept first word
cx.update_editor(|editor, window, cx| {
editor.accept_partial_inline_completion(&Default::default(), window, cx);
});
// Test documents current behavior - this shows the issue with partial accepts
// The fake provider doesn't adjust for what's already been typed
cx.assert_editor_state("let x = helloˇ;");
cx.editor(|editor, _, _| {
// For providers that don't handle partial accepts properly,
// the completion might still be active but suggesting the wrong thing
println!(
"Has active completion after partial accept: {}",
editor.has_active_inline_completion()
);
});
}

View file

@ -199,11 +199,34 @@ impl EditPredictionProvider for OllamaCompletionProvider {
} }
let buffer_snapshot = buffer.read(cx); let buffer_snapshot = buffer.read(cx);
let cursor_offset = cursor_position.to_offset(buffer_snapshot);
// Get text before cursor to check what's already been typed
let text_before_cursor = buffer_snapshot
.text_for_range(0..cursor_offset)
.collect::<String>();
// Find how much of the completion has already been typed by checking
// if the text before the cursor ends with a prefix of our completion
let mut prefix_len = 0;
for i in 1..=completion_text.len().min(text_before_cursor.len()) {
if text_before_cursor.ends_with(&completion_text[..i]) {
prefix_len = i;
}
}
// Only suggest the remaining part of the completion
let remaining_completion = &completion_text[prefix_len..];
if remaining_completion.trim().is_empty() {
return None;
}
let position = cursor_position.bias_right(buffer_snapshot); let position = cursor_position.bias_right(buffer_snapshot);
Some(InlineCompletion { Some(InlineCompletion {
id: None, id: None,
edits: vec![(position..position, completion_text)], edits: vec![(position..position, remaining_completion.to_string())],
edit_preview: None, edit_preview: None,
}) })
} }
@ -429,4 +452,126 @@ mod tests {
// Test that Ollama provider shows completions in menu to enable hover icon // Test that Ollama provider shows completions in menu to enable hover icon
assert!(OllamaCompletionProvider::show_completions_in_menu()); assert!(OllamaCompletionProvider::show_completions_in_menu());
} }
#[gpui::test]
async fn test_partial_accept_behavior(cx: &mut TestAppContext) {
let provider = cx.new(|_| {
OllamaCompletionProvider::new(
Arc::new(FakeHttpClient::with_404_response()),
"http://localhost:11434".to_string(),
"codellama:7b".to_string(),
None,
)
});
let buffer_text = "let x = ";
let buffer = cx.new(|cx| language::Buffer::local(buffer_text, cx));
// Set up a completion with multiple words
provider.update(cx, |provider, _| {
provider.current_completion = Some("hello world".to_string());
provider.buffer_id = Some(buffer.entity_id());
});
let cursor_position = cx.read(|cx| buffer.read(cx).anchor_after(text::Point::new(0, 8)));
// First suggestion should return the full completion
let completion = provider.update(cx, |provider, cx| {
provider.suggest(&buffer, cursor_position, cx)
});
assert!(completion.is_some());
let completion = completion.unwrap();
assert_eq!(completion.edits.len(), 1);
assert_eq!(completion.edits[0].1, "hello world");
// Simulate what happens after partial accept - cursor moves forward
let buffer_text_after_partial = "let x = hello";
let buffer_after_partial =
cx.new(|cx| language::Buffer::local(buffer_text_after_partial, cx));
let cursor_position_after = cx.read(|cx| {
buffer_after_partial
.read(cx)
.anchor_after(text::Point::new(0, 13))
});
// Update provider to track the new buffer
provider.update(cx, |provider, _| {
provider.buffer_id = Some(buffer_after_partial.entity_id());
});
// The provider should now adjust its completion based on what's already been typed
let completion_after = provider.update(cx, |provider, cx| {
provider.suggest(&buffer_after_partial, cursor_position_after, cx)
});
// With the fix, the provider should only suggest the remaining part " world"
assert!(completion_after.is_some());
let completion_after = completion_after.unwrap();
assert_eq!(completion_after.edits[0].1, " world");
// Test another partial accept scenario
let buffer_text_final = "let x = hello world";
let buffer_final = cx.new(|cx| language::Buffer::local(buffer_text_final, cx));
let cursor_position_final =
cx.read(|cx| buffer_final.read(cx).anchor_after(text::Point::new(0, 19)));
provider.update(cx, |provider, _| {
provider.buffer_id = Some(buffer_final.entity_id());
});
// Should return None since the full completion is already typed
let completion_final = provider.update(cx, |provider, cx| {
provider.suggest(&buffer_final, cursor_position_final, cx)
});
assert!(completion_final.is_none());
}
#[gpui::test]
async fn test_partial_accept_with_non_word_characters(cx: &mut TestAppContext) {
let provider = cx.new(|_| {
OllamaCompletionProvider::new(
Arc::new(FakeHttpClient::with_404_response()),
"http://localhost:11434".to_string(),
"codellama:7b".to_string(),
None,
)
});
let buffer_text = "console.";
let buffer = cx.new(|cx| language::Buffer::local(buffer_text, cx));
// Set up a completion with method call
provider.update(cx, |provider, _| {
provider.current_completion = Some("log('test')".to_string());
provider.buffer_id = Some(buffer.entity_id());
});
let cursor_position = cx.read(|cx| buffer.read(cx).anchor_after(text::Point::new(0, 8)));
// First suggestion should return the full completion
let completion = provider.update(cx, |provider, cx| {
provider.suggest(&buffer, cursor_position, cx)
});
assert!(completion.is_some());
let completion = completion.unwrap();
assert_eq!(completion.edits[0].1, "log('test')");
// Simulate partial typing of "log"
let buffer_text_after = "console.log";
let buffer_after = cx.new(|cx| language::Buffer::local(buffer_text_after, cx));
let cursor_position_after =
cx.read(|cx| buffer_after.read(cx).anchor_after(text::Point::new(0, 11)));
provider.update(cx, |provider, _| {
provider.buffer_id = Some(buffer_after.entity_id());
});
// Should suggest the remaining part "('test')"
let completion_after = provider.update(cx, |provider, cx| {
provider.suggest(&buffer_after, cursor_position_after, cx)
});
assert!(completion_after.is_some());
let completion_after = completion_after.unwrap();
assert_eq!(completion_after.edits[0].1, "('test')");
}
} }