Compare commits
9 Commits
fix-git-wo
...
login-logo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ccee5124a7 | ||
|
|
4c411b9fc8 | ||
|
|
5ac6ae501f | ||
|
|
c01f12b15d | ||
|
|
dfa066dfe8 | ||
|
|
ac8c653ae6 | ||
|
|
d2318be8d9 | ||
|
|
a026163746 | ||
|
|
ad3ddd381d |
@@ -1005,14 +1005,14 @@ impl ContextCompletion {
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, PartialEq)]
|
||||
struct SlashCommandCompletion {
|
||||
source_range: Range<usize>,
|
||||
command: Option<String>,
|
||||
argument: Option<String>,
|
||||
pub struct SlashCommandCompletion {
|
||||
pub source_range: Range<usize>,
|
||||
pub command: Option<String>,
|
||||
pub argument: Option<String>,
|
||||
}
|
||||
|
||||
impl SlashCommandCompletion {
|
||||
fn try_parse(line: &str, offset_to_line: usize) -> Option<Self> {
|
||||
pub fn try_parse(line: &str, offset_to_line: usize) -> Option<Self> {
|
||||
// If we decide to support commands that are not at the beginning of the prompt, we can remove this check
|
||||
if !line.starts_with('/') || offset_to_line != 0 {
|
||||
return None;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::{
|
||||
acp::completion_provider::ContextPickerCompletionProvider,
|
||||
acp::completion_provider::{ContextPickerCompletionProvider, SlashCommandCompletion},
|
||||
context_picker::{ContextPickerAction, fetch_context_picker::fetch_url_content},
|
||||
};
|
||||
use acp_thread::{MentionUri, selection_name};
|
||||
@@ -11,10 +11,10 @@ use assistant_slash_commands::codeblock_fence_for_path;
|
||||
use collections::{HashMap, HashSet};
|
||||
use editor::{
|
||||
Addon, Anchor, AnchorRangeExt, ContextMenuOptions, ContextMenuPlacement, Editor, EditorElement,
|
||||
EditorEvent, EditorMode, EditorSnapshot, EditorStyle, ExcerptId, FoldPlaceholder, MultiBuffer,
|
||||
ToOffset,
|
||||
EditorEvent, EditorMode, EditorSnapshot, EditorStyle, ExcerptId, FoldPlaceholder, InlayId,
|
||||
MultiBuffer, ToOffset,
|
||||
actions::Paste,
|
||||
display_map::{Crease, CreaseId, FoldId},
|
||||
display_map::{Crease, CreaseId, FoldId, Inlay},
|
||||
};
|
||||
use futures::{
|
||||
FutureExt as _,
|
||||
@@ -25,10 +25,12 @@ use gpui::{
|
||||
EventEmitter, FocusHandle, Focusable, Image, ImageFormat, Img, KeyContext, Subscription, Task,
|
||||
TextStyle, WeakEntity, pulsating_between,
|
||||
};
|
||||
use language::{Buffer, Language};
|
||||
use language::{Buffer, Language, language_settings::InlayHintKind};
|
||||
use language_model::LanguageModelImage;
|
||||
use postage::stream::Stream as _;
|
||||
use project::{CompletionIntent, Project, ProjectItem, ProjectPath, Worktree};
|
||||
use project::{
|
||||
CompletionIntent, InlayHint, InlayHintLabel, Project, ProjectItem, ProjectPath, Worktree,
|
||||
};
|
||||
use prompt_store::{PromptId, PromptStore};
|
||||
use rope::Point;
|
||||
use settings::Settings;
|
||||
@@ -62,6 +64,7 @@ pub struct MessageEditor {
|
||||
history_store: Entity<HistoryStore>,
|
||||
prompt_store: Option<Entity<PromptStore>>,
|
||||
prompt_capabilities: Rc<Cell<acp::PromptCapabilities>>,
|
||||
available_commands: Rc<RefCell<Vec<acp::AvailableCommand>>>,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
_parse_slash_command_task: Task<()>,
|
||||
}
|
||||
@@ -76,6 +79,8 @@ pub enum MessageEditorEvent {
|
||||
|
||||
impl EventEmitter<MessageEditorEvent> for MessageEditor {}
|
||||
|
||||
const COMMAND_HINT_INLAY_ID: usize = 0;
|
||||
|
||||
impl MessageEditor {
|
||||
pub fn new(
|
||||
workspace: WeakEntity<Workspace>,
|
||||
@@ -102,7 +107,7 @@ impl MessageEditor {
|
||||
history_store.clone(),
|
||||
prompt_store.clone(),
|
||||
prompt_capabilities.clone(),
|
||||
available_commands,
|
||||
available_commands.clone(),
|
||||
));
|
||||
let mention_set = MentionSet::default();
|
||||
let editor = cx.new(|cx| {
|
||||
@@ -133,12 +138,33 @@ impl MessageEditor {
|
||||
})
|
||||
.detach();
|
||||
|
||||
let mut has_hint = false;
|
||||
let mut subscriptions = Vec::new();
|
||||
|
||||
subscriptions.push(cx.subscribe_in(&editor, window, {
|
||||
move |this, editor, event, window, cx| {
|
||||
if let EditorEvent::Edited { .. } = event {
|
||||
let snapshot = editor.update(cx, |editor, cx| editor.snapshot(window, cx));
|
||||
let snapshot = editor.update(cx, |editor, cx| {
|
||||
let new_hints = this
|
||||
.command_hint(editor.buffer(), cx)
|
||||
.into_iter()
|
||||
.collect::<Vec<_>>();
|
||||
let has_new_hint = !new_hints.is_empty();
|
||||
editor.splice_inlays(
|
||||
if has_hint {
|
||||
&[InlayId::Hint(COMMAND_HINT_INLAY_ID)]
|
||||
} else {
|
||||
&[]
|
||||
},
|
||||
new_hints,
|
||||
cx,
|
||||
);
|
||||
has_hint = has_new_hint;
|
||||
|
||||
editor.snapshot(window, cx)
|
||||
});
|
||||
this.mention_set.remove_invalid(snapshot);
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
@@ -152,11 +178,55 @@ impl MessageEditor {
|
||||
history_store,
|
||||
prompt_store,
|
||||
prompt_capabilities,
|
||||
available_commands,
|
||||
_subscriptions: subscriptions,
|
||||
_parse_slash_command_task: Task::ready(()),
|
||||
}
|
||||
}
|
||||
|
||||
fn command_hint(&self, buffer: &Entity<MultiBuffer>, cx: &App) -> Option<Inlay> {
|
||||
let available_commands = self.available_commands.borrow();
|
||||
if available_commands.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let snapshot = buffer.read(cx).snapshot(cx);
|
||||
let parsed_command = SlashCommandCompletion::try_parse(&snapshot.text(), 0)?;
|
||||
if parsed_command.argument.is_some() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let command_name = parsed_command.command?;
|
||||
let available_command = available_commands
|
||||
.iter()
|
||||
.find(|command| command.name == command_name)?;
|
||||
|
||||
let acp::AvailableCommandInput::Unstructured { mut hint } =
|
||||
available_command.input.clone()?;
|
||||
|
||||
let mut hint_pos = parsed_command.source_range.end + 1;
|
||||
if hint_pos > snapshot.len() {
|
||||
hint_pos = snapshot.len();
|
||||
hint.insert(0, ' ');
|
||||
}
|
||||
|
||||
let hint_pos = snapshot.anchor_after(hint_pos);
|
||||
|
||||
Some(Inlay::hint(
|
||||
COMMAND_HINT_INLAY_ID,
|
||||
hint_pos,
|
||||
&InlayHint {
|
||||
position: hint_pos.text_anchor,
|
||||
label: InlayHintLabel::String(hint),
|
||||
kind: Some(InlayHintKind::Parameter),
|
||||
padding_left: false,
|
||||
padding_right: false,
|
||||
tooltip: None,
|
||||
resolve_state: project::ResolveState::Resolved,
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
pub fn insert_thread_summary(
|
||||
&mut self,
|
||||
thread: agent2::DbThreadMetadata,
|
||||
@@ -1184,6 +1254,7 @@ impl Render for MessageEditor {
|
||||
local_player: cx.theme().players().local(),
|
||||
text: text_style,
|
||||
syntax: cx.theme().syntax().clone(),
|
||||
inlay_hints_style: editor::make_inlay_hints_style(cx),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
@@ -1639,7 +1710,7 @@ mod tests {
|
||||
name: "say-hello".to_string(),
|
||||
description: "Say hello to whoever you want".to_string(),
|
||||
input: Some(acp::AvailableCommandInput::Unstructured {
|
||||
hint: "Who do you want to say hello to?".to_string(),
|
||||
hint: "<name>".to_string(),
|
||||
}),
|
||||
},
|
||||
]));
|
||||
@@ -1714,7 +1785,7 @@ mod tests {
|
||||
cx.run_until_parked();
|
||||
|
||||
editor.update_in(&mut cx, |editor, window, cx| {
|
||||
assert_eq!(editor.text(cx), "/quick-math ");
|
||||
assert_eq!(editor.display_text(cx), "/quick-math ");
|
||||
assert!(!editor.has_visible_completions_menu());
|
||||
editor.set_text("", window, cx);
|
||||
});
|
||||
@@ -1722,7 +1793,7 @@ mod tests {
|
||||
cx.simulate_input("/say");
|
||||
|
||||
editor.update_in(&mut cx, |editor, _window, cx| {
|
||||
assert_eq!(editor.text(cx), "/say");
|
||||
assert_eq!(editor.display_text(cx), "/say");
|
||||
assert!(editor.has_visible_completions_menu());
|
||||
|
||||
assert_eq!(
|
||||
@@ -1740,6 +1811,7 @@ mod tests {
|
||||
|
||||
editor.update_in(&mut cx, |editor, _window, cx| {
|
||||
assert_eq!(editor.text(cx), "/say-hello ");
|
||||
assert_eq!(editor.display_text(cx), "/say-hello <name>");
|
||||
assert!(editor.has_visible_completions_menu());
|
||||
|
||||
assert_eq!(
|
||||
@@ -1757,8 +1829,35 @@ mod tests {
|
||||
|
||||
cx.run_until_parked();
|
||||
|
||||
editor.update_in(&mut cx, |editor, _window, cx| {
|
||||
editor.update_in(&mut cx, |editor, window, cx| {
|
||||
assert_eq!(editor.text(cx), "/say-hello GPT5");
|
||||
assert_eq!(editor.display_text(cx), "/say-hello GPT5");
|
||||
assert!(!editor.has_visible_completions_menu());
|
||||
|
||||
// Delete argument
|
||||
for _ in 0..4 {
|
||||
editor.backspace(&editor::actions::Backspace, window, cx);
|
||||
}
|
||||
});
|
||||
|
||||
cx.run_until_parked();
|
||||
|
||||
editor.update_in(&mut cx, |editor, window, cx| {
|
||||
assert_eq!(editor.text(cx), "/say-hello ");
|
||||
// Hint is visible because argument was deleted
|
||||
assert_eq!(editor.display_text(cx), "/say-hello <name>");
|
||||
|
||||
// Delete last command letter
|
||||
editor.backspace(&editor::actions::Backspace, window, cx);
|
||||
editor.backspace(&editor::actions::Backspace, window, cx);
|
||||
});
|
||||
|
||||
cx.run_until_parked();
|
||||
|
||||
editor.update_in(&mut cx, |editor, _window, cx| {
|
||||
// Hint goes away once command no longer matches an available one
|
||||
assert_eq!(editor.text(cx), "/say-hell");
|
||||
assert_eq!(editor.display_text(cx), "/say-hell");
|
||||
assert!(!editor.has_visible_completions_menu());
|
||||
});
|
||||
}
|
||||
|
||||
@@ -907,6 +907,39 @@ impl AcpThreadView {
|
||||
return;
|
||||
}
|
||||
|
||||
let text = self.message_editor.read(cx).text(cx);
|
||||
if text == "/login" || text == "/logout" {
|
||||
let ThreadState::Ready { thread, .. } = &self.thread_state else {
|
||||
return;
|
||||
};
|
||||
|
||||
let connection = thread.read(cx).connection().clone();
|
||||
if !connection
|
||||
.auth_methods()
|
||||
.iter()
|
||||
.any(|method| method.id.0.as_ref() == "claude-login")
|
||||
{
|
||||
return;
|
||||
};
|
||||
let this = cx.weak_entity();
|
||||
let agent = self.agent.clone();
|
||||
window.defer(cx, |window, cx| {
|
||||
Self::handle_auth_required(
|
||||
this,
|
||||
AuthRequired {
|
||||
description: None,
|
||||
provider_id: Some(language_model::ANTHROPIC_PROVIDER_ID),
|
||||
},
|
||||
agent,
|
||||
connection,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
cx.notify();
|
||||
return;
|
||||
}
|
||||
|
||||
let contents = self
|
||||
.message_editor
|
||||
.update(cx, |message_editor, cx| message_editor.contents(cx));
|
||||
|
||||
@@ -1911,13 +1911,17 @@ impl AgentPanel {
|
||||
AgentType::Gemini => {
|
||||
self.external_thread(Some(crate::ExternalAgent::Gemini), None, None, window, cx)
|
||||
}
|
||||
AgentType::ClaudeCode => self.external_thread(
|
||||
Some(crate::ExternalAgent::ClaudeCode),
|
||||
None,
|
||||
None,
|
||||
window,
|
||||
cx,
|
||||
),
|
||||
AgentType::ClaudeCode => {
|
||||
self.selected_agent = AgentType::ClaudeCode;
|
||||
self.serialize(cx);
|
||||
self.external_thread(
|
||||
Some(crate::ExternalAgent::ClaudeCode),
|
||||
None,
|
||||
None,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
AgentType::Custom { name, command } => self.external_thread(
|
||||
Some(crate::ExternalAgent::Custom { name, command }),
|
||||
None,
|
||||
|
||||
@@ -144,7 +144,8 @@ impl InlineAssistant {
|
||||
let Some(terminal_panel) = workspace.read(cx).panel::<TerminalPanel>(cx) else {
|
||||
return;
|
||||
};
|
||||
let enabled = AgentSettings::get_global(cx).enabled;
|
||||
let enabled = !DisableAiSettings::get_global(cx).disable_ai
|
||||
&& AgentSettings::get_global(cx).enabled;
|
||||
terminal_panel.update(cx, |terminal_panel, cx| {
|
||||
terminal_panel.set_assistant_enabled(enabled, cx)
|
||||
});
|
||||
|
||||
@@ -93,8 +93,8 @@ impl<T: 'static> Render for PromptEditor<T> {
|
||||
};
|
||||
|
||||
let bottom_padding = match &self.mode {
|
||||
PromptEditorMode::Buffer { .. } => Pixels::from(0.),
|
||||
PromptEditorMode::Terminal { .. } => Pixels::from(8.0),
|
||||
PromptEditorMode::Buffer { .. } => rems_from_px(2.0),
|
||||
PromptEditorMode::Terminal { .. } => rems_from_px(8.0),
|
||||
};
|
||||
|
||||
buttons.extend(self.render_buttons(window, cx));
|
||||
@@ -762,20 +762,22 @@ impl<T: 'static> PromptEditor<T> {
|
||||
)
|
||||
}
|
||||
|
||||
fn render_editor(&mut self, window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
|
||||
let font_size = TextSize::Default.rems(cx);
|
||||
let line_height = font_size.to_pixels(window.rem_size()) * 1.3;
|
||||
fn render_editor(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
|
||||
let colors = cx.theme().colors();
|
||||
|
||||
div()
|
||||
.key_context("InlineAssistEditor")
|
||||
.size_full()
|
||||
.p_2()
|
||||
.pl_1()
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.bg(colors.editor_background)
|
||||
.child({
|
||||
let settings = ThemeSettings::get_global(cx);
|
||||
let font_size = settings.buffer_font_size(cx);
|
||||
let line_height = font_size * 1.2;
|
||||
|
||||
let text_style = TextStyle {
|
||||
color: cx.theme().colors().editor_foreground,
|
||||
color: colors.editor_foreground,
|
||||
font_family: settings.buffer_font.family.clone(),
|
||||
font_features: settings.buffer_font.features.clone(),
|
||||
font_size: font_size.into(),
|
||||
@@ -786,7 +788,7 @@ impl<T: 'static> PromptEditor<T> {
|
||||
EditorElement::new(
|
||||
&self.editor,
|
||||
EditorStyle {
|
||||
background: cx.theme().colors().editor_background,
|
||||
background: colors.editor_background,
|
||||
local_player: cx.theme().players().local(),
|
||||
text: text_style,
|
||||
..Default::default()
|
||||
|
||||
@@ -43,7 +43,7 @@ use pathfinder_geometry::{
|
||||
vector::{Vector2F, Vector2I},
|
||||
};
|
||||
use smallvec::SmallVec;
|
||||
use std::{borrow::Cow, char, convert::TryFrom, sync::Arc};
|
||||
use std::{borrow::Cow, char, cmp, convert::TryFrom, sync::Arc};
|
||||
|
||||
use super::open_type::apply_features_and_fallbacks;
|
||||
|
||||
@@ -67,7 +67,6 @@ struct MacTextSystemState {
|
||||
font_ids_by_postscript_name: HashMap<String, FontId>,
|
||||
font_ids_by_font_key: HashMap<FontKey, SmallVec<[FontId; 4]>>,
|
||||
postscript_names_by_font_id: HashMap<FontId, String>,
|
||||
zwnjs_scratch_space: Vec<(usize, usize)>,
|
||||
}
|
||||
|
||||
impl MacTextSystem {
|
||||
@@ -80,7 +79,6 @@ impl MacTextSystem {
|
||||
font_ids_by_postscript_name: HashMap::default(),
|
||||
font_ids_by_font_key: HashMap::default(),
|
||||
postscript_names_by_font_id: HashMap::default(),
|
||||
zwnjs_scratch_space: Vec::new(),
|
||||
}))
|
||||
}
|
||||
}
|
||||
@@ -426,41 +424,29 @@ impl MacTextSystemState {
|
||||
}
|
||||
|
||||
fn layout_line(&mut self, text: &str, font_size: Pixels, font_runs: &[FontRun]) -> LineLayout {
|
||||
const ZWNJ: char = '\u{200C}';
|
||||
const ZWNJ_STR: &str = "\u{200C}";
|
||||
const ZWNJ_SIZE_16: usize = ZWNJ.len_utf16();
|
||||
|
||||
self.zwnjs_scratch_space.clear();
|
||||
// Construct the attributed string, converting UTF8 ranges to UTF16 ranges.
|
||||
let mut string = CFMutableAttributedString::new();
|
||||
|
||||
{
|
||||
let mut ix_converter = StringIndexConverter::new(&text);
|
||||
let mut last_font_run = None;
|
||||
string.replace_str(&CFString::new(text), CFRange::init(0, 0));
|
||||
let utf16_line_len = string.char_len() as usize;
|
||||
|
||||
let mut ix_converter = StringIndexConverter::new(text);
|
||||
for run in font_runs {
|
||||
let text = &text[ix_converter.utf8_ix..][..run.len];
|
||||
// if the fonts are the same, we need to disconnect the text with a ZWNJ
|
||||
// to prevent core text from forming ligatures between them
|
||||
let needs_zwnj = last_font_run.replace(run.font_id) == Some(run.font_id);
|
||||
let utf8_end = ix_converter.utf8_ix + run.len;
|
||||
let utf16_start = ix_converter.utf16_ix;
|
||||
|
||||
let n_zwnjs = self.zwnjs_scratch_space.len();
|
||||
let utf16_start = ix_converter.utf16_ix + n_zwnjs * ZWNJ_SIZE_16;
|
||||
ix_converter.advance_to_utf8_ix(ix_converter.utf8_ix + run.len);
|
||||
|
||||
string.replace_str(&CFString::new(text), CFRange::init(utf16_start as isize, 0));
|
||||
if needs_zwnj {
|
||||
let zwnjs_pos = string.char_len();
|
||||
self.zwnjs_scratch_space.push((n_zwnjs, zwnjs_pos as usize));
|
||||
string.replace_str(
|
||||
&CFString::from_static_string(ZWNJ_STR),
|
||||
CFRange::init(zwnjs_pos, 0),
|
||||
);
|
||||
if utf16_start >= utf16_line_len {
|
||||
break;
|
||||
}
|
||||
let utf16_end = string.char_len() as usize;
|
||||
|
||||
ix_converter.advance_to_utf8_ix(utf8_end);
|
||||
let utf16_end = cmp::min(ix_converter.utf16_ix, utf16_line_len);
|
||||
|
||||
let cf_range =
|
||||
CFRange::init(utf16_start as isize, (utf16_end - utf16_start) as isize);
|
||||
let font = &self.fonts[run.font_id.0];
|
||||
|
||||
let font: &FontKitFont = &self.fonts[run.font_id.0];
|
||||
|
||||
unsafe {
|
||||
string.set_attribute(
|
||||
cf_range,
|
||||
@@ -468,12 +454,17 @@ impl MacTextSystemState {
|
||||
&font.native_font().clone_with_font_size(font_size.into()),
|
||||
);
|
||||
}
|
||||
|
||||
if utf16_end == utf16_line_len {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Retrieve the glyphs from the shaped line, converting UTF16 offsets to UTF8 offsets.
|
||||
let line = CTLine::new_with_attributed_string(string.as_concrete_TypeRef());
|
||||
let glyph_runs = line.glyph_runs();
|
||||
let mut runs = <Vec<ShapedRun>>::with_capacity(glyph_runs.len() as usize);
|
||||
let mut runs = Vec::with_capacity(glyph_runs.len() as usize);
|
||||
let mut ix_converter = StringIndexConverter::new(text);
|
||||
for run in glyph_runs.into_iter() {
|
||||
let attributes = run.attributes().unwrap();
|
||||
@@ -485,44 +476,28 @@ impl MacTextSystemState {
|
||||
};
|
||||
let font_id = self.id_for_native_font(font);
|
||||
|
||||
let mut glyphs = match runs.last_mut() {
|
||||
Some(run) if run.font_id == font_id => &mut run.glyphs,
|
||||
_ => {
|
||||
runs.push(ShapedRun {
|
||||
font_id,
|
||||
glyphs: Vec::with_capacity(run.glyph_count().try_into().unwrap_or(0)),
|
||||
});
|
||||
&mut runs.last_mut().unwrap().glyphs
|
||||
}
|
||||
};
|
||||
for ((&glyph_id, position), &glyph_utf16_ix) in run
|
||||
let mut glyphs = Vec::with_capacity(run.glyph_count().try_into().unwrap_or(0));
|
||||
for ((glyph_id, position), glyph_utf16_ix) in run
|
||||
.glyphs()
|
||||
.iter()
|
||||
.zip(run.positions().iter())
|
||||
.zip(run.string_indices().iter())
|
||||
{
|
||||
let mut glyph_utf16_ix = usize::try_from(glyph_utf16_ix).unwrap();
|
||||
let r = self
|
||||
.zwnjs_scratch_space
|
||||
.binary_search_by(|&(_, it)| it.cmp(&glyph_utf16_ix));
|
||||
match r {
|
||||
// this glyph is a ZWNJ, skip it
|
||||
Ok(_) => continue,
|
||||
// adjust the index to account for the ZWNJs we've inserted
|
||||
Err(idx) => glyph_utf16_ix -= idx * ZWNJ_SIZE_16,
|
||||
}
|
||||
let glyph_utf16_ix = usize::try_from(*glyph_utf16_ix).unwrap();
|
||||
if ix_converter.utf16_ix > glyph_utf16_ix {
|
||||
// We cannot reuse current index converter, as it can only seek forward. Restart the search.
|
||||
ix_converter = StringIndexConverter::new(text);
|
||||
}
|
||||
ix_converter.advance_to_utf16_ix(glyph_utf16_ix);
|
||||
glyphs.push(ShapedGlyph {
|
||||
id: GlyphId(glyph_id as u32),
|
||||
id: GlyphId(*glyph_id as u32),
|
||||
position: point(position.x as f32, position.y as f32).map(px),
|
||||
index: ix_converter.utf8_ix,
|
||||
is_emoji: self.is_emoji(font_id),
|
||||
});
|
||||
}
|
||||
|
||||
runs.push(ShapedRun { font_id, glyphs });
|
||||
}
|
||||
let typographic_bounds = line.get_typographic_bounds();
|
||||
LineLayout {
|
||||
@@ -721,93 +696,4 @@ mod tests {
|
||||
// There's no glyph for \u{feff}
|
||||
assert_eq!(layout.runs[0].glyphs[1].id, GlyphId(69u32)); // b
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_layout_line_zwnj_insertion() {
|
||||
let fonts = MacTextSystem::new();
|
||||
let font_id = fonts.font_id(&font("Helvetica")).unwrap();
|
||||
|
||||
let text = "hello world";
|
||||
let font_runs = &[
|
||||
FontRun { font_id, len: 5 }, // "hello"
|
||||
FontRun { font_id, len: 6 }, // " world"
|
||||
];
|
||||
|
||||
let layout = fonts.layout_line(text, px(16.), font_runs);
|
||||
assert_eq!(layout.len, text.len());
|
||||
|
||||
for run in &layout.runs {
|
||||
for glyph in &run.glyphs {
|
||||
assert!(
|
||||
glyph.index < text.len(),
|
||||
"Glyph index {} is out of bounds for text length {}",
|
||||
glyph.index,
|
||||
text.len()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Test with different font runs - should not insert ZWNJ
|
||||
let font_id2 = fonts.font_id(&font("Times")).unwrap_or(font_id);
|
||||
let font_runs_different = &[
|
||||
FontRun { font_id, len: 5 }, // "hello"
|
||||
// " world"
|
||||
FontRun {
|
||||
font_id: font_id2,
|
||||
len: 6,
|
||||
},
|
||||
];
|
||||
|
||||
let layout2 = fonts.layout_line(text, px(16.), font_runs_different);
|
||||
assert_eq!(layout2.len, text.len());
|
||||
|
||||
for run in &layout2.runs {
|
||||
for glyph in &run.glyphs {
|
||||
assert!(
|
||||
glyph.index < text.len(),
|
||||
"Glyph index {} is out of bounds for text length {}",
|
||||
glyph.index,
|
||||
text.len()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_layout_line_zwnj_edge_cases() {
|
||||
let fonts = MacTextSystem::new();
|
||||
let font_id = fonts.font_id(&font("Helvetica")).unwrap();
|
||||
|
||||
let text = "hello";
|
||||
let font_runs = &[FontRun { font_id, len: 5 }];
|
||||
let layout = fonts.layout_line(text, px(16.), font_runs);
|
||||
assert_eq!(layout.len, text.len());
|
||||
|
||||
let text = "abc";
|
||||
let font_runs = &[
|
||||
FontRun { font_id, len: 1 }, // "a"
|
||||
FontRun { font_id, len: 1 }, // "b"
|
||||
FontRun { font_id, len: 1 }, // "c"
|
||||
];
|
||||
let layout = fonts.layout_line(text, px(16.), font_runs);
|
||||
assert_eq!(layout.len, text.len());
|
||||
|
||||
for run in &layout.runs {
|
||||
for glyph in &run.glyphs {
|
||||
assert!(
|
||||
glyph.index < text.len(),
|
||||
"Glyph index {} is out of bounds for text length {}",
|
||||
glyph.index,
|
||||
text.len()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Test with empty text
|
||||
let text = "";
|
||||
let font_runs = &[];
|
||||
let layout = fonts.layout_line(text, px(16.), font_runs);
|
||||
assert_eq!(layout.len, 0);
|
||||
assert!(layout.runs.is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -413,10 +413,9 @@ impl WindowTextSystem {
|
||||
let mut wrapped_lines = 0;
|
||||
|
||||
let mut process_line = |line_text: SharedString| {
|
||||
font_runs.clear();
|
||||
let line_end = line_start + line_text.len();
|
||||
|
||||
let mut last_font: Option<FontId> = None;
|
||||
let mut last_font: Option<Font> = None;
|
||||
let mut decoration_runs = SmallVec::<[DecorationRun; 32]>::new();
|
||||
let mut run_start = line_start;
|
||||
while run_start < line_end {
|
||||
@@ -426,14 +425,23 @@ impl WindowTextSystem {
|
||||
|
||||
let run_len_within_line = cmp::min(line_end, run_start + run.len) - run_start;
|
||||
|
||||
let decoration_changed = if let Some(last_run) = decoration_runs.last_mut()
|
||||
&& last_run.color == run.color
|
||||
&& last_run.underline == run.underline
|
||||
&& last_run.strikethrough == run.strikethrough
|
||||
&& last_run.background_color == run.background_color
|
||||
{
|
||||
last_run.len += run_len_within_line as u32;
|
||||
false
|
||||
if last_font == Some(run.font.clone()) {
|
||||
font_runs.last_mut().unwrap().len += run_len_within_line;
|
||||
} else {
|
||||
last_font = Some(run.font.clone());
|
||||
font_runs.push(FontRun {
|
||||
len: run_len_within_line,
|
||||
font_id: self.resolve_font(&run.font),
|
||||
});
|
||||
}
|
||||
|
||||
if decoration_runs.last().is_some_and(|last_run| {
|
||||
last_run.color == run.color
|
||||
&& last_run.underline == run.underline
|
||||
&& last_run.strikethrough == run.strikethrough
|
||||
&& last_run.background_color == run.background_color
|
||||
}) {
|
||||
decoration_runs.last_mut().unwrap().len += run_len_within_line as u32;
|
||||
} else {
|
||||
decoration_runs.push(DecorationRun {
|
||||
len: run_len_within_line as u32,
|
||||
@@ -442,21 +450,6 @@ impl WindowTextSystem {
|
||||
underline: run.underline,
|
||||
strikethrough: run.strikethrough,
|
||||
});
|
||||
true
|
||||
};
|
||||
|
||||
if let Some(font_run) = font_runs.last_mut()
|
||||
&& Some(font_run.font_id) == last_font
|
||||
&& !decoration_changed
|
||||
{
|
||||
font_run.len += run_len_within_line;
|
||||
} else {
|
||||
let font_id = self.resolve_font(&run.font);
|
||||
last_font = Some(font_id);
|
||||
font_runs.push(FontRun {
|
||||
len: run_len_within_line,
|
||||
font_id,
|
||||
});
|
||||
}
|
||||
|
||||
if run_len_within_line == run.len {
|
||||
@@ -491,6 +484,8 @@ impl WindowTextSystem {
|
||||
runs.next();
|
||||
}
|
||||
}
|
||||
|
||||
font_runs.clear();
|
||||
};
|
||||
|
||||
let mut split_lines = text.split('\n');
|
||||
@@ -524,54 +519,37 @@ impl WindowTextSystem {
|
||||
/// Subsets of the line can be styled independently with the `runs` parameter.
|
||||
/// Generally, you should prefer to use `TextLayout::shape_line` instead, which
|
||||
/// can be painted directly.
|
||||
pub fn layout_line(
|
||||
pub fn layout_line<Text>(
|
||||
&self,
|
||||
text: &str,
|
||||
text: Text,
|
||||
font_size: Pixels,
|
||||
runs: &[TextRun],
|
||||
force_width: Option<Pixels>,
|
||||
) -> Arc<LineLayout> {
|
||||
let mut last_run = None::<&TextRun>;
|
||||
let mut last_font: Option<FontId> = None;
|
||||
) -> Arc<LineLayout>
|
||||
where
|
||||
Text: AsRef<str>,
|
||||
SharedString: From<Text>,
|
||||
{
|
||||
let mut font_runs = self.font_runs_pool.lock().pop().unwrap_or_default();
|
||||
font_runs.clear();
|
||||
|
||||
for run in runs.iter() {
|
||||
let decoration_changed = if let Some(last_run) = last_run
|
||||
&& last_run.color == run.color
|
||||
&& last_run.underline == run.underline
|
||||
&& last_run.strikethrough == run.strikethrough
|
||||
// we do not consider differing background color relevant, as it does not affect glyphs
|
||||
// && last_run.background_color == run.background_color
|
||||
let font_id = self.resolve_font(&run.font);
|
||||
if let Some(last_run) = font_runs.last_mut()
|
||||
&& last_run.font_id == font_id
|
||||
{
|
||||
false
|
||||
} else {
|
||||
last_run = Some(run);
|
||||
true
|
||||
};
|
||||
|
||||
if let Some(font_run) = font_runs.last_mut()
|
||||
&& Some(font_run.font_id) == last_font
|
||||
&& !decoration_changed
|
||||
{
|
||||
font_run.len += run.len;
|
||||
} else {
|
||||
let font_id = self.resolve_font(&run.font);
|
||||
last_font = Some(font_id);
|
||||
font_runs.push(FontRun {
|
||||
len: run.len,
|
||||
font_id,
|
||||
});
|
||||
last_run.len += run.len;
|
||||
continue;
|
||||
}
|
||||
font_runs.push(FontRun {
|
||||
len: run.len,
|
||||
font_id,
|
||||
});
|
||||
}
|
||||
|
||||
let layout = self.line_layout_cache.layout_line(
|
||||
&SharedString::new(text),
|
||||
font_size,
|
||||
&font_runs,
|
||||
force_width,
|
||||
);
|
||||
let layout =
|
||||
self.line_layout_cache
|
||||
.layout_line_internal(text, font_size, &font_runs, force_width);
|
||||
|
||||
font_runs.clear();
|
||||
self.font_runs_pool.lock().push(font_runs);
|
||||
|
||||
layout
|
||||
|
||||
@@ -501,7 +501,7 @@ impl LineLayoutCache {
|
||||
} else {
|
||||
drop(current_frame);
|
||||
let text = SharedString::from(text);
|
||||
let unwrapped_layout = self.layout_line::<&SharedString>(&text, font_size, runs, None);
|
||||
let unwrapped_layout = self.layout_line::<&SharedString>(&text, font_size, runs);
|
||||
let wrap_boundaries = if let Some(wrap_width) = wrap_width {
|
||||
unwrapped_layout.compute_wrap_boundaries(text.as_ref(), wrap_width, max_lines)
|
||||
} else {
|
||||
@@ -535,6 +535,19 @@ impl LineLayoutCache {
|
||||
text: Text,
|
||||
font_size: Pixels,
|
||||
runs: &[FontRun],
|
||||
) -> Arc<LineLayout>
|
||||
where
|
||||
Text: AsRef<str>,
|
||||
SharedString: From<Text>,
|
||||
{
|
||||
self.layout_line_internal(text, font_size, runs, None)
|
||||
}
|
||||
|
||||
pub fn layout_line_internal<Text>(
|
||||
&self,
|
||||
text: Text,
|
||||
font_size: Pixels,
|
||||
runs: &[FontRun],
|
||||
force_width: Option<Pixels>,
|
||||
) -> Arc<LineLayout>
|
||||
where
|
||||
|
||||
@@ -9,7 +9,7 @@ use language_model::{
|
||||
AuthenticateError, LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent,
|
||||
LanguageModelId, LanguageModelName, LanguageModelProvider, LanguageModelProviderId,
|
||||
LanguageModelProviderName, LanguageModelProviderState, LanguageModelRequest,
|
||||
LanguageModelToolChoice, RateLimiter,
|
||||
LanguageModelToolChoice, LanguageModelToolSchemaFormat, RateLimiter,
|
||||
};
|
||||
use menu;
|
||||
use open_ai::{ResponseStreamEvent, stream_completion};
|
||||
@@ -322,6 +322,10 @@ impl LanguageModel for OpenAiCompatibleLanguageModel {
|
||||
self.model.capabilities.tools
|
||||
}
|
||||
|
||||
fn tool_input_format(&self) -> LanguageModelToolSchemaFormat {
|
||||
LanguageModelToolSchemaFormat::JsonSchemaSubset
|
||||
}
|
||||
|
||||
fn supports_images(&self) -> bool {
|
||||
self.model.capabilities.images
|
||||
}
|
||||
|
||||
@@ -666,7 +666,6 @@ pub enum ResolveState {
|
||||
CanResolve(LanguageServerId, Option<lsp::LSPAny>),
|
||||
Resolving,
|
||||
}
|
||||
|
||||
impl InlayHint {
|
||||
pub fn text(&self) -> Rope {
|
||||
match &self.label {
|
||||
|
||||
@@ -202,20 +202,13 @@ fn check_pattern(pattern: &[PatternPart], input: &str) -> bool {
|
||||
match_any_chars.end += part.match_any_chars.end;
|
||||
continue;
|
||||
}
|
||||
let mut matched = false;
|
||||
for skip_count in match_any_chars.start..=match_any_chars.end {
|
||||
let end_ix = input_ix.saturating_sub(skip_count);
|
||||
if end_ix < part.text.len() {
|
||||
break;
|
||||
}
|
||||
if input[..end_ix].ends_with(&part.text) {
|
||||
matched = true;
|
||||
input_ix = end_ix - part.text.len();
|
||||
match_any_chars = part.match_any_chars.clone();
|
||||
break;
|
||||
}
|
||||
}
|
||||
if !matched && !part.optional {
|
||||
let search_range_start = input_ix.saturating_sub(match_any_chars.end + part.text.len());
|
||||
let search_range_end = input_ix.saturating_sub(match_any_chars.start);
|
||||
let found_ix = &input[search_range_start..search_range_end].rfind(&part.text);
|
||||
if let Some(found_ix) = found_ix {
|
||||
input_ix = search_range_start + found_ix;
|
||||
match_any_chars = part.match_any_chars.clone();
|
||||
} else if !part.optional {
|
||||
log::trace!(
|
||||
"Failed to match pattern `...{}` against input `...{}`",
|
||||
&part.text[part.text.len().saturating_sub(128)..],
|
||||
|
||||
@@ -157,5 +157,6 @@
|
||||
- [FreeBSD](./development/freebsd.md)
|
||||
- [Local Collaboration](./development/local-collaboration.md)
|
||||
- [Using Debuggers](./development/debuggers.md)
|
||||
- [Glossary](./development/glossary.md)
|
||||
- [Release Process](./development/releases.md)
|
||||
- [Debugging Crashes](./development/debugging-crashes.md)
|
||||
|
||||
@@ -1,9 +1,17 @@
|
||||
These are some terms and structures frequently used throughout the zed codebase. This is a best effort list.
|
||||
# Zed Development: Glossary
|
||||
|
||||
These are some terms and structures frequently used throughout the zed codebase.
|
||||
|
||||
This is a best effort list and a work in progress.
|
||||
|
||||
<!--
|
||||
TBD: Glossary Improvement
|
||||
|
||||
Questions:
|
||||
|
||||
- Can we generate this list from doc comments throughout zed?
|
||||
- We should have a section that shows the various UI parts and their names. (Can't do that in the channel.)
|
||||
-->
|
||||
|
||||
## Naming conventions
|
||||
|
||||
|
||||
Reference in New Issue
Block a user