Compare commits

...

7 Commits

Author SHA1 Message Date
Nathan Sobo
b53e7635d9 Refine insertion prompt and comment out logging 2024-08-17 00:14:52 -06:00
Nathan Sobo
0d976fd773 Do not indent via the model 2024-08-16 23:58:42 -06:00
Nathan Sobo
97216bbf5c WIP 2024-08-16 23:30:06 -06:00
Nathan Sobo
6b8c3d42b3 WIP 2024-08-16 22:50:43 -06:00
Nathan Sobo
0d1a6b9c83 WIP 2024-08-16 22:49:49 -06:00
Nathan Sobo
2373a60de5 WIP 2024-08-16 20:24:43 -06:00
Nathan Sobo
f25e85b639 WIP: Radically simplify our inline transformation prompt.
Co-Authored-By: Max <max@zed.dev>
2024-08-16 18:09:26 -06:00
9 changed files with 425 additions and 106 deletions

View File

@@ -0,0 +1,79 @@
You are an assistant in a code editor, helping a developer to edit their code.
<task>
Output {{ content_type }} to insert into the given `<document>` in accordance with the given `<prompt>` tag.
Generate output to be inserted prior to the special ⎀ character.
</task>
<rule>
You are only to insert NEW {{{ content_type }}} based on the specified `<prompt>` tag.
DO NOT repeat any content that surrounds the ⎀ character
DO NOT repeat the ⎀ character itself.
<rule_example>
<example_input>
<document language="Rust">
impl Foo {
fn bar() {}
}
</document>
<prompt>Document method</prompt>
</example_input>
<bad_output failure="Not a pure insert. Repeated content surrounding ⎀">
// Example method
// This is an example of a doc commment
// It could be multiple lines
fn bar() {}
</bad_output>
<bad_output failure="Repeated ⎀ in output">
// Example method
// This is an example of a doc commment
// It could be multiple lines⎀
</bad_output>
<good_output success="Generated ONLY new code based on the prompt tag">
// Example method
// This is an example of a doc commment
// It could be multiple lines
</good_output>
</rule_example>
</rule>
{{#if truncated}}
<document language="{{{ language }}}" truncated="true">
{{else}}
<document language="{{{ language }}}">
{{/if}}
{{{ document_prefix }}}{{{ document_suffix }}}
</document>
{{#if truncated}}
<note>
The contents of the document tag above have been truncated for brevity.
</note>
{{/if}}
<instruction>
Focus on inserting {{{ content_type }}} at the location prior to the ⎀ character based on the `<prompt>` below.
</instruction>
Here's the excerpt from the document where you will perform the edit.
<document_excerpt>
{{{ context_prefix }}}{{{ context_suffix }}}
</document_excerpt>
<prompt>
{{{ prompt }}}
</prompt>
<rules>
- Your first token should be {{{ content_type }}} written in {{{ language }}}.
- DO NOT repeat existing {{{ content_type }}}.
- DO NOT output the ⎀ character.
- DO NOT add extra indentation to the generated content.
</rules>
<output_format>
Output pure {{{ language }}} with no commentary.
</output_format>

View File

@@ -0,0 +1,73 @@
You are an assistant in a code editor, helping a developer to edit their code.
<task>
Output code according to the given `<prompt>` tag, which should be inserted into the given `<document>` tag replacing the contents of the `<replaced_text>` tag.
</task>
<instructions>
* Maintain the original indentation level of the file in rewritten sections.
</instructions>
<document language="Python">
import pygame
import random
# Initialize Pygame
pygame.init()
# Set up the game window
width = 800
height = 600
window = pygame.display.set_mode((width, height))
pygame.display.set_caption("Snake Game")
# Colors
BLACK = (0, 0, 0)
WHITE = (255, 255, 255)
RED = (255, 0, 0)
GREEN = (0, 255, 0)
# Snake properties
snake_block = 20
snake_speed = 15
# Initialize the snake
snake = [(width // 2, height // 2)]
snake_direction = (0, -snake_block)
# Initialize the food
food = (random.randrange(0, width - snake_block, snake_block),
random.randrange(0, height - snake_block, snake_block))
# Set up the game clock
clock = pygame.time.Clock()
# Game loop
running = True
while running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
elif event.type == pygame.KEYDOWN:
if <replaced_text>event.key == pygame.K_UP and snake_direction != (0, snake_block):
snake_direction = (0, -snake_block)
elif event.key == pygame.K_DOWN and snake_direction != (0, -snake_block):</replaced_text>
snake_direction = (0, snake_block)
elif event.key == pygame.K_LEFT and snake_direction != (snake_block, 0):
snake_direction = (-snake_block, 0)
elif event.key == pygame.K_RIGHT and snake_direction != (-snake_block, 0):
snake_direction = (snake_block, 0)</replaced_text>
# Move the snake
new_head = (snake[0][0] + snake_direction[0], snake[0][1] +
</document>
<prompt>
Apply demorgans
</prompt>
<directive>
Output code immediately. No commentary. Your first token should be in Python.
</directive>
event.key == pygame.K_UP and not snake_direction == (0, snake_block):
snake_direction = (0, -snake_block)
elif event.key == pygame.K_DOWN and not snake_direction == (0, -snake_block):

View File

@@ -28,7 +28,7 @@ use gpui::{
FontWeight, Global, HighlightStyle, Model, ModelContext, Subscription, Task, TextStyle,
UpdateGlobal, View, ViewContext, WeakView, WindowContext,
};
use language::{Buffer, IndentKind, Point, TransactionId};
use language::{Buffer, Point, TransactionId};
use language_model::{
LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage, Role,
};
@@ -46,7 +46,6 @@ use std::{
task::{self, Poll},
time::{Duration, Instant},
};
use text::OffsetRangeExt as _;
use theme::ThemeSettings;
use ui::{prelude::*, CheckboxWithLabel, IconButtonShape, Popover, Tooltip};
use util::{RangeExt, ResultExt};
@@ -73,6 +72,7 @@ const PROMPT_HISTORY_MAX_LEN: usize = 20;
pub struct InlineAssistant {
next_assist_id: InlineAssistId,
next_assist_group_id: InlineAssistGroupId,
assists: HashMap<InlineAssistId, InlineAssist>,
assists_by_editor: HashMap<WeakView<Editor>, EditorInlineAssists>,
assist_groups: HashMap<InlineAssistGroupId, InlineAssistGroup>,
@@ -154,40 +154,51 @@ impl InlineAssistant {
.map(|range| range.to_point(&snapshot))
.collect::<Vec<Range<Point>>>();
for selection_range in selection_ranges {
let selection_is_newest = newest_selection_range.contains_inclusive(&selection_range);
let mut transform_range = selection_range.start..selection_range.end;
// Expand the transform range to start/end of lines.
// If a non-empty selection ends at the start of the last line, clip at the end of the penultimate line.
transform_range.start.column = 0;
if transform_range.end.column == 0 && transform_range.end > transform_range.start {
transform_range.end.row -= 1;
}
transform_range.end.column = snapshot.line_len(MultiBufferRow(transform_range.end.row));
let selection_range =
selection_range.start..selection_range.end.min(transform_range.end);
// If we intersect the previous transform range,
if let Some(CodegenRange {
transform_range: prev_transform_range,
selection_ranges,
focus_assist,
}) = codegen_ranges.last_mut()
{
if transform_range.start <= prev_transform_range.end {
prev_transform_range.end = transform_range.end;
selection_ranges.push(selection_range);
*focus_assist |= selection_is_newest;
continue;
}
}
if selection_ranges.len() == 1 {
let selection_range = &selection_ranges[0];
codegen_ranges.push(CodegenRange {
transform_range,
selection_ranges: vec![selection_range],
focus_assist: selection_is_newest,
})
transform_range: selection_range.clone(),
selection_ranges: vec![selection_range.clone()],
focus_assist: true,
});
} else {
for selection_range in selection_ranges {
let selection_is_newest =
newest_selection_range.contains_inclusive(&selection_range);
let mut transform_range = selection_range.start..selection_range.end;
// Expand the transform range to start/end of lines.
// If a non-empty selection ends at the start of the last line, clip at the end of the penultimate line.
transform_range.start.column = 0;
if transform_range.end.column == 0 && transform_range.end > transform_range.start {
transform_range.end.row -= 1;
}
transform_range.end.column =
snapshot.line_len(MultiBufferRow(transform_range.end.row));
let selection_range =
selection_range.start..selection_range.end.min(transform_range.end);
// If we intersect the previous transform range,
if let Some(CodegenRange {
transform_range: prev_transform_range,
selection_ranges,
focus_assist,
}) = codegen_ranges.last_mut()
{
if transform_range.start <= prev_transform_range.end {
prev_transform_range.end = transform_range.end;
selection_ranges.push(selection_range);
*focus_assist |= selection_is_newest;
continue;
}
}
codegen_ranges.push(CodegenRange {
transform_range,
selection_ranges: vec![selection_range],
focus_assist: selection_is_newest,
})
}
}
let assist_group_id = self.next_assist_group_id.post_inc();
@@ -1078,10 +1089,10 @@ impl InlineAssistant {
let mut new_blocks = Vec::new();
for (new_row, old_row_range) in deleted_row_ranges {
let (_, buffer_start) = old_snapshot
.point_to_buffer_offset(Point::new(*old_row_range.start(), 0))
.position_to_buffer_offset(Point::new(*old_row_range.start(), 0))
.unwrap();
let (_, buffer_end) = old_snapshot
.point_to_buffer_offset(Point::new(
.position_to_buffer_offset(Point::new(
*old_row_range.end(),
old_snapshot.line_len(MultiBufferRow(*old_row_range.end())),
))
@@ -2303,6 +2314,12 @@ impl Codegen {
} else {
let request = self.build_request(user_prompt, assistant_panel_context, cx)?;
// todo!
// println!(
// "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n{}",
// request
// );
let chunks =
cx.spawn(|_, cx| async move { model.stream_completion(request, &cx).await });
async move { Ok(chunks.await?.boxed()) }.boxed_local()
@@ -2317,8 +2334,8 @@ impl Codegen {
assistant_panel_context: Option<LanguageModelRequest>,
cx: &AppContext,
) -> Result<LanguageModelRequest> {
let buffer = self.buffer.read(cx).snapshot(cx);
let language = buffer.language_at(self.transform_range.start);
let multi_buffer = self.buffer.read(cx).snapshot(cx);
let language = multi_buffer.language_at(self.transform_range.start);
let language_name = if let Some(language) = language.as_ref() {
if Arc::ptr_eq(language, &language::PLAIN_TEXT) {
None
@@ -2343,54 +2360,55 @@ impl Codegen {
};
let language_name = language_name.as_deref();
let start = buffer.point_to_buffer_offset(self.transform_range.start);
let end = buffer.point_to_buffer_offset(self.transform_range.end);
let (transform_buffer, transform_range) = if let Some((start, end)) = start.zip(end) {
let (start_buffer, start_buffer_offset) = start;
let (end_buffer, end_buffer_offset) = end;
if start_buffer.remote_id() == end_buffer.remote_id() {
(start_buffer.clone(), start_buffer_offset..end_buffer_offset)
} else {
return Err(anyhow::anyhow!("invalid transformation range"));
}
} else {
return Err(anyhow::anyhow!("invalid transformation range"));
};
let mut transform_context_range = transform_range.to_point(&transform_buffer);
transform_context_range.start.row = transform_context_range.start.row.saturating_sub(3);
transform_context_range.start.column = 0;
transform_context_range.end =
(transform_context_range.end + Point::new(3, 0)).min(transform_buffer.max_point());
transform_context_range.end.column =
transform_buffer.line_len(transform_context_range.end.row);
let transform_context_range = transform_context_range.to_offset(&transform_buffer);
let start = multi_buffer.position_to_buffer_offset(self.transform_range.start);
let end = multi_buffer.position_to_buffer_offset(self.transform_range.end);
let buffer = start
.zip(end)
.and_then(|((start_buffer, _), (end_buffer, _))| {
if start_buffer.remote_id() == end_buffer.remote_id() {
Some(start_buffer.clone())
} else {
None
}
})
.ok_or_else(|| anyhow::anyhow!("invalid transformation range"))?;
let selected_ranges = self
.selected_ranges
.iter()
.filter_map(|selected_range| {
let start = buffer
.point_to_buffer_offset(selected_range.start)
let start = multi_buffer
.position_to_buffer_point(selected_range.start)
.map(|(_, offset)| offset)?;
let end = buffer
.point_to_buffer_offset(selected_range.end)
let end = multi_buffer
.position_to_buffer_point(selected_range.end)
.map(|(_, offset)| offset)?;
Some(start..end)
})
.collect::<Vec<_>>();
let prompt = self
.prompt_builder
.generate_content_prompt(
user_prompt,
language_name,
transform_buffer,
transform_range,
selected_ranges,
transform_context_range,
)
.map_err(|e| anyhow::anyhow!("Failed to generate content prompt: {}", e))?;
let inline_assist_prompt = if selected_ranges.len() == 1 {
let selected_range = &selected_ranges[0];
if selected_range.start == selected_range.end {
self.prompt_builder.build_insertion_prompt(
user_prompt,
language_name,
buffer,
selected_range.start,
)?
} else {
// Single non-empty range
// Implement logic for replacement
// TODO: Add replacement logic here
anyhow::bail!("Unsupported transformation!")
}
} else {
// Multiple ranges
// Implement logic for handling multiple ranges
// TODO: Add multiple range handling logic here
anyhow::bail!("Unsupported transformation!")
};
let mut messages = Vec::new();
if let Some(context_request) = assistant_panel_context {
@@ -2399,7 +2417,7 @@ impl Codegen {
messages.push(LanguageModelRequestMessage {
role: Role::User,
content: vec![prompt.into()],
content: vec![inline_assist_prompt.into()],
cache: false,
});
@@ -2423,25 +2441,15 @@ impl Codegen {
.collect::<Rope>();
let selection_start = edit_range.start.to_point(&snapshot);
let base_indent_size = snapshot.indent_size_for_line(MultiBufferRow(selection_start.row));
let indent_len_before_selection = base_indent_size.len.min(selection_start.column);
let base_indent = match base_indent_size.kind {
language::IndentKind::Space => " ".repeat(indent_len_before_selection as usize),
language::IndentKind::Tab => "\t".repeat(indent_len_before_selection as usize),
};
// Start with the indentation of the first line in the selection
let mut suggested_line_indent = snapshot
.suggested_indents(selection_start.row..=selection_start.row, cx)
.into_values()
.next()
.unwrap_or_else(|| snapshot.indent_size_for_line(MultiBufferRow(selection_start.row)));
// If the first line in the selection does not have indentation, check the following lines
if suggested_line_indent.len == 0 && suggested_line_indent.kind == IndentKind::Space {
for row in selection_start.row..=edit_range.end.to_point(&snapshot).row {
let line_indent = snapshot.indent_size_for_line(MultiBufferRow(row));
// Prefer tabs if a line in the selection uses tabs as indentation
if line_indent.kind == IndentKind::Tab {
suggested_line_indent.kind = IndentKind::Tab;
break;
}
}
}
let mut generated_text = String::new();
let mut raw_output = String::new();
let telemetry = self.telemetry.clone();
self.diff = Diff::default();
@@ -2467,7 +2475,17 @@ impl Codegen {
response_latency = Some(request_start.elapsed());
}
let chunk = chunk?;
let char_ops = diff.push_new(&chunk);
raw_output.push_str(&chunk);
let start_offset = generated_text.len();
for line in chunk.split_inclusive('\n') {
if generated_text.ends_with('\n') {
generated_text.push_str(&base_indent);
}
generated_text.push_str(line);
}
let char_ops = diff.push_new(&generated_text[start_offset..]);
line_diff.push_char_operations(&char_ops, &selected_text);
diff_tx
.send((char_ops, line_diff.line_operations()))
@@ -2485,6 +2503,10 @@ impl Codegen {
};
let result = diff.await;
// todo!
// println!("Base indent: {:?}", base_indent_size);
// println!("Raw output: {:?}", raw_output);
// println!("Generated text: {:?}", generated_text);
let error_message =
result.as_ref().err().map(|error| error.to_string());

View File

@@ -4,10 +4,23 @@ use futures::StreamExt;
use handlebars::{Handlebars, RenderError, TemplateError};
use language::BufferSnapshot;
use parking_lot::Mutex;
use rope::Point;
use serde::Serialize;
use std::{ops::Range, sync::Arc, time::Duration};
use std::{cmp, ops::Range, sync::Arc, time::Duration};
use util::ResultExt;
#[derive(Serialize)]
pub struct InsertionContext {
pub document_prefix: String,
pub document_suffix: String,
pub context_prefix: String,
pub context_suffix: String,
pub prompt: String,
pub language: String,
pub content_type: String,
pub truncated: bool,
}
#[derive(Serialize)]
pub struct ContentPromptContext {
pub content_type: String,
@@ -160,6 +173,7 @@ impl PromptBuilder {
.map_err(Box::new)
};
register_template("insertion")?;
register_template("content_prompt")?;
register_template("terminal_assistant_prompt")?;
register_template("edit_workflow")?;
@@ -168,6 +182,59 @@ impl PromptBuilder {
Ok(())
}
pub fn build_insertion_prompt(
&self,
user_prompt: String,
language_name: Option<&str>,
buffer: BufferSnapshot,
position: Point,
) -> Result<String, RenderError> {
let mut document_prefix = String::new();
for chunk in buffer.text_for_range(Point::zero()..position) {
document_prefix.push_str(chunk);
}
let mut document_suffix = String::new();
for chunk in buffer.text_for_range(position..buffer.max_point()) {
document_suffix.push_str(chunk);
}
// Get 5 lines of context above and below the insertion
let mut context_range = position..position;
context_range.start.row = context_range.start.row.saturating_sub(5);
context_range.start.column = 0;
context_range.end = cmp::min(context_range.end + Point::new(5, 0), buffer.max_point());
context_range.end.column = buffer.line_len(context_range.end.row);
let mut context_prefix = String::new();
let mut context_suffix = String::new();
for chunk in buffer.text_for_range(context_range.start..position) {
context_prefix.push_str(chunk);
}
for chunk in buffer.text_for_range(position..context_range.end) {
context_suffix.push_str(chunk);
}
let language = language_name.unwrap_or("Unknown").to_string();
let content_type = match language_name {
None | Some("Markdown" | "Plain Text") => "text".to_string(),
Some(_) => "code".to_string(),
};
let truncated = false; // Assuming no truncation for now
let context = InsertionContext {
document_prefix,
document_suffix,
context_prefix,
context_suffix,
prompt: user_prompt,
language,
content_type,
truncated,
};
self.handlebars.lock().render("insertion", &context)
}
pub fn generate_content_prompt(
&self,
user_prompt: String,

View File

@@ -152,6 +152,25 @@ pub struct IndentSize {
pub kind: IndentKind,
}
impl std::fmt::Display for IndentSize {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self.kind {
IndentKind::Space => {
for _ in 0..self.len {
write!(f, " ")?;
}
Ok(())
}
IndentKind::Tab => {
for _ in 0..self.len {
write!(f, "\t")?;
}
Ok(())
}
}
}
}
/// A whitespace character that's used for indentation.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub enum IndentKind {

View File

@@ -196,6 +196,24 @@ pub struct LanguageModelRequestMessage {
pub cache: bool,
}
impl std::fmt::Display for LanguageModelRequestMessage {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Role: {}\n", self.role)?;
write!(f, "Content:\n")?;
for (index, content) in self.content.iter().enumerate() {
match content {
MessageContent::Text(text) => write!(f, " {}: Text ({})\n", index, text)?,
MessageContent::Image(image) => write!(
f,
" {}: Image ({}x{})\n",
index, image.size.width.0, image.size.height.0
)?,
}
}
write!(f, "Cache: {}", self.cache)
}
}
impl LanguageModelRequestMessage {
pub fn string_contents(&self) -> String {
let mut string_buffer = String::new();
@@ -228,6 +246,20 @@ pub struct LanguageModelRequest {
pub temperature: f32,
}
impl std::fmt::Display for LanguageModelRequest {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
writeln!(f, "LanguageModelRequest {{")?;
writeln!(f, " messages: [")?;
for message in &self.messages {
writeln!(f, " {}", message)?;
}
writeln!(f, " ],")?;
writeln!(f, " stop: {:?},", self.stop)?;
writeln!(f, " temperature: {}", self.temperature)?;
write!(f, "}}")
}
}
impl LanguageModelRequest {
pub fn into_open_ai(self, model: String) -> open_ai::Request {
open_ai::Request {

View File

@@ -2650,11 +2650,11 @@ impl MultiBufferSnapshot {
}
}
pub fn point_to_buffer_offset<T: ToOffset>(
pub fn position_to_buffer_offset<T: ToOffset>(
&self,
point: T,
position: T,
) -> Option<(&BufferSnapshot, usize)> {
let offset = point.to_offset(self);
let offset = position.to_offset(self);
let mut cursor = self.excerpts.cursor::<usize>();
cursor.seek(&offset, Bias::Right, &());
if cursor.item().is_none() {
@@ -2668,6 +2668,24 @@ impl MultiBufferSnapshot {
})
}
pub fn position_to_buffer_point<T: ToPoint>(
&self,
position: T,
) -> Option<(&BufferSnapshot, Point)> {
let point = position.to_point(self);
let mut cursor = self.excerpts.cursor::<Point>();
cursor.seek(&point, Bias::Right, &());
if cursor.item().is_none() {
cursor.prev(&());
}
cursor.item().map(|excerpt| {
let excerpt_start = excerpt.range.context.start.to_point(&excerpt.buffer);
let buffer_point = excerpt_start + point - *cursor.start();
(&excerpt.buffer, buffer_point)
})
}
pub fn suggested_indents(
&self,
rows: impl IntoIterator<Item = u32>,
@@ -3448,12 +3466,12 @@ impl MultiBufferSnapshot {
}
pub fn file_at<T: ToOffset>(&self, point: T) -> Option<&Arc<dyn File>> {
self.point_to_buffer_offset(point)
self.position_to_buffer_offset(point)
.and_then(|(buffer, _)| buffer.file())
}
pub fn language_at<T: ToOffset>(&self, point: T) -> Option<&Arc<Language>> {
self.point_to_buffer_offset(point)
self.position_to_buffer_offset(point)
.and_then(|(buffer, offset)| buffer.language_at(offset))
}
@@ -3464,7 +3482,7 @@ impl MultiBufferSnapshot {
) -> &'a LanguageSettings {
let mut language = None;
let mut file = None;
if let Some((buffer, offset)) = self.point_to_buffer_offset(point) {
if let Some((buffer, offset)) = self.position_to_buffer_offset(point) {
language = buffer.language_at(offset);
file = buffer.file();
}
@@ -3472,7 +3490,7 @@ impl MultiBufferSnapshot {
}
pub fn language_scope_at<T: ToOffset>(&self, point: T) -> Option<LanguageScope> {
self.point_to_buffer_offset(point)
self.position_to_buffer_offset(point)
.and_then(|(buffer, offset)| buffer.language_scope_at(offset))
}
@@ -3481,7 +3499,7 @@ impl MultiBufferSnapshot {
position: T,
cx: &AppContext,
) -> Option<IndentSize> {
let (buffer_snapshot, offset) = self.point_to_buffer_offset(position)?;
let (buffer_snapshot, offset) = self.position_to_buffer_offset(position)?;
Some(buffer_snapshot.language_indent_size_at(offset, cx))
}

View File

@@ -1275,6 +1275,15 @@ mod tests {
max: u8,
}
/// Represents a summary of integers with various properties.
///
/// This struct provides a summary of a collection of integers, including:
/// - The count of integers
/// - The sum of all integers
/// - Whether the collection contains any even numbers
/// - The maximum value in the collection
///
/// It is used as the summary type for the `SumTree` when storing `u8` values.
#[derive(Ord, PartialOrd, Default, Eq, PartialEq, Clone, Debug)]
struct Count(usize);

View File

@@ -166,7 +166,7 @@ fn active_item_selection_properties(
let multi_buffer = editor.buffer().clone();
let multi_buffer_snapshot = multi_buffer.read(cx).snapshot(cx);
let (buffer_snapshot, buffer_offset) =
multi_buffer_snapshot.point_to_buffer_offset(selection.head())?;
multi_buffer_snapshot.position_to_buffer_offset(selection.head())?;
let buffer_anchor = buffer_snapshot.anchor_before(buffer_offset);
let buffer = multi_buffer.read(cx).buffer(buffer_snapshot.remote_id())?;
Some(Location {