DB & Dev Tools · Data formats: YAML lists at their key's indent, XML entities
YAML reads a list written at its key's own indent, as manifests do, and a list at a map's indent ends at the next key. A `#` inside a quoted scalar is no comment. XML reads entity references in text and attributes, failing on one it does not know, and a text's path counts text nodes from one. A tree's note takes at most half its row, cut with an ellipsis, so a long value or attribute leaves the key its name. Format and Minify fail loud if pressed while the JSON does not read. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
1c01adc4d6
commit
a0984e80a3
8 files changed
+204
-40
No files matched your search
@@ -387,3 +387,4 @@ Per-component lines live in `tasks/`. Tags there:
|
|||||||
| T33a | 1 | FAIL | the plan, the Redis browser and the structure editor drew past a 280px box; the form's rules and a row button's press went untested |
|
| T33a | 1 | FAIL | the plan, the Redis browser and the structure editor drew past a 280px box; the form's rules and a row button's press went untested |
|
||||||
| T33a | 2 | FAIL | the structure editor's cells kept the narrow box's first measure, and its Tab stops past the sideways fold stayed out of view |
|
| T33a | 2 | FAIL | the structure editor's cells kept the narrow box's first measure, and its Tab stops past the sideways fold stayed out of view |
|
||||||
| T33a | 3 | PASS | |
|
| T33a | 3 | PASS | |
|
||||||
|
| T33b | 1 | FAIL | YAML lists at their key's indent and quoted `#` misread; a long note hid the key at 280px; XML text paths counted from 0 and entities stayed raw; odd-key paths and Format at rest untested |
|
||||||
+75
-19
@@ -69,16 +69,41 @@ fn scalar(text: &str) -> Datum {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Where a line's comment starts: a `#` at its start or after a space, outside a quoted scalar.
|
||||||
|
fn comment(line: &str) -> Option<usize> {
|
||||||
|
let mut chars = line.char_indices().peekable();
|
||||||
|
let (mut quote, mut last) = (None, ' ');
|
||||||
|
while let Some((at, ch)) = chars.next() {
|
||||||
|
match quote {
|
||||||
|
Some('"') if ch == '\\' => {
|
||||||
|
chars.next();
|
||||||
|
}
|
||||||
|
Some('\'') if ch == '\'' && chars.peek().is_some_and(|(_, next)| *next == '\'') => {
|
||||||
|
chars.next();
|
||||||
|
}
|
||||||
|
Some(open) if ch == open => quote = None,
|
||||||
|
Some(_) => {}
|
||||||
|
None if ch == '#' && last.is_whitespace() => return Some(at),
|
||||||
|
None if matches!(ch, '"' | '\'') && last.is_whitespace() => quote = Some(ch),
|
||||||
|
None => {}
|
||||||
|
}
|
||||||
|
last = ch;
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether a line starts a list item.
|
||||||
|
fn dashed(words: &str) -> bool {
|
||||||
|
let words = words.trim_start();
|
||||||
|
words.starts_with("- ") || words == "-"
|
||||||
|
}
|
||||||
|
|
||||||
/// Lines of a YAML block: each one's number, indent and words, without comments and blank lines.
|
/// Lines of a YAML block: each one's number, indent and words, without comments and blank lines.
|
||||||
fn lines(text: &str) -> Vec<(usize, usize, &str)> {
|
fn lines(text: &str) -> Vec<(usize, usize, &str)> {
|
||||||
text.lines()
|
text.lines()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.filter_map(|(ix, line)| {
|
.filter_map(|(ix, line)| {
|
||||||
let words = match line.find(" #") {
|
let words = &line[..comment(line).unwrap_or(line.len())];
|
||||||
Some(at) => &line[..at],
|
|
||||||
None if line.trim_start().starts_with('#') => "",
|
|
||||||
None => line,
|
|
||||||
};
|
|
||||||
let indent = words.len() - words.trim_start().len();
|
let indent = words.len() - words.trim_start().len();
|
||||||
(!words.trim().is_empty() && words.trim() != "---")
|
(!words.trim().is_empty() && words.trim() != "---")
|
||||||
.then(|| (ix + 1, indent, words.trim_end()))
|
.then(|| (ix + 1, indent, words.trim_end()))
|
||||||
@@ -88,18 +113,11 @@ fn lines(text: &str) -> Vec<(usize, usize, &str)> {
|
|||||||
|
|
||||||
/// The block that starts at `at` and holds the lines at `indent`.
|
/// The block that starts at `at` and holds the lines at `indent`.
|
||||||
fn block(lines: &[(usize, usize, &str)], at: &mut usize, indent: usize) -> Result<Datum, Unread> {
|
fn block(lines: &[(usize, usize, &str)], at: &mut usize, indent: usize) -> Result<Datum, Unread> {
|
||||||
let list = lines[*at].2.trim_start().starts_with("- ") || lines[*at].2.trim() == "-";
|
if dashed(lines[*at].2) {
|
||||||
if list {
|
|
||||||
let mut items = Vec::new();
|
let mut items = Vec::new();
|
||||||
while *at < lines.len() && lines[*at].1 == indent {
|
while *at < lines.len() && lines[*at].1 == indent && dashed(lines[*at].2) {
|
||||||
let (number, _, words) = lines[*at];
|
let (number, _, words) = lines[*at];
|
||||||
let Some(rest) = words.trim_start().strip_prefix('-') else {
|
let rest = words.trim_start()[1..].trim_start();
|
||||||
return Err(Unread {
|
|
||||||
line: number,
|
|
||||||
why: "a list holds only items starting with -".into(),
|
|
||||||
});
|
|
||||||
};
|
|
||||||
let rest = rest.trim_start();
|
|
||||||
*at += 1;
|
*at += 1;
|
||||||
items.push(if rest.is_empty() {
|
items.push(if rest.is_empty() {
|
||||||
nested(lines, at, indent)?
|
nested(lines, at, indent)?
|
||||||
@@ -146,10 +164,10 @@ fn entries(
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
let key = SharedString::from(key.trim().trim_matches('"').to_string());
|
let key = SharedString::from(key.trim().trim_matches('"').to_string());
|
||||||
let value = if value.trim().is_empty() {
|
let value = match lines.get(*at) {
|
||||||
nested(lines, at, indent)?
|
_ if !value.trim().is_empty() => scalar(value),
|
||||||
} else {
|
Some((_, same, next)) if *same == indent && dashed(next) => block(lines, at, indent)?,
|
||||||
scalar(value)
|
_ => nested(lines, at, indent)?,
|
||||||
};
|
};
|
||||||
map.push((key, value));
|
map.push((key, value));
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -244,6 +262,44 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn yaml_reads_a_list_at_its_key_indent() {
|
||||||
|
let steps = |run: &str| Datum::Map(vec![("run".into(), text(run))]);
|
||||||
|
assert_eq!(
|
||||||
|
read_yaml("steps:\n- run: build\n- run: test\nname: ci\n").expect("yaml"),
|
||||||
|
Datum::Map(vec![
|
||||||
|
(
|
||||||
|
"steps".into(),
|
||||||
|
Datum::List(vec![steps("build"), steps("test")])
|
||||||
|
),
|
||||||
|
("name".into(), text("ci"))
|
||||||
|
])
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
read_yaml("ports:\n- 80\n- 443\n").expect("yaml"),
|
||||||
|
Datum::Map(vec![(
|
||||||
|
"ports".into(),
|
||||||
|
Datum::List(vec![
|
||||||
|
Datum::Number("80".into()),
|
||||||
|
Datum::Number("443".into())
|
||||||
|
])
|
||||||
|
)])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_hash_inside_quotes_is_no_comment() {
|
||||||
|
assert_eq!(
|
||||||
|
read_yaml("title: \"Issue #12\" # a note\nmessage: 'Fix #42'\nnote: it's #1\n")
|
||||||
|
.expect("yaml"),
|
||||||
|
Datum::Map(vec![
|
||||||
|
("title".into(), text("Issue #12")),
|
||||||
|
("message".into(), text("Fix #42")),
|
||||||
|
("note".into(), text("it's"))
|
||||||
|
])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn yaml_says_which_line_it_could_not_read() {
|
fn yaml_says_which_line_it_could_not_read() {
|
||||||
assert_eq!(read_yaml("a: 1\njust words\n").expect_err("no key").line, 2);
|
assert_eq!(read_yaml("a: 1\njust words\n").expect_err("no key").line, 2);
|
||||||
|
|||||||
@@ -218,6 +218,10 @@ mod tests {
|
|||||||
let mut all = Vec::new();
|
let mut all = Vec::new();
|
||||||
let every = nodes(&datum, "$", "", &mut all);
|
let every = nodes(&datum, "$", "", &mut all);
|
||||||
assert_eq!(every.len(), 3);
|
assert_eq!(every.len(), 3);
|
||||||
|
assert_eq!(
|
||||||
|
every[2].key, "$[\"odd key\"]",
|
||||||
|
"a key beyond letters takes brackets"
|
||||||
|
);
|
||||||
assert!(all.is_empty(), "without a query nothing is opened");
|
assert!(all.is_empty(), "without a query nothing is opened");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -80,9 +80,8 @@ impl RenderOnce for JsonEditor {
|
|||||||
.disabled(read.is_err())
|
.disabled(read.is_err())
|
||||||
.on_click(move |_, _, cx| {
|
.on_click(move |_, _, cx| {
|
||||||
let text = editor.read(cx).text().to_string();
|
let text = editor.read(cx).text().to_string();
|
||||||
let Some(out) = reformat(&text, pretty) else {
|
let out = reformat(&text, pretty)
|
||||||
return;
|
.expect("Format and Minify rest until the JSON reads");
|
||||||
};
|
|
||||||
log::info!("json editor: {label}");
|
log::info!("json editor: {label}");
|
||||||
editor.update(cx, |editor, cx| editor.set_text(out, cx));
|
editor.update(cx, |editor, cx| editor.set_text(out, cx));
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
use gpui::{AnyElement, App, AppContext as _, Entity, IntoElement, TestAppContext, Window, div};
|
use gpui::{
|
||||||
|
AnyElement, App, AppContext as _, Entity, Focusable as _, IntoElement, TestAppContext, Window,
|
||||||
|
div,
|
||||||
|
};
|
||||||
|
|
||||||
use super::{Bench, bench, settle, tab, tap};
|
use super::{Bench, bench, settle, tab, tap};
|
||||||
use crate::{
|
use crate::{
|
||||||
@@ -61,3 +64,20 @@ fn format_indents_the_json_and_minify_folds_it(cx: &mut TestAppContext) {
|
|||||||
r#"{"a":[1,2]}"#
|
r#"{"a":[1,2]}"#
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Stops: the editor alone, the buttons resting while the JSON does not read.
|
||||||
|
#[gpui::test]
|
||||||
|
fn format_and_minify_rest_while_the_json_does_not_read(cx: &mut TestAppContext) {
|
||||||
|
let (host, cx) = bench(editor, cx);
|
||||||
|
let editor = cx.update(|window, cx| cx.new(|cx| CodeEditor::new("{", window, cx)));
|
||||||
|
host.update(cx, |bench, cx| {
|
||||||
|
bench.editor = Some(editor.clone());
|
||||||
|
cx.notify();
|
||||||
|
});
|
||||||
|
settle(cx);
|
||||||
|
tab(1, cx);
|
||||||
|
assert!(
|
||||||
|
cx.update(|window, cx| editor.focus_handle(cx).is_focused(window)),
|
||||||
|
"Tab passes Format and Minify"
|
||||||
|
);
|
||||||
|
}
|
||||||
+81
-14
@@ -32,15 +32,51 @@ struct Reader<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Reader<'a> {
|
impl<'a> Reader<'a> {
|
||||||
fn line(&self) -> usize {
|
/// Fails on the line `ahead` bytes past where the reader stands.
|
||||||
self.text[..self.at].matches('\n').count() + 1
|
fn fail_at<T>(&self, ahead: usize, why: impl Into<String>) -> Result<T, Unread> {
|
||||||
|
Err(Unread {
|
||||||
|
line: self.text[..self.at + ahead].matches('\n').count() + 1,
|
||||||
|
why: why.into(),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn fail<T>(&self, why: impl Into<String>) -> Result<T, Unread> {
|
fn fail<T>(&self, why: impl Into<String>) -> Result<T, Unread> {
|
||||||
Err(Unread {
|
self.fail_at(0, why)
|
||||||
line: self.line(),
|
}
|
||||||
why: why.into(),
|
|
||||||
})
|
/// The `len` bytes where the reader stands, their entity references read.
|
||||||
|
fn decoded(&self, len: usize) -> Result<String, Unread> {
|
||||||
|
let raw = &self.rest()[..len];
|
||||||
|
let mut out = String::with_capacity(len);
|
||||||
|
let mut done = 0;
|
||||||
|
while let Some(amp) = raw[done..].find('&').map(|at| done + at) {
|
||||||
|
out.push_str(&raw[done..amp]);
|
||||||
|
let Some(end) = raw[amp..].find(';').map(|at| amp + at) else {
|
||||||
|
return self.fail_at(amp, "& starts no entity");
|
||||||
|
};
|
||||||
|
let name = &raw[amp + 1..end];
|
||||||
|
if name.is_empty() || name.contains(|c: char| c.is_whitespace() || c == '&') {
|
||||||
|
return self.fail_at(amp, "& starts no entity");
|
||||||
|
}
|
||||||
|
let number = match name.strip_prefix("#x") {
|
||||||
|
Some(hex) => u32::from_str_radix(hex, 16).ok(),
|
||||||
|
None => name.strip_prefix('#').and_then(|dec| dec.parse().ok()),
|
||||||
|
};
|
||||||
|
out.push(match name {
|
||||||
|
"amp" => '&',
|
||||||
|
"lt" => '<',
|
||||||
|
"gt" => '>',
|
||||||
|
"quot" => '"',
|
||||||
|
"apos" => '\'',
|
||||||
|
_ => match number.and_then(char::from_u32) {
|
||||||
|
Some(ch) => ch,
|
||||||
|
None => return self.fail_at(amp, format!("&{name}; is no entity")),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
done = end + 1;
|
||||||
|
}
|
||||||
|
out.push_str(&raw[done..]);
|
||||||
|
Ok(out)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn rest(&self) -> &'a str {
|
fn rest(&self) -> &'a str {
|
||||||
@@ -112,7 +148,7 @@ impl<'a> Reader<'a> {
|
|||||||
let Some(end) = self.rest().find(quote) else {
|
let Some(end) = self.rest().find(quote) else {
|
||||||
return self.fail(format!("{key} in <{name}> is never closed"));
|
return self.fail(format!("{key} in <{name}> is never closed"));
|
||||||
};
|
};
|
||||||
attributes.push((key, self.rest()[..end].to_string().into()));
|
attributes.push((key, self.decoded(end)?.into()));
|
||||||
self.at += end + 1;
|
self.at += end + 1;
|
||||||
}
|
}
|
||||||
let children = self.content()?;
|
let children = self.content()?;
|
||||||
@@ -152,9 +188,9 @@ impl<'a> Reader<'a> {
|
|||||||
children.push(XmlNode::Element(self.element()?));
|
children.push(XmlNode::Element(self.element()?));
|
||||||
} else {
|
} else {
|
||||||
let end = rest.find('<').unwrap_or(rest.len());
|
let end = rest.find('<').unwrap_or(rest.len());
|
||||||
let words = rest[..end].trim();
|
let words = self.decoded(end)?;
|
||||||
if !words.is_empty() {
|
if !words.trim().is_empty() {
|
||||||
children.push(XmlNode::Text(words.to_string().into()));
|
children.push(XmlNode::Text(words.trim().to_string().into()));
|
||||||
}
|
}
|
||||||
self.at += end;
|
self.at += end;
|
||||||
}
|
}
|
||||||
@@ -189,12 +225,15 @@ pub fn read_xml(text: &str) -> Result<XmlElement, Unread> {
|
|||||||
/// Rows for what `element` holds, each element's path listed in `open`.
|
/// Rows for what `element` holds, each element's path listed in `open`.
|
||||||
fn nodes(element: &XmlElement, path: &str, open: &mut Vec<SharedString>) -> Vec<TreeNode> {
|
fn nodes(element: &XmlElement, path: &str, open: &mut Vec<SharedString>) -> Vec<TreeNode> {
|
||||||
let mut counts: Vec<(SharedString, usize)> = Vec::new();
|
let mut counts: Vec<(SharedString, usize)> = Vec::new();
|
||||||
|
let mut texts = 0;
|
||||||
element
|
element
|
||||||
.children
|
.children
|
||||||
.iter()
|
.iter()
|
||||||
.enumerate()
|
.map(|child| match child {
|
||||||
.map(|(ix, child)| match child {
|
XmlNode::Text(text) => {
|
||||||
XmlNode::Text(text) => TreeNode::new(format!("{path}/text()[{ix}]"), text.clone()),
|
texts += 1;
|
||||||
|
TreeNode::new(format!("{path}/text()[{texts}]"), text.clone())
|
||||||
|
}
|
||||||
XmlNode::Element(child) => {
|
XmlNode::Element(child) => {
|
||||||
let seen = match counts.iter_mut().find(|(name, _)| *name == child.name) {
|
let seen = match counts.iter_mut().find(|(name, _)| *name == child.name) {
|
||||||
Some((_, count)) => {
|
Some((_, count)) => {
|
||||||
@@ -289,7 +328,35 @@ impl RenderOnce for XmlViewer {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::{XmlNode, read_xml};
|
use super::{XmlNode, nodes, read_xml};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn entities_read_in_text_and_attributes() {
|
||||||
|
let root =
|
||||||
|
read_xml("<title a=\"Tom & Jerry\">Tom & Jerry <3 AB</title>")
|
||||||
|
.expect("xml");
|
||||||
|
assert_eq!(root.attributes, [("a".into(), "Tom & Jerry".into())]);
|
||||||
|
assert_eq!(root.children, [XmlNode::Text("Tom & Jerry <3 AB".into())]);
|
||||||
|
let unread = read_xml("<a>\n &nope;</a>").expect_err("an unknown entity");
|
||||||
|
assert_eq!(
|
||||||
|
(unread.line, unread.why.as_str()),
|
||||||
|
(2, "&nope; is no entity")
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
read_xml("<a>Q&A</a>").expect_err("a bare ampersand").why,
|
||||||
|
"& starts no entity"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_text_path_counts_text_nodes_from_one() {
|
||||||
|
let root = read_xml("<a>x<b/>y</a>").expect("xml");
|
||||||
|
let keys: Vec<_> = nodes(&root, "/a", &mut Vec::new())
|
||||||
|
.into_iter()
|
||||||
|
.map(|node| node.key)
|
||||||
|
.collect();
|
||||||
|
assert_eq!(keys, ["/a/text()[1]", "/a/b[1]", "/a/text()[2]"]);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn elements_attributes_and_text_read_in_order() {
|
fn elements_attributes_and_text_read_in_order() {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ use std::{ops::Range, rc::Rc};
|
|||||||
use gpui::{
|
use gpui::{
|
||||||
AnyElement, App, DragMoveEvent, ElementId, Entity, EntityId, FocusHandle, InteractiveElement,
|
AnyElement, App, DragMoveEvent, ElementId, Entity, EntityId, FocusHandle, InteractiveElement,
|
||||||
IntoElement, MouseButton, ParentElement, SharedString, StatefulInteractiveElement, Styled,
|
IntoElement, MouseButton, ParentElement, SharedString, StatefulInteractiveElement, Styled,
|
||||||
Window, div, prelude::*,
|
Window, div, prelude::*, relative,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
@@ -210,10 +210,12 @@ fn row(rows: &Rows, ix: usize, window: &mut Window, cx: &mut App) -> AnyElement
|
|||||||
.child(body)
|
.child(body)
|
||||||
.children(note.map(|note| {
|
.children(note.map(|note| {
|
||||||
div()
|
div()
|
||||||
|
.debug_selector(|| "tree-note".into())
|
||||||
.flex_none()
|
.flex_none()
|
||||||
|
.max_w(relative(0.5))
|
||||||
.text_size(theme.text_size(TextSize::Xs))
|
.text_size(theme.text_size(TextSize::Xs))
|
||||||
.text_color(ink.unwrap_or(colors.fg_subtle))
|
.text_color(ink.unwrap_or(colors.fg_subtle))
|
||||||
.child(note)
|
.child(Ellipsis::new(note))
|
||||||
}))
|
}))
|
||||||
.children(landing.filter(|at| *at != DropAt::Inside).map(line))
|
.children(landing.filter(|at| *at != DropAt::Inside).map(line))
|
||||||
.on_click(move |event, window, cx| {
|
.on_click(move |event, window, cx| {
|
||||||
|
|||||||
+16
-1
@@ -34,7 +34,8 @@ impl Render for Explorer {
|
|||||||
TreeNode::new("ui", "ui").child(TreeNode::new("button", "button.rs")),
|
TreeNode::new("ui", "ui").child(TreeNode::new("button", "button.rs")),
|
||||||
]),
|
]),
|
||||||
docs,
|
docs,
|
||||||
TreeNode::new("readme", "README.md"),
|
TreeNode::new("readme", "README.md")
|
||||||
|
.note("a note long enough to take the whole row and more"),
|
||||||
];
|
];
|
||||||
let (select, check, load, rename, moved) = (
|
let (select, check, load, rename, moved) = (
|
||||||
view.clone(),
|
view.clone(),
|
||||||
@@ -95,6 +96,20 @@ fn read<T>(view: &Entity<Explorer>, cx: &mut VisualTestContext, f: impl Fn(&Expl
|
|||||||
view.read_with(cx, |explorer, _| f(explorer))
|
view.read_with(cx, |explorer, _| f(explorer))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
fn a_long_note_leaves_the_label_half_the_row(cx: &mut TestAppContext) {
|
||||||
|
let (_, cx) = explorer(false, cx);
|
||||||
|
let note = cx.debug_bounds("tree-note").expect("the readme's note");
|
||||||
|
assert!(
|
||||||
|
note.size.width <= px(150.0),
|
||||||
|
"the note takes at most half the row: {note:?}"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
note.size.width > px(100.0),
|
||||||
|
"the note keeps its half: {note:?}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
fn keys_walk_open_climb_and_close(cx: &mut TestAppContext) {
|
fn keys_walk_open_climb_and_close(cx: &mut TestAppContext) {
|
||||||
let (view, cx) = explorer(false, cx);
|
let (view, cx) = explorer(false, cx);
|
||||||
|
|||||||
Reference in new issue
Block a user