Compare commits
11 Commits
shell-quot
...
v0.189.1-p
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0e562f2176 | ||
|
|
abea2409cd | ||
|
|
a5b511edb3 | ||
|
|
be1efa5d33 | ||
|
|
ce8ff9572e | ||
|
|
5ec3dffa11 | ||
|
|
261646656e | ||
|
|
0ac8774cd4 | ||
|
|
ba5c7326e9 | ||
|
|
41f944bd9e | ||
|
|
fc306a271c |
5
Cargo.lock
generated
5
Cargo.lock
generated
@@ -4732,6 +4732,7 @@ dependencies = [
|
||||
"tree-sitter-rust",
|
||||
"tree-sitter-typescript",
|
||||
"ui",
|
||||
"unicode-script",
|
||||
"unicode-segmentation",
|
||||
"unindent",
|
||||
"url",
|
||||
@@ -17115,8 +17116,6 @@ dependencies = [
|
||||
"tempfile",
|
||||
"tendril",
|
||||
"unicase",
|
||||
"unicode-script",
|
||||
"unicode-segmentation",
|
||||
"util_macros",
|
||||
"walkdir",
|
||||
"workspace-hack",
|
||||
@@ -19680,7 +19679,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed"
|
||||
version = "0.189.0"
|
||||
version = "0.189.1"
|
||||
dependencies = [
|
||||
"activity_indicator",
|
||||
"agent",
|
||||
|
||||
@@ -1452,9 +1452,7 @@
|
||||
"language_servers": ["erlang-ls", "!elp", "..."]
|
||||
},
|
||||
"Git Commit": {
|
||||
"allow_rewrap": "anywhere",
|
||||
"preferred_line_length": 72,
|
||||
"soft_wrap": "bounded"
|
||||
"allow_rewrap": "anywhere"
|
||||
},
|
||||
"Go": {
|
||||
"code_actions_on_format": {
|
||||
|
||||
@@ -311,6 +311,31 @@ impl ActivityIndicator {
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(session) = self
|
||||
.project
|
||||
.read(cx)
|
||||
.dap_store()
|
||||
.read(cx)
|
||||
.sessions()
|
||||
.find(|s| !s.read(cx).is_started())
|
||||
{
|
||||
return Some(Content {
|
||||
icon: Some(
|
||||
Icon::new(IconName::ArrowCircle)
|
||||
.size(IconSize::Small)
|
||||
.with_animation(
|
||||
"arrow-circle",
|
||||
Animation::new(Duration::from_secs(2)).repeat(),
|
||||
|icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
|
||||
)
|
||||
.into_any_element(),
|
||||
),
|
||||
message: format!("Debug: {}", session.read(cx).adapter()),
|
||||
tooltip_message: Some(session.read(cx).label().to_string()),
|
||||
on_click: None,
|
||||
});
|
||||
}
|
||||
|
||||
let current_job = self
|
||||
.project
|
||||
.read(cx)
|
||||
|
||||
@@ -149,22 +149,8 @@ impl DebugAdapter for PhpDebugAdapter {
|
||||
"default": false
|
||||
},
|
||||
"pathMappings": {
|
||||
"type": "array",
|
||||
"description": "A list of server paths mapping to the local source paths on your machine for remote host debugging",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"serverPath": {
|
||||
"type": "string",
|
||||
"description": "Path on the server"
|
||||
},
|
||||
"localPath": {
|
||||
"type": "string",
|
||||
"description": "Corresponding path on the local machine"
|
||||
}
|
||||
},
|
||||
"required": ["serverPath", "localPath"]
|
||||
}
|
||||
"type": "object",
|
||||
"description": "A mapping of server paths to local paths.",
|
||||
},
|
||||
"log": {
|
||||
"type": "boolean",
|
||||
|
||||
@@ -432,7 +432,10 @@ impl DebugPanel {
|
||||
};
|
||||
|
||||
let dap_store_handle = self.project.read(cx).dap_store().clone();
|
||||
let label = parent_session.read(cx).label().clone();
|
||||
let mut label = parent_session.read(cx).label().clone();
|
||||
if !label.ends_with("(child)") {
|
||||
label = format!("{label} (child)").into();
|
||||
}
|
||||
let adapter = parent_session.read(cx).adapter().clone();
|
||||
let mut binary = parent_session.read(cx).binary().clone();
|
||||
binary.request_args = request.clone();
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
use gpui::Entity;
|
||||
use std::time::Duration;
|
||||
|
||||
use gpui::{Animation, AnimationExt as _, Entity, Transformation, percentage};
|
||||
use project::debugger::session::{ThreadId, ThreadStatus};
|
||||
use ui::{ContextMenu, DropdownMenu, DropdownStyle, Indicator, prelude::*};
|
||||
|
||||
@@ -23,31 +25,40 @@ impl DebugPanel {
|
||||
let sessions = self.sessions().clone();
|
||||
let weak = cx.weak_entity();
|
||||
let running_state = running_state.read(cx);
|
||||
let label = if let Some(active_session) = active_session {
|
||||
let label = if let Some(active_session) = active_session.clone() {
|
||||
active_session.read(cx).session(cx).read(cx).label()
|
||||
} else {
|
||||
SharedString::new_static("Unknown Session")
|
||||
};
|
||||
|
||||
let is_terminated = running_state.session().read(cx).is_terminated();
|
||||
let session_state_indicator = {
|
||||
if is_terminated {
|
||||
Some(Indicator::dot().color(Color::Error))
|
||||
} else {
|
||||
match running_state.thread_status(cx).unwrap_or_default() {
|
||||
project::debugger::session::ThreadStatus::Stopped => {
|
||||
Some(Indicator::dot().color(Color::Conflict))
|
||||
}
|
||||
_ => Some(Indicator::dot().color(Color::Success)),
|
||||
let is_started = active_session
|
||||
.is_some_and(|session| session.read(cx).session(cx).read(cx).is_started());
|
||||
|
||||
let session_state_indicator = if is_terminated {
|
||||
Indicator::dot().color(Color::Error).into_any_element()
|
||||
} else if !is_started {
|
||||
Icon::new(IconName::ArrowCircle)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Muted)
|
||||
.with_animation(
|
||||
"arrow-circle",
|
||||
Animation::new(Duration::from_secs(2)).repeat(),
|
||||
|icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
|
||||
)
|
||||
.into_any_element()
|
||||
} else {
|
||||
match running_state.thread_status(cx).unwrap_or_default() {
|
||||
ThreadStatus::Stopped => {
|
||||
Indicator::dot().color(Color::Conflict).into_any_element()
|
||||
}
|
||||
_ => Indicator::dot().color(Color::Success).into_any_element(),
|
||||
}
|
||||
};
|
||||
|
||||
let trigger = h_flex()
|
||||
.gap_2()
|
||||
.when_some(session_state_indicator, |this, indicator| {
|
||||
this.child(indicator)
|
||||
})
|
||||
.child(session_state_indicator)
|
||||
.justify_between()
|
||||
.child(
|
||||
DebugPanel::dropdown_label(label)
|
||||
|
||||
@@ -110,7 +110,7 @@ impl Console {
|
||||
}
|
||||
|
||||
fn is_running(&self, cx: &Context<Self>) -> bool {
|
||||
self.session.read(cx).is_local()
|
||||
self.session.read(cx).is_running()
|
||||
}
|
||||
|
||||
fn handle_stack_frame_list_events(
|
||||
|
||||
@@ -250,9 +250,6 @@ impl StackFrameList {
|
||||
let Some(abs_path) = Self::abs_path_from_stack_frame(&stack_frame) else {
|
||||
return Task::ready(Err(anyhow!("Project path not found")));
|
||||
};
|
||||
if abs_path.starts_with("<node_internals>") {
|
||||
return Task::ready(Ok(()));
|
||||
}
|
||||
let row = stack_frame.line.saturating_sub(1) as u32;
|
||||
cx.emit(StackFrameListEvent::SelectedStackFrameChanged(
|
||||
stack_frame_id,
|
||||
@@ -345,6 +342,7 @@ impl StackFrameList {
|
||||
s.path
|
||||
.as_deref()
|
||||
.map(|path| Arc::<Path>::from(Path::new(path)))
|
||||
.filter(|path| path.is_absolute())
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -82,6 +82,7 @@ tree-sitter-rust = { workspace = true, optional = true }
|
||||
tree-sitter-typescript = { workspace = true, optional = true }
|
||||
tree-sitter-python = { workspace = true, optional = true }
|
||||
unicode-segmentation.workspace = true
|
||||
unicode-script.workspace = true
|
||||
unindent = { workspace = true, optional = true }
|
||||
ui.workspace = true
|
||||
url.workspace = true
|
||||
|
||||
@@ -201,7 +201,7 @@ use ui::{
|
||||
ButtonSize, ButtonStyle, ContextMenu, Disclosure, IconButton, IconButtonShape, IconName,
|
||||
IconSize, Indicator, Key, Tooltip, h_flex, prelude::*,
|
||||
};
|
||||
use util::{RangeExt, ResultExt, TryFutureExt, maybe, post_inc, wrap_with_prefix};
|
||||
use util::{RangeExt, ResultExt, TryFutureExt, maybe, post_inc};
|
||||
use workspace::{
|
||||
CollaboratorId, Item as WorkspaceItem, ItemId, ItemNavHistory, OpenInTerminal, OpenTerminal,
|
||||
RestoreOnStartupBehavior, SERIALIZATION_THROTTLE_TIME, SplitDirection, TabBarSettings, Toast,
|
||||
@@ -19587,6 +19587,347 @@ fn update_uncommitted_diff_for_buffer(
|
||||
})
|
||||
}
|
||||
|
||||
fn char_len_with_expanded_tabs(offset: usize, text: &str, tab_size: NonZeroU32) -> usize {
|
||||
let tab_size = tab_size.get() as usize;
|
||||
let mut width = offset;
|
||||
|
||||
for ch in text.chars() {
|
||||
width += if ch == '\t' {
|
||||
tab_size - (width % tab_size)
|
||||
} else {
|
||||
1
|
||||
};
|
||||
}
|
||||
|
||||
width - offset
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_string_size_with_expanded_tabs() {
|
||||
let nz = |val| NonZeroU32::new(val).unwrap();
|
||||
assert_eq!(char_len_with_expanded_tabs(0, "", nz(4)), 0);
|
||||
assert_eq!(char_len_with_expanded_tabs(0, "hello", nz(4)), 5);
|
||||
assert_eq!(char_len_with_expanded_tabs(0, "\thello", nz(4)), 9);
|
||||
assert_eq!(char_len_with_expanded_tabs(0, "abc\tab", nz(4)), 6);
|
||||
assert_eq!(char_len_with_expanded_tabs(0, "hello\t", nz(4)), 8);
|
||||
assert_eq!(char_len_with_expanded_tabs(0, "\t\t", nz(8)), 16);
|
||||
assert_eq!(char_len_with_expanded_tabs(0, "x\t", nz(8)), 8);
|
||||
assert_eq!(char_len_with_expanded_tabs(7, "x\t", nz(8)), 9);
|
||||
}
|
||||
}
|
||||
|
||||
/// Tokenizes a string into runs of text that should stick together, or that is whitespace.
|
||||
struct WordBreakingTokenizer<'a> {
|
||||
input: &'a str,
|
||||
}
|
||||
|
||||
impl<'a> WordBreakingTokenizer<'a> {
|
||||
fn new(input: &'a str) -> Self {
|
||||
Self { input }
|
||||
}
|
||||
}
|
||||
|
||||
fn is_char_ideographic(ch: char) -> bool {
|
||||
use unicode_script::Script::*;
|
||||
use unicode_script::UnicodeScript;
|
||||
matches!(ch.script(), Han | Tangut | Yi)
|
||||
}
|
||||
|
||||
fn is_grapheme_ideographic(text: &str) -> bool {
|
||||
text.chars().any(is_char_ideographic)
|
||||
}
|
||||
|
||||
fn is_grapheme_whitespace(text: &str) -> bool {
|
||||
text.chars().any(|x| x.is_whitespace())
|
||||
}
|
||||
|
||||
fn should_stay_with_preceding_ideograph(text: &str) -> bool {
|
||||
text.chars().next().map_or(false, |ch| {
|
||||
matches!(ch, '。' | '、' | ',' | '?' | '!' | ':' | ';' | '…')
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, Debug, Clone, Copy)]
|
||||
enum WordBreakToken<'a> {
|
||||
Word { token: &'a str, grapheme_len: usize },
|
||||
InlineWhitespace { token: &'a str, grapheme_len: usize },
|
||||
Newline,
|
||||
}
|
||||
|
||||
impl<'a> Iterator for WordBreakingTokenizer<'a> {
|
||||
/// Yields a span, the count of graphemes in the token, and whether it was
|
||||
/// whitespace. Note that it also breaks at word boundaries.
|
||||
type Item = WordBreakToken<'a>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
if self.input.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut iter = self.input.graphemes(true).peekable();
|
||||
let mut offset = 0;
|
||||
let mut grapheme_len = 0;
|
||||
if let Some(first_grapheme) = iter.next() {
|
||||
let is_newline = first_grapheme == "\n";
|
||||
let is_whitespace = is_grapheme_whitespace(first_grapheme);
|
||||
offset += first_grapheme.len();
|
||||
grapheme_len += 1;
|
||||
if is_grapheme_ideographic(first_grapheme) && !is_whitespace {
|
||||
if let Some(grapheme) = iter.peek().copied() {
|
||||
if should_stay_with_preceding_ideograph(grapheme) {
|
||||
offset += grapheme.len();
|
||||
grapheme_len += 1;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let mut words = self.input[offset..].split_word_bound_indices().peekable();
|
||||
let mut next_word_bound = words.peek().copied();
|
||||
if next_word_bound.map_or(false, |(i, _)| i == 0) {
|
||||
next_word_bound = words.next();
|
||||
}
|
||||
while let Some(grapheme) = iter.peek().copied() {
|
||||
if next_word_bound.map_or(false, |(i, _)| i == offset) {
|
||||
break;
|
||||
};
|
||||
if is_grapheme_whitespace(grapheme) != is_whitespace
|
||||
|| (grapheme == "\n") != is_newline
|
||||
{
|
||||
break;
|
||||
};
|
||||
offset += grapheme.len();
|
||||
grapheme_len += 1;
|
||||
iter.next();
|
||||
}
|
||||
}
|
||||
let token = &self.input[..offset];
|
||||
self.input = &self.input[offset..];
|
||||
if token == "\n" {
|
||||
Some(WordBreakToken::Newline)
|
||||
} else if is_whitespace {
|
||||
Some(WordBreakToken::InlineWhitespace {
|
||||
token,
|
||||
grapheme_len,
|
||||
})
|
||||
} else {
|
||||
Some(WordBreakToken::Word {
|
||||
token,
|
||||
grapheme_len,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_word_breaking_tokenizer() {
|
||||
let tests: &[(&str, &[WordBreakToken<'static>])] = &[
|
||||
("", &[]),
|
||||
(" ", &[whitespace(" ", 2)]),
|
||||
("Ʒ", &[word("Ʒ", 1)]),
|
||||
("Ǽ", &[word("Ǽ", 1)]),
|
||||
("⋑", &[word("⋑", 1)]),
|
||||
("⋑⋑", &[word("⋑⋑", 2)]),
|
||||
(
|
||||
"原理,进而",
|
||||
&[word("原", 1), word("理,", 2), word("进", 1), word("而", 1)],
|
||||
),
|
||||
(
|
||||
"hello world",
|
||||
&[word("hello", 5), whitespace(" ", 1), word("world", 5)],
|
||||
),
|
||||
(
|
||||
"hello, world",
|
||||
&[word("hello,", 6), whitespace(" ", 1), word("world", 5)],
|
||||
),
|
||||
(
|
||||
" hello world",
|
||||
&[
|
||||
whitespace(" ", 2),
|
||||
word("hello", 5),
|
||||
whitespace(" ", 1),
|
||||
word("world", 5),
|
||||
],
|
||||
),
|
||||
(
|
||||
"这是什么 \n 钢笔",
|
||||
&[
|
||||
word("这", 1),
|
||||
word("是", 1),
|
||||
word("什", 1),
|
||||
word("么", 1),
|
||||
whitespace(" ", 1),
|
||||
newline(),
|
||||
whitespace(" ", 1),
|
||||
word("钢", 1),
|
||||
word("笔", 1),
|
||||
],
|
||||
),
|
||||
(" mutton", &[whitespace(" ", 1), word("mutton", 6)]),
|
||||
];
|
||||
|
||||
fn word(token: &'static str, grapheme_len: usize) -> WordBreakToken<'static> {
|
||||
WordBreakToken::Word {
|
||||
token,
|
||||
grapheme_len,
|
||||
}
|
||||
}
|
||||
|
||||
fn whitespace(token: &'static str, grapheme_len: usize) -> WordBreakToken<'static> {
|
||||
WordBreakToken::InlineWhitespace {
|
||||
token,
|
||||
grapheme_len,
|
||||
}
|
||||
}
|
||||
|
||||
fn newline() -> WordBreakToken<'static> {
|
||||
WordBreakToken::Newline
|
||||
}
|
||||
|
||||
for (input, result) in tests {
|
||||
assert_eq!(
|
||||
WordBreakingTokenizer::new(input)
|
||||
.collect::<Vec<_>>()
|
||||
.as_slice(),
|
||||
*result,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn wrap_with_prefix(
|
||||
line_prefix: String,
|
||||
unwrapped_text: String,
|
||||
wrap_column: usize,
|
||||
tab_size: NonZeroU32,
|
||||
preserve_existing_whitespace: bool,
|
||||
) -> String {
|
||||
let line_prefix_len = char_len_with_expanded_tabs(0, &line_prefix, tab_size);
|
||||
let mut wrapped_text = String::new();
|
||||
let mut current_line = line_prefix.clone();
|
||||
|
||||
let tokenizer = WordBreakingTokenizer::new(&unwrapped_text);
|
||||
let mut current_line_len = line_prefix_len;
|
||||
let mut in_whitespace = false;
|
||||
for token in tokenizer {
|
||||
let have_preceding_whitespace = in_whitespace;
|
||||
match token {
|
||||
WordBreakToken::Word {
|
||||
token,
|
||||
grapheme_len,
|
||||
} => {
|
||||
in_whitespace = false;
|
||||
if current_line_len + grapheme_len > wrap_column
|
||||
&& current_line_len != line_prefix_len
|
||||
{
|
||||
wrapped_text.push_str(current_line.trim_end());
|
||||
wrapped_text.push('\n');
|
||||
current_line.truncate(line_prefix.len());
|
||||
current_line_len = line_prefix_len;
|
||||
}
|
||||
current_line.push_str(token);
|
||||
current_line_len += grapheme_len;
|
||||
}
|
||||
WordBreakToken::InlineWhitespace {
|
||||
mut token,
|
||||
mut grapheme_len,
|
||||
} => {
|
||||
in_whitespace = true;
|
||||
if have_preceding_whitespace && !preserve_existing_whitespace {
|
||||
continue;
|
||||
}
|
||||
if !preserve_existing_whitespace {
|
||||
token = " ";
|
||||
grapheme_len = 1;
|
||||
}
|
||||
if current_line_len + grapheme_len > wrap_column {
|
||||
wrapped_text.push_str(current_line.trim_end());
|
||||
wrapped_text.push('\n');
|
||||
current_line.truncate(line_prefix.len());
|
||||
current_line_len = line_prefix_len;
|
||||
} else if current_line_len != line_prefix_len || preserve_existing_whitespace {
|
||||
current_line.push_str(token);
|
||||
current_line_len += grapheme_len;
|
||||
}
|
||||
}
|
||||
WordBreakToken::Newline => {
|
||||
in_whitespace = true;
|
||||
if preserve_existing_whitespace {
|
||||
wrapped_text.push_str(current_line.trim_end());
|
||||
wrapped_text.push('\n');
|
||||
current_line.truncate(line_prefix.len());
|
||||
current_line_len = line_prefix_len;
|
||||
} else if have_preceding_whitespace {
|
||||
continue;
|
||||
} else if current_line_len + 1 > wrap_column && current_line_len != line_prefix_len
|
||||
{
|
||||
wrapped_text.push_str(current_line.trim_end());
|
||||
wrapped_text.push('\n');
|
||||
current_line.truncate(line_prefix.len());
|
||||
current_line_len = line_prefix_len;
|
||||
} else if current_line_len != line_prefix_len {
|
||||
current_line.push(' ');
|
||||
current_line_len += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !current_line.is_empty() {
|
||||
wrapped_text.push_str(¤t_line);
|
||||
}
|
||||
wrapped_text
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_wrap_with_prefix() {
|
||||
assert_eq!(
|
||||
wrap_with_prefix(
|
||||
"# ".to_string(),
|
||||
"abcdefg".to_string(),
|
||||
4,
|
||||
NonZeroU32::new(4).unwrap(),
|
||||
false,
|
||||
),
|
||||
"# abcdefg"
|
||||
);
|
||||
assert_eq!(
|
||||
wrap_with_prefix(
|
||||
"".to_string(),
|
||||
"\thello world".to_string(),
|
||||
8,
|
||||
NonZeroU32::new(4).unwrap(),
|
||||
false,
|
||||
),
|
||||
"hello\nworld"
|
||||
);
|
||||
assert_eq!(
|
||||
wrap_with_prefix(
|
||||
"// ".to_string(),
|
||||
"xx \nyy zz aa bb cc".to_string(),
|
||||
12,
|
||||
NonZeroU32::new(4).unwrap(),
|
||||
false,
|
||||
),
|
||||
"// xx yy zz\n// aa bb cc"
|
||||
);
|
||||
assert_eq!(
|
||||
wrap_with_prefix(
|
||||
String::new(),
|
||||
"这是什么 \n 钢笔".to_string(),
|
||||
3,
|
||||
NonZeroU32::new(4).unwrap(),
|
||||
false,
|
||||
),
|
||||
"这是什\n么 钢\n笔"
|
||||
);
|
||||
}
|
||||
|
||||
pub trait CollaborationHub {
|
||||
fn collaborators<'a>(&self, cx: &'a App) -> &'a HashMap<PeerId, Collaborator>;
|
||||
fn user_participant_indices<'a>(&self, cx: &'a App) -> &'a HashMap<u64, ParticipantIndex>;
|
||||
|
||||
@@ -7607,7 +7607,10 @@ impl Element for EditorElement {
|
||||
editor.gutter_dimensions = gutter_dimensions;
|
||||
editor.set_visible_line_count(bounds.size.height / line_height, window, cx);
|
||||
|
||||
if matches!(editor.mode, EditorMode::Minimap { .. }) {
|
||||
if matches!(
|
||||
editor.mode,
|
||||
EditorMode::AutoHeight { .. } | EditorMode::Minimap { .. }
|
||||
) {
|
||||
snapshot
|
||||
} else {
|
||||
let wrap_width_for = |column: u32| (column as f32 * em_advance).ceil();
|
||||
@@ -9626,7 +9629,6 @@ fn compute_auto_height_layout(
|
||||
let font_size = style.text.font_size.to_pixels(window.rem_size());
|
||||
let line_height = style.text.line_height_in_pixels(window.rem_size());
|
||||
let em_width = window.text_system().em_width(font_id, font_size).unwrap();
|
||||
let em_advance = window.text_system().em_advance(font_id, font_size).unwrap();
|
||||
|
||||
let mut snapshot = editor.snapshot(window, cx);
|
||||
let gutter_dimensions = snapshot
|
||||
@@ -9643,18 +9645,10 @@ fn compute_auto_height_layout(
|
||||
let overscroll = size(em_width, px(0.));
|
||||
|
||||
let editor_width = text_width - gutter_dimensions.margin - overscroll.width - em_width;
|
||||
let content_offset = point(gutter_dimensions.margin, Pixels::ZERO);
|
||||
let editor_content_width = editor_width - content_offset.x;
|
||||
let wrap_width_for = |column: u32| (column as f32 * em_advance).ceil();
|
||||
let wrap_width = match editor.soft_wrap_mode(cx) {
|
||||
SoftWrap::GitDiff => None,
|
||||
SoftWrap::None => Some(wrap_width_for(MAX_LINE_LEN as u32 / 2)),
|
||||
SoftWrap::EditorWidth => Some(editor_content_width),
|
||||
SoftWrap::Column(column) => Some(wrap_width_for(column)),
|
||||
SoftWrap::Bounded(column) => Some(editor_content_width.min(wrap_width_for(column))),
|
||||
};
|
||||
if editor.set_wrap_width(wrap_width, cx) {
|
||||
snapshot = editor.snapshot(window, cx);
|
||||
if !matches!(editor.soft_wrap_mode(cx), SoftWrap::None) {
|
||||
if editor.set_wrap_width(Some(editor_width), cx) {
|
||||
snapshot = editor.snapshot(window, cx);
|
||||
}
|
||||
}
|
||||
|
||||
let scroll_height = (snapshot.max_point().row().next_row().0 as f32) * line_height;
|
||||
|
||||
@@ -1527,7 +1527,6 @@ impl PickerDelegate for FileFinderDelegate {
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.p_2()
|
||||
.gap_2()
|
||||
.child(
|
||||
Button::new("open-selection", "Open").on_click(|_, window, cx| {
|
||||
|
||||
@@ -54,7 +54,6 @@ use project::{
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{Settings as _, SettingsStore};
|
||||
use std::future::Future;
|
||||
use std::num::NonZeroU32;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::{collections::HashSet, sync::Arc, time::Duration, usize};
|
||||
use strum::{IntoEnumIterator, VariantNames};
|
||||
@@ -63,7 +62,7 @@ use ui::{
|
||||
Checkbox, ContextMenu, ElevationIndex, PopoverMenu, Scrollbar, ScrollbarState, SplitButton,
|
||||
Tooltip, prelude::*,
|
||||
};
|
||||
use util::{ResultExt, TryFutureExt, maybe, wrap_with_prefix};
|
||||
use util::{ResultExt, TryFutureExt, maybe};
|
||||
use workspace::AppState;
|
||||
|
||||
use notifications::status_toast::{StatusToast, ToastIcon};
|
||||
@@ -385,6 +384,7 @@ pub(crate) fn commit_message_editor(
|
||||
commit_editor.set_show_gutter(false, cx);
|
||||
commit_editor.set_show_wrap_guides(false, cx);
|
||||
commit_editor.set_show_indent_guides(false, cx);
|
||||
commit_editor.set_hard_wrap(Some(72), cx);
|
||||
let placeholder = placeholder.unwrap_or("Enter commit message".into());
|
||||
commit_editor.set_placeholder_text(placeholder, cx);
|
||||
commit_editor
|
||||
@@ -1486,22 +1486,8 @@ impl GitPanel {
|
||||
|
||||
fn custom_or_suggested_commit_message(&self, cx: &mut Context<Self>) -> Option<String> {
|
||||
let message = self.commit_editor.read(cx).text(cx);
|
||||
let width = self
|
||||
.commit_editor
|
||||
.read(cx)
|
||||
.buffer()
|
||||
.read(cx)
|
||||
.language_settings(cx)
|
||||
.preferred_line_length as usize;
|
||||
|
||||
if !message.trim().is_empty() {
|
||||
let message = wrap_with_prefix(
|
||||
String::new(),
|
||||
message,
|
||||
width,
|
||||
NonZeroU32::new(8).unwrap(), // tab size doesn't matter when prefix is empty
|
||||
false,
|
||||
);
|
||||
return Some(message);
|
||||
}
|
||||
|
||||
|
||||
@@ -680,7 +680,7 @@ pub struct CodeLabel {
|
||||
pub filter_range: Range<usize>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, JsonSchema)]
|
||||
#[derive(Clone, Deserialize, JsonSchema)]
|
||||
pub struct LanguageConfig {
|
||||
/// Human-readable name of the language.
|
||||
pub name: LanguageName,
|
||||
@@ -791,7 +791,7 @@ pub struct LanguageMatcher {
|
||||
}
|
||||
|
||||
/// The configuration for JSX tag auto-closing.
|
||||
#[derive(Clone, Debug, Deserialize, JsonSchema)]
|
||||
#[derive(Clone, Deserialize, JsonSchema)]
|
||||
pub struct JsxTagAutoCloseConfig {
|
||||
/// The name of the node for a opening tag
|
||||
pub open_tag_node_name: String,
|
||||
@@ -824,7 +824,7 @@ pub struct JsxTagAutoCloseConfig {
|
||||
}
|
||||
|
||||
/// The configuration for documentation block for this language.
|
||||
#[derive(Clone, Debug, Deserialize, JsonSchema)]
|
||||
#[derive(Clone, Deserialize, JsonSchema)]
|
||||
pub struct DocumentationConfig {
|
||||
/// A start tag of documentation block.
|
||||
pub start: Arc<str>,
|
||||
|
||||
@@ -357,6 +357,9 @@ const PYTHON_TEST_TARGET_TASK_VARIABLE: VariableName =
|
||||
const PYTHON_ACTIVE_TOOLCHAIN_PATH: VariableName =
|
||||
VariableName::Custom(Cow::Borrowed("PYTHON_ACTIVE_ZED_TOOLCHAIN"));
|
||||
|
||||
const PYTHON_ACTIVE_TOOLCHAIN_PATH_RAW: VariableName =
|
||||
VariableName::Custom(Cow::Borrowed("PYTHON_ACTIVE_ZED_TOOLCHAIN_RAW"));
|
||||
|
||||
const PYTHON_MODULE_NAME_TASK_VARIABLE: VariableName =
|
||||
VariableName::Custom(Cow::Borrowed("PYTHON_MODULE_NAME"));
|
||||
|
||||
@@ -378,21 +381,25 @@ impl ContextProvider for PythonContextProvider {
|
||||
let worktree_id = location.buffer.read(cx).file().map(|f| f.worktree_id(cx));
|
||||
|
||||
cx.spawn(async move |cx| {
|
||||
let active_toolchain = if let Some(worktree_id) = worktree_id {
|
||||
let raw_toolchain = if let Some(worktree_id) = worktree_id {
|
||||
toolchains
|
||||
.active_toolchain(worktree_id, Arc::from("".as_ref()), "Python".into(), cx)
|
||||
.await
|
||||
.map_or_else(|| "python3".to_owned(), |toolchain| toolchain.path.into())
|
||||
.map_or_else(
|
||||
|| String::from("python3"),
|
||||
|toolchain| toolchain.path.to_string(),
|
||||
)
|
||||
} else {
|
||||
String::from("python3")
|
||||
};
|
||||
let active_toolchain = format!("\"{raw_toolchain}\"");
|
||||
let toolchain = (PYTHON_ACTIVE_TOOLCHAIN_PATH, active_toolchain);
|
||||
|
||||
let raw_toolchain = (PYTHON_ACTIVE_TOOLCHAIN_PATH_RAW, raw_toolchain);
|
||||
Ok(task::TaskVariables::from_iter(
|
||||
test_target
|
||||
.into_iter()
|
||||
.chain(module_target.into_iter())
|
||||
.chain([toolchain]),
|
||||
.chain([toolchain, raw_toolchain]),
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ use async_trait::async_trait;
|
||||
use dap::{DapLocator, DebugRequest, adapters::DebugAdapterName};
|
||||
use gpui::SharedString;
|
||||
|
||||
use task::{DebugScenario, SpawnInTerminal, TaskTemplate};
|
||||
use task::{DebugScenario, SpawnInTerminal, TaskTemplate, VariableName};
|
||||
|
||||
pub(crate) struct PythonLocator;
|
||||
|
||||
@@ -35,6 +35,13 @@ impl DapLocator for PythonLocator {
|
||||
// We cannot debug selections.
|
||||
return None;
|
||||
}
|
||||
let command = if build_config.command
|
||||
== VariableName::Custom("PYTHON_ACTIVE_ZED_TOOLCHAIN".into()).template_value()
|
||||
{
|
||||
VariableName::Custom("PYTHON_ACTIVE_ZED_TOOLCHAIN_RAW".into()).template_value()
|
||||
} else {
|
||||
build_config.command.clone()
|
||||
};
|
||||
let module_specifier_position = build_config
|
||||
.args
|
||||
.iter()
|
||||
@@ -68,7 +75,7 @@ impl DapLocator for PythonLocator {
|
||||
}
|
||||
let mut config = serde_json::json!({
|
||||
"request": "launch",
|
||||
"python": build_config.command,
|
||||
"python": command,
|
||||
"args": args,
|
||||
"cwd": build_config.cwd.clone()
|
||||
});
|
||||
|
||||
@@ -121,16 +121,17 @@ impl From<dap::Thread> for Thread {
|
||||
|
||||
pub enum Mode {
|
||||
Building,
|
||||
Running(LocalMode),
|
||||
Running(RunningMode),
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct LocalMode {
|
||||
pub struct RunningMode {
|
||||
client: Arc<DebugAdapterClient>,
|
||||
binary: DebugAdapterBinary,
|
||||
tmp_breakpoint: Option<SourceBreakpoint>,
|
||||
worktree: WeakEntity<Worktree>,
|
||||
executor: BackgroundExecutor,
|
||||
is_started: bool,
|
||||
}
|
||||
|
||||
fn client_source(abs_path: &Path) -> dap::Source {
|
||||
@@ -148,7 +149,7 @@ fn client_source(abs_path: &Path) -> dap::Source {
|
||||
}
|
||||
}
|
||||
|
||||
impl LocalMode {
|
||||
impl RunningMode {
|
||||
async fn new(
|
||||
session_id: SessionId,
|
||||
parent_session: Option<Entity<Session>>,
|
||||
@@ -181,6 +182,7 @@ impl LocalMode {
|
||||
tmp_breakpoint: None,
|
||||
binary,
|
||||
executor: cx.background_executor().clone(),
|
||||
is_started: false,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -373,7 +375,7 @@ impl LocalMode {
|
||||
capabilities: &Capabilities,
|
||||
initialized_rx: oneshot::Receiver<()>,
|
||||
dap_store: WeakEntity<DapStore>,
|
||||
cx: &App,
|
||||
cx: &mut Context<Session>,
|
||||
) -> Task<Result<()>> {
|
||||
let raw = self.binary.request_args.clone();
|
||||
|
||||
@@ -405,7 +407,7 @@ impl LocalMode {
|
||||
let this = self.clone();
|
||||
let worktree = self.worktree().clone();
|
||||
let configuration_sequence = cx.spawn({
|
||||
async move |cx| {
|
||||
async move |_, cx| {
|
||||
let breakpoint_store =
|
||||
dap_store.read_with(cx, |dap_store, _| dap_store.breakpoint_store().clone())?;
|
||||
initialized_rx.await?;
|
||||
@@ -453,9 +455,20 @@ impl LocalMode {
|
||||
}
|
||||
});
|
||||
|
||||
cx.background_spawn(async move {
|
||||
futures::future::try_join(launch, configuration_sequence).await?;
|
||||
Ok(())
|
||||
let task = cx.background_spawn(futures::future::try_join(launch, configuration_sequence));
|
||||
|
||||
cx.spawn(async move |this, cx| {
|
||||
task.await?;
|
||||
|
||||
this.update(cx, |this, cx| {
|
||||
if let Some(this) = this.as_running_mut() {
|
||||
this.is_started = true;
|
||||
cx.notify();
|
||||
}
|
||||
})
|
||||
.ok();
|
||||
|
||||
anyhow::Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
@@ -704,7 +717,7 @@ impl Session {
|
||||
cx.subscribe(&breakpoint_store, |this, store, event, cx| match event {
|
||||
BreakpointStoreEvent::BreakpointsUpdated(path, reason) => {
|
||||
if let Some(local) = (!this.ignore_breakpoints)
|
||||
.then(|| this.as_local_mut())
|
||||
.then(|| this.as_running_mut())
|
||||
.flatten()
|
||||
{
|
||||
local
|
||||
@@ -714,7 +727,7 @@ impl Session {
|
||||
}
|
||||
BreakpointStoreEvent::BreakpointsCleared(paths) => {
|
||||
if let Some(local) = (!this.ignore_breakpoints)
|
||||
.then(|| this.as_local_mut())
|
||||
.then(|| this.as_running_mut())
|
||||
.flatten()
|
||||
{
|
||||
local.unset_breakpoints_from_paths(paths, cx).detach();
|
||||
@@ -806,7 +819,7 @@ impl Session {
|
||||
let parent_session = self.parent_session.clone();
|
||||
|
||||
cx.spawn(async move |this, cx| {
|
||||
let mode = LocalMode::new(
|
||||
let mode = RunningMode::new(
|
||||
id,
|
||||
parent_session,
|
||||
worktree.downgrade(),
|
||||
@@ -906,18 +919,29 @@ impl Session {
|
||||
return tx;
|
||||
}
|
||||
|
||||
pub fn is_local(&self) -> bool {
|
||||
pub fn is_started(&self) -> bool {
|
||||
match &self.mode {
|
||||
Mode::Building => false,
|
||||
Mode::Running(running) => running.is_started,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_building(&self) -> bool {
|
||||
matches!(self.mode, Mode::Building)
|
||||
}
|
||||
|
||||
pub fn is_running(&self) -> bool {
|
||||
matches!(self.mode, Mode::Running(_))
|
||||
}
|
||||
|
||||
pub fn as_local_mut(&mut self) -> Option<&mut LocalMode> {
|
||||
pub fn as_running_mut(&mut self) -> Option<&mut RunningMode> {
|
||||
match &mut self.mode {
|
||||
Mode::Running(local_mode) => Some(local_mode),
|
||||
Mode::Building => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_local(&self) -> Option<&LocalMode> {
|
||||
pub fn as_running(&self) -> Option<&RunningMode> {
|
||||
match &self.mode {
|
||||
Mode::Running(local_mode) => Some(local_mode),
|
||||
Mode::Building => None,
|
||||
@@ -1140,7 +1164,7 @@ impl Session {
|
||||
body: Option<serde_json::Value>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
let Some(local_session) = self.as_local() else {
|
||||
let Some(local_session) = self.as_running() else {
|
||||
unreachable!("Cannot respond to remote client");
|
||||
};
|
||||
let client = local_session.client.clone();
|
||||
@@ -1162,7 +1186,7 @@ impl Session {
|
||||
fn handle_stopped_event(&mut self, event: StoppedEvent, cx: &mut Context<Self>) {
|
||||
// todo(debugger): Find a clean way to get around the clone
|
||||
let breakpoint_store = self.breakpoint_store.clone();
|
||||
if let Some((local, path)) = self.as_local_mut().and_then(|local| {
|
||||
if let Some((local, path)) = self.as_running_mut().and_then(|local| {
|
||||
let breakpoint = local.tmp_breakpoint.take()?;
|
||||
let path = breakpoint.path.clone();
|
||||
Some((local, path))
|
||||
@@ -1528,7 +1552,7 @@ impl Session {
|
||||
|
||||
self.ignore_breakpoints = ignore;
|
||||
|
||||
if let Some(local) = self.as_local() {
|
||||
if let Some(local) = self.as_running() {
|
||||
local.send_source_breakpoints(ignore, &self.breakpoint_store, cx)
|
||||
} else {
|
||||
// todo(debugger): We need to propagate this change to downstream sessions and send a message to upstream sessions
|
||||
@@ -1550,7 +1574,7 @@ impl Session {
|
||||
}
|
||||
|
||||
fn send_exception_breakpoints(&mut self, cx: &App) {
|
||||
if let Some(local) = self.as_local() {
|
||||
if let Some(local) = self.as_running() {
|
||||
let exception_filters = self
|
||||
.exception_breakpoints
|
||||
.values()
|
||||
|
||||
@@ -384,7 +384,6 @@ impl ShellBuilder {
|
||||
|
||||
/// Returns the program and arguments to run this task in a shell.
|
||||
pub fn build(mut self, task_command: String, task_args: &Vec<String>) -> (String, Vec<String>) {
|
||||
let task_command = format!("\"{task_command}\"");
|
||||
let combined_command = task_args
|
||||
.into_iter()
|
||||
.fold(task_command, |mut command, arg| {
|
||||
|
||||
@@ -37,8 +37,6 @@ smol.workspace = true
|
||||
take-until.workspace = true
|
||||
tempfile.workspace = true
|
||||
unicase.workspace = true
|
||||
unicode-script.workspace = true
|
||||
unicode-segmentation.workspace = true
|
||||
util_macros = { workspace = true, optional = true }
|
||||
walkdir.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
|
||||
@@ -14,7 +14,6 @@ use anyhow::Result;
|
||||
use futures::Future;
|
||||
use itertools::Either;
|
||||
use regex::Regex;
|
||||
use std::num::NonZeroU32;
|
||||
use std::sync::{LazyLock, OnceLock};
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
@@ -184,208 +183,29 @@ pub fn truncate_lines_to_byte_limit(s: &str, max_bytes: usize) -> &str {
|
||||
truncate_to_byte_limit(s, max_bytes)
|
||||
}
|
||||
|
||||
fn char_len_with_expanded_tabs(offset: usize, text: &str, tab_size: NonZeroU32) -> usize {
|
||||
let tab_size = tab_size.get() as usize;
|
||||
let mut width = offset;
|
||||
#[test]
|
||||
fn test_truncate_lines_to_byte_limit() {
|
||||
let text = "Line 1\nLine 2\nLine 3\nLine 4";
|
||||
|
||||
for ch in text.chars() {
|
||||
width += if ch == '\t' {
|
||||
tab_size - (width % tab_size)
|
||||
} else {
|
||||
1
|
||||
};
|
||||
}
|
||||
// Limit that includes all lines
|
||||
assert_eq!(truncate_lines_to_byte_limit(text, 100), text);
|
||||
|
||||
width - offset
|
||||
}
|
||||
// Exactly the first line
|
||||
assert_eq!(truncate_lines_to_byte_limit(text, 7), "Line 1\n");
|
||||
|
||||
/// Tokenizes a string into runs of text that should stick together, or that is whitespace.
|
||||
struct WordBreakingTokenizer<'a> {
|
||||
input: &'a str,
|
||||
}
|
||||
// Limit between lines
|
||||
assert_eq!(truncate_lines_to_byte_limit(text, 13), "Line 1\n");
|
||||
assert_eq!(truncate_lines_to_byte_limit(text, 20), "Line 1\nLine 2\n");
|
||||
|
||||
impl<'a> WordBreakingTokenizer<'a> {
|
||||
fn new(input: &'a str) -> Self {
|
||||
Self { input }
|
||||
}
|
||||
}
|
||||
// Limit before first newline
|
||||
assert_eq!(truncate_lines_to_byte_limit(text, 6), "Line ");
|
||||
|
||||
fn is_char_ideographic(ch: char) -> bool {
|
||||
use unicode_script::Script::*;
|
||||
use unicode_script::UnicodeScript;
|
||||
matches!(ch.script(), Han | Tangut | Yi)
|
||||
}
|
||||
|
||||
fn is_grapheme_ideographic(text: &str) -> bool {
|
||||
text.chars().any(is_char_ideographic)
|
||||
}
|
||||
|
||||
fn is_grapheme_whitespace(text: &str) -> bool {
|
||||
text.chars().any(|x| x.is_whitespace())
|
||||
}
|
||||
|
||||
fn should_stay_with_preceding_ideograph(text: &str) -> bool {
|
||||
text.chars().next().map_or(false, |ch| {
|
||||
matches!(ch, '。' | '、' | ',' | '?' | '!' | ':' | ';' | '…')
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, Debug, Clone, Copy)]
|
||||
enum WordBreakToken<'a> {
|
||||
Word { token: &'a str, grapheme_len: usize },
|
||||
InlineWhitespace { token: &'a str, grapheme_len: usize },
|
||||
Newline,
|
||||
}
|
||||
|
||||
impl<'a> Iterator for WordBreakingTokenizer<'a> {
|
||||
/// Yields a span, the count of graphemes in the token, and whether it was
|
||||
/// whitespace. Note that it also breaks at word boundaries.
|
||||
type Item = WordBreakToken<'a>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
if self.input.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut iter = self.input.graphemes(true).peekable();
|
||||
let mut offset = 0;
|
||||
let mut grapheme_len = 0;
|
||||
if let Some(first_grapheme) = iter.next() {
|
||||
let is_newline = first_grapheme == "\n";
|
||||
let is_whitespace = is_grapheme_whitespace(first_grapheme);
|
||||
offset += first_grapheme.len();
|
||||
grapheme_len += 1;
|
||||
if is_grapheme_ideographic(first_grapheme) && !is_whitespace {
|
||||
if let Some(grapheme) = iter.peek().copied() {
|
||||
if should_stay_with_preceding_ideograph(grapheme) {
|
||||
offset += grapheme.len();
|
||||
grapheme_len += 1;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let mut words = self.input[offset..].split_word_bound_indices().peekable();
|
||||
let mut next_word_bound = words.peek().copied();
|
||||
if next_word_bound.map_or(false, |(i, _)| i == 0) {
|
||||
next_word_bound = words.next();
|
||||
}
|
||||
while let Some(grapheme) = iter.peek().copied() {
|
||||
if next_word_bound.map_or(false, |(i, _)| i == offset) {
|
||||
break;
|
||||
};
|
||||
if is_grapheme_whitespace(grapheme) != is_whitespace
|
||||
|| (grapheme == "\n") != is_newline
|
||||
{
|
||||
break;
|
||||
};
|
||||
offset += grapheme.len();
|
||||
grapheme_len += 1;
|
||||
iter.next();
|
||||
}
|
||||
}
|
||||
let token = &self.input[..offset];
|
||||
self.input = &self.input[offset..];
|
||||
if token == "\n" {
|
||||
Some(WordBreakToken::Newline)
|
||||
} else if is_whitespace {
|
||||
Some(WordBreakToken::InlineWhitespace {
|
||||
token,
|
||||
grapheme_len,
|
||||
})
|
||||
} else {
|
||||
Some(WordBreakToken::Word {
|
||||
token,
|
||||
grapheme_len,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn wrap_with_prefix(
|
||||
line_prefix: String,
|
||||
unwrapped_text: String,
|
||||
wrap_column: usize,
|
||||
tab_size: NonZeroU32,
|
||||
preserve_existing_whitespace: bool,
|
||||
) -> String {
|
||||
let line_prefix_len = char_len_with_expanded_tabs(0, &line_prefix, tab_size);
|
||||
let mut wrapped_text = String::new();
|
||||
let mut current_line = line_prefix.clone();
|
||||
|
||||
let tokenizer = WordBreakingTokenizer::new(&unwrapped_text);
|
||||
let mut current_line_len = line_prefix_len;
|
||||
let mut in_whitespace = false;
|
||||
for token in tokenizer {
|
||||
let have_preceding_whitespace = in_whitespace;
|
||||
match token {
|
||||
WordBreakToken::Word {
|
||||
token,
|
||||
grapheme_len,
|
||||
} => {
|
||||
in_whitespace = false;
|
||||
if current_line_len + grapheme_len > wrap_column
|
||||
&& current_line_len != line_prefix_len
|
||||
{
|
||||
wrapped_text.push_str(current_line.trim_end());
|
||||
wrapped_text.push('\n');
|
||||
current_line.truncate(line_prefix.len());
|
||||
current_line_len = line_prefix_len;
|
||||
}
|
||||
current_line.push_str(token);
|
||||
current_line_len += grapheme_len;
|
||||
}
|
||||
WordBreakToken::InlineWhitespace {
|
||||
mut token,
|
||||
mut grapheme_len,
|
||||
} => {
|
||||
in_whitespace = true;
|
||||
if have_preceding_whitespace && !preserve_existing_whitespace {
|
||||
continue;
|
||||
}
|
||||
if !preserve_existing_whitespace {
|
||||
token = " ";
|
||||
grapheme_len = 1;
|
||||
}
|
||||
if current_line_len + grapheme_len > wrap_column {
|
||||
wrapped_text.push_str(current_line.trim_end());
|
||||
wrapped_text.push('\n');
|
||||
current_line.truncate(line_prefix.len());
|
||||
current_line_len = line_prefix_len;
|
||||
} else if current_line_len != line_prefix_len || preserve_existing_whitespace {
|
||||
current_line.push_str(token);
|
||||
current_line_len += grapheme_len;
|
||||
}
|
||||
}
|
||||
WordBreakToken::Newline => {
|
||||
in_whitespace = true;
|
||||
if preserve_existing_whitespace {
|
||||
wrapped_text.push_str(current_line.trim_end());
|
||||
wrapped_text.push('\n');
|
||||
current_line.truncate(line_prefix.len());
|
||||
current_line_len = line_prefix_len;
|
||||
} else if have_preceding_whitespace {
|
||||
continue;
|
||||
} else if current_line_len + 1 > wrap_column && current_line_len != line_prefix_len
|
||||
{
|
||||
wrapped_text.push_str(current_line.trim_end());
|
||||
wrapped_text.push('\n');
|
||||
current_line.truncate(line_prefix.len());
|
||||
current_line_len = line_prefix_len;
|
||||
} else if current_line_len != line_prefix_len {
|
||||
current_line.push(' ');
|
||||
current_line_len += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !current_line.is_empty() {
|
||||
wrapped_text.push_str(¤t_line);
|
||||
}
|
||||
wrapped_text
|
||||
// Test with non-ASCII characters
|
||||
let text_utf8 = "Line 1\nLíne 2\nLine 3";
|
||||
assert_eq!(
|
||||
truncate_lines_to_byte_limit(text_utf8, 15),
|
||||
"Line 1\nLíne 2\n"
|
||||
);
|
||||
}
|
||||
|
||||
pub fn post_inc<T: From<u8> + AddAssign<T> + Copy>(value: &mut T) -> T {
|
||||
@@ -1581,163 +1401,6 @@ Line 3"#
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_truncate_lines_to_byte_limit() {
|
||||
let text = "Line 1\nLine 2\nLine 3\nLine 4";
|
||||
|
||||
// Limit that includes all lines
|
||||
assert_eq!(truncate_lines_to_byte_limit(text, 100), text);
|
||||
|
||||
// Exactly the first line
|
||||
assert_eq!(truncate_lines_to_byte_limit(text, 7), "Line 1\n");
|
||||
|
||||
// Limit between lines
|
||||
assert_eq!(truncate_lines_to_byte_limit(text, 13), "Line 1\n");
|
||||
assert_eq!(truncate_lines_to_byte_limit(text, 20), "Line 1\nLine 2\n");
|
||||
|
||||
// Limit before first newline
|
||||
assert_eq!(truncate_lines_to_byte_limit(text, 6), "Line ");
|
||||
|
||||
// Test with non-ASCII characters
|
||||
let text_utf8 = "Line 1\nLíne 2\nLine 3";
|
||||
assert_eq!(
|
||||
truncate_lines_to_byte_limit(text_utf8, 15),
|
||||
"Line 1\nLíne 2\n"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_string_size_with_expanded_tabs() {
|
||||
let nz = |val| NonZeroU32::new(val).unwrap();
|
||||
assert_eq!(char_len_with_expanded_tabs(0, "", nz(4)), 0);
|
||||
assert_eq!(char_len_with_expanded_tabs(0, "hello", nz(4)), 5);
|
||||
assert_eq!(char_len_with_expanded_tabs(0, "\thello", nz(4)), 9);
|
||||
assert_eq!(char_len_with_expanded_tabs(0, "abc\tab", nz(4)), 6);
|
||||
assert_eq!(char_len_with_expanded_tabs(0, "hello\t", nz(4)), 8);
|
||||
assert_eq!(char_len_with_expanded_tabs(0, "\t\t", nz(8)), 16);
|
||||
assert_eq!(char_len_with_expanded_tabs(0, "x\t", nz(8)), 8);
|
||||
assert_eq!(char_len_with_expanded_tabs(7, "x\t", nz(8)), 9);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_word_breaking_tokenizer() {
|
||||
let tests: &[(&str, &[WordBreakToken<'static>])] = &[
|
||||
("", &[]),
|
||||
(" ", &[whitespace(" ", 2)]),
|
||||
("Ʒ", &[word("Ʒ", 1)]),
|
||||
("Ǽ", &[word("Ǽ", 1)]),
|
||||
("⋑", &[word("⋑", 1)]),
|
||||
("⋑⋑", &[word("⋑⋑", 2)]),
|
||||
(
|
||||
"原理,进而",
|
||||
&[word("原", 1), word("理,", 2), word("进", 1), word("而", 1)],
|
||||
),
|
||||
(
|
||||
"hello world",
|
||||
&[word("hello", 5), whitespace(" ", 1), word("world", 5)],
|
||||
),
|
||||
(
|
||||
"hello, world",
|
||||
&[word("hello,", 6), whitespace(" ", 1), word("world", 5)],
|
||||
),
|
||||
(
|
||||
" hello world",
|
||||
&[
|
||||
whitespace(" ", 2),
|
||||
word("hello", 5),
|
||||
whitespace(" ", 1),
|
||||
word("world", 5),
|
||||
],
|
||||
),
|
||||
(
|
||||
"这是什么 \n 钢笔",
|
||||
&[
|
||||
word("这", 1),
|
||||
word("是", 1),
|
||||
word("什", 1),
|
||||
word("么", 1),
|
||||
whitespace(" ", 1),
|
||||
newline(),
|
||||
whitespace(" ", 1),
|
||||
word("钢", 1),
|
||||
word("笔", 1),
|
||||
],
|
||||
),
|
||||
(" mutton", &[whitespace(" ", 1), word("mutton", 6)]),
|
||||
];
|
||||
|
||||
fn word(token: &'static str, grapheme_len: usize) -> WordBreakToken<'static> {
|
||||
WordBreakToken::Word {
|
||||
token,
|
||||
grapheme_len,
|
||||
}
|
||||
}
|
||||
|
||||
fn whitespace(token: &'static str, grapheme_len: usize) -> WordBreakToken<'static> {
|
||||
WordBreakToken::InlineWhitespace {
|
||||
token,
|
||||
grapheme_len,
|
||||
}
|
||||
}
|
||||
|
||||
fn newline() -> WordBreakToken<'static> {
|
||||
WordBreakToken::Newline
|
||||
}
|
||||
|
||||
for (input, result) in tests {
|
||||
assert_eq!(
|
||||
WordBreakingTokenizer::new(input)
|
||||
.collect::<Vec<_>>()
|
||||
.as_slice(),
|
||||
*result,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_wrap_with_prefix() {
|
||||
assert_eq!(
|
||||
wrap_with_prefix(
|
||||
"# ".to_string(),
|
||||
"abcdefg".to_string(),
|
||||
4,
|
||||
NonZeroU32::new(4).unwrap(),
|
||||
false,
|
||||
),
|
||||
"# abcdefg"
|
||||
);
|
||||
assert_eq!(
|
||||
wrap_with_prefix(
|
||||
"".to_string(),
|
||||
"\thello world".to_string(),
|
||||
8,
|
||||
NonZeroU32::new(4).unwrap(),
|
||||
false,
|
||||
),
|
||||
"hello\nworld"
|
||||
);
|
||||
assert_eq!(
|
||||
wrap_with_prefix(
|
||||
"// ".to_string(),
|
||||
"xx \nyy zz aa bb cc".to_string(),
|
||||
12,
|
||||
NonZeroU32::new(4).unwrap(),
|
||||
false,
|
||||
),
|
||||
"// xx yy zz\n// aa bb cc"
|
||||
);
|
||||
assert_eq!(
|
||||
wrap_with_prefix(
|
||||
String::new(),
|
||||
"这是什么 \n 钢笔".to_string(),
|
||||
3,
|
||||
NonZeroU32::new(4).unwrap(),
|
||||
false,
|
||||
),
|
||||
"这是什\n么 钢\n笔"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_split_with_ranges() {
|
||||
let input = "hi";
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
description = "The fast, collaborative code editor."
|
||||
edition.workspace = true
|
||||
name = "zed"
|
||||
version = "0.189.0"
|
||||
version = "0.189.1"
|
||||
publish.workspace = true
|
||||
license = "GPL-3.0-or-later"
|
||||
authors = ["Zed Team <hi@zed.dev>"]
|
||||
|
||||
@@ -1 +1 @@
|
||||
dev
|
||||
preview
|
||||
Reference in New Issue
Block a user