Compare commits

...

9 Commits

Author SHA1 Message Date
Conrad Irwin
ccee5124a7 Intercept /login and /logout commands for now 2025-09-02 12:49:31 -07:00
Umesh Yadav
4c411b9fc8 language_models: Make JsonSchemaSubset the default tool_input_format for the OpenAI-compatible provider (#34921)
Closes #30188
Closes #34911
Closes #34906

Many OpenAI-compatible providers do not automatically filter the tool
schema to comply with the underlying model's requirements; they simply
proxy the request. This creates issues, as models like **Gemini**,
**Grok**, and **Claude** (when accessed via LiteLLM on Bedrock) are
incompatible with Zed's default tool schema.

This PR addresses this by defaulting to a more compatible schema subset
instead of the full schema.

### Why this approach?

* **Avoids Poor User Experience:** One alternative was to add an option
for users to manually set the JSON schema for models that return a `400
Bad Request` due to an invalid tool schema. This was discarded as it
provides a poor user experience.
* **Simplifies Complex Logic:** Another option was to filter the schema
based on the model ID. However, as demonstrated in the attached issues,
this is unreliable. For instance, `claude-4-sonnet` fails when proxied
through LiteLLM on Bedrock. Reliably determining behavior would require
a non-trivial implementation to manage provider-and-model combinations.
* **Better Default Behavior:** The current approach ensures that tool
usage works out-of-the-box for the majority of cases by default,
providing the most robust and user-friendly solution.


Release Notes:

- Improved tool compatibility with OpenAI API-compatible providers

Signed-off-by: Umesh Yadav <git@umesh.dev>
Co-authored-by: Peter Tripp <peter@zed.dev>
2025-09-02 14:29:07 -04:00
Peter Tripp
5ac6ae501f docs: Link glossary (#37387)
Follow-up to: https://github.com/zed-industries/zed/pull/37360

Add glossary.md to SUMMARY.md so it's linked to the public
documentation.

Release Notes:

- N/A
2025-09-02 17:57:48 +00:00
Michael Sloan
c01f12b15d zeta: Small refactoring in license detection check - rfind instead of iterated ends_with (#37329)
Release Notes:

- N/A
2025-09-02 17:23:35 +00:00
Agus Zubiaga
dfa066dfe8 acp: Display slash command hints (#37376)
Displays the slash command's argument hint while it hasn't been
provided:


https://github.com/user-attachments/assets/f3bb148c-247d-43bc-810d-92055a313514


Release Notes:

- N/A

---------

Co-authored-by: Bennet Bo Fenner <bennetbo@gmx.de>
2025-09-02 16:39:55 +00:00
Richard Feldman
ac8c653ae6 Fix race condition between feature flag and deserialization (#37381)
Right now if you open Zed, and we deserialize an agent that's behind a
feature flag (e.g. CC), we don't restore it because the feature flag
check hasn't happened yet at the time we're deserializing (due to auth
not having finished yet).

This is a simple fix: assume that if you had serialized it in the first
place, you must have had the feature flag enabled, so go ahead and
reopen it for you.

Release Notes:

- N/A
2025-09-02 12:28:07 -04:00
Danilo Leal
d2318be8d9 terminal view: Hide inline assist button if AI is disabled (#37378)
Closes https://github.com/zed-industries/zed/issues/37372

Release Notes:

- Fix the terminal inline assistant button showing despite `disable_ai`
being turned on.

---------

Co-authored-by: MrSubidubi <finn@zed.dev>
2025-09-02 13:27:06 -03:00
Danilo Leal
a026163746 inline assistant: Adjust completion menu item font size (#37375)
Now the @ completion menu items font size respect/match the buffer's
font size, as opposed to being rendered a bit bigger.

| Before | After |
|--------|--------|
| <img width="1226" height="468" alt="Screenshot 2025-09-02 at 11 
09@2x"
src="https://github.com/user-attachments/assets/a6d37110-b544-40c3-bf7a-447ea003d4d7"
/> | <img width="1218" height="462" alt="Screenshot 2025-09-02 at 11  09
2@2x"
src="https://github.com/user-attachments/assets/19e58bf8-2db5-442e-8f60-02dd9ee1308f"
/> |

Release Notes:

- inline assistant: Improved @-mention menu item font size, better
matching the buffer's font size.
2025-09-02 13:26:56 -03:00
Marshall Bowers
ad3ddd381d Revert "gpui: Do not render ligatures between different styled text runs (#37175) (#37382)
This reverts commit 62083fe796.

We're reverting this as it causes layout shift when typing/selecting
with ligatures:


https://github.com/user-attachments/assets/80b78909-62f5-404f-8cca-3535c5594ceb

Release Notes:

- Reverted #37175
2025-09-02 16:18:49 +00:00
14 changed files with 275 additions and 254 deletions

View File

@@ -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;

View File

@@ -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());
});
}

View File

@@ -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));

View File

@@ -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,

View File

@@ -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)
});

View File

@@ -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()

View File

@@ -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());
}
}

View File

@@ -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

View File

@@ -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

View File

@@ -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
}

View File

@@ -666,7 +666,6 @@ pub enum ResolveState {
CanResolve(LanguageServerId, Option<lsp::LSPAny>),
Resolving,
}
impl InlayHint {
pub fn text(&self) -> Rope {
match &self.label {

View File

@@ -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)..],

View File

@@ -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)

View File

@@ -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