Merge remote-tracking branch 'origin/main' into assistant-buffer

This commit is contained in:
Antonio Scandurra 2023-06-14 09:16:45 +02:00
commit ea5d677ef8
38 changed files with 4097 additions and 3182 deletions

61
Cargo.lock generated
View File

@ -3515,6 +3515,29 @@ dependencies = [
"workspace",
]
[[package]]
name = "language_tools"
version = "0.1.0"
dependencies = [
"anyhow",
"client",
"collections",
"editor",
"env_logger 0.9.3",
"futures 0.3.28",
"gpui",
"language",
"lsp",
"project",
"serde",
"settings",
"theme",
"tree-sitter",
"unindent",
"util",
"workspace",
]
[[package]]
name = "lazy_static"
version = "1.4.0"
@ -3759,28 +3782,6 @@ dependencies = [
"url",
]
[[package]]
name = "lsp_log"
version = "0.1.0"
dependencies = [
"anyhow",
"client",
"collections",
"editor",
"env_logger 0.9.3",
"futures 0.3.28",
"gpui",
"language",
"lsp",
"project",
"serde",
"settings",
"theme",
"unindent",
"util",
"workspace",
]
[[package]]
name = "mach"
version = "0.3.2"
@ -7358,8 +7359,8 @@ dependencies = [
[[package]]
name = "tree-sitter"
version = "0.20.9"
source = "git+https://github.com/tree-sitter/tree-sitter?rev=c51896d32dcc11a38e41f36e3deb1a6a9c4f4b14#c51896d32dcc11a38e41f36e3deb1a6a9c4f4b14"
version = "0.20.10"
source = "git+https://github.com/tree-sitter/tree-sitter?rev=49226023693107fba9a1191136a4f47f38cdca73#49226023693107fba9a1191136a4f47f38cdca73"
dependencies = [
"cc",
"regex",
@ -7422,6 +7423,15 @@ dependencies = [
"tree-sitter",
]
[[package]]
name = "tree-sitter-heex"
version = "0.0.1"
source = "git+https://github.com/phoenixframework/tree-sitter-heex?rev=2e1348c3cf2c9323e87c2744796cf3f3868aa82a#2e1348c3cf2c9323e87c2744796cf3f3868aa82a"
dependencies = [
"cc",
"tree-sitter",
]
[[package]]
name = "tree-sitter-html"
version = "0.19.0"
@ -8829,11 +8839,11 @@ dependencies = [
"journal",
"language",
"language_selector",
"language_tools",
"lazy_static",
"libc",
"log",
"lsp",
"lsp_log",
"node_runtime",
"num_cpus",
"outline",
@ -8875,6 +8885,7 @@ dependencies = [
"tree-sitter-elixir",
"tree-sitter-embedded-template",
"tree-sitter-go",
"tree-sitter-heex",
"tree-sitter-html",
"tree-sitter-json 0.20.0",
"tree-sitter-lua",

View File

@ -32,10 +32,10 @@ members = [
"crates/journal",
"crates/language",
"crates/language_selector",
"crates/language_tools",
"crates/live_kit_client",
"crates/live_kit_server",
"crates/lsp",
"crates/lsp_log",
"crates/media",
"crates/menu",
"crates/node_runtime",
@ -98,10 +98,11 @@ tempdir = { version = "0.3.7" }
thiserror = { version = "1.0.29" }
time = { version = "0.3", features = ["serde", "serde-well-known"] }
toml = { version = "0.5" }
tree-sitter = "0.20"
unindent = { version = "0.1.7" }
[patch.crates-io]
tree-sitter = { git = "https://github.com/tree-sitter/tree-sitter", rev = "c51896d32dcc11a38e41f36e3deb1a6a9c4f4b14" }
tree-sitter = { git = "https://github.com/tree-sitter/tree-sitter", rev = "49226023693107fba9a1191136a4f47f38cdca73" }
async-task = { git = "https://github.com/zed-industries/async-task", rev = "341b57d6de98cdfd7b418567b8de2022ca993a6e" }
# TODO - Remove when a version is released with this PR: https://github.com/servo/core-foundation-rs/pull/457

View File

@ -108,6 +108,8 @@
// Whether or not to remove any trailing whitespace from lines of a buffer
// before saving it.
"remove_trailing_whitespace_on_save": true,
// Whether to start a new line with a comment when a previous line is a comment as well.
"extend_comment_on_newline": true,
// Whether or not to ensure there's a single newline at the end of a buffer
// when saving it.
"ensure_final_newline_on_save": true,

View File

@ -1,11 +1,5 @@
// Folder-specific Zed settings
// Folder-specific settings
//
// A subset of Zed's settings can be configured on a per-folder basis.
//
// For information on how to configure Zed, see the Zed
// documentation: https://zed.dev/docs/configuring-zed
//
// To see all of Zed's default settings without changing your
// custom settings, run the `open default settings` command
// from the command palette or from `Zed` application menu.
// For a full list of overridable settings, and general information on folder-specific settings,
// see the documentation: https://docs.zed.dev/configuration/configuring-zed#folder-specific-settings
{}

View File

@ -1,7 +1,7 @@
// Folder-specific settings
// Zed settings
//
// For a full list of overridable settings, and general information on folder-specific settings, see the documentation:
// https://docs.zed.dev/configuration/configuring-zed#folder-specific-settings
// For information on how to configure Zed, see the Zed
// documentation: https://zed.dev/docs/configuring-zed
//
// To see all of Zed's default settings without changing your
// custom settings, run the `open default settings` command

View File

@ -83,7 +83,7 @@ ctor.workspace = true
env_logger.workspace = true
rand.workspace = true
unindent.workspace = true
tree-sitter = "0.20"
tree-sitter.workspace = true
tree-sitter-rust = "0.20"
tree-sitter-html = "0.19"
tree-sitter-typescript = { git = "https://github.com/tree-sitter/tree-sitter-typescript", rev = "5d20856f34315b068c41edaee2ac8a100081d259" }

View File

@ -2169,8 +2169,8 @@ impl Editor {
self.transact(cx, |this, cx| {
let (edits, selection_fixup_info): (Vec<_>, Vec<_>) = {
let selections = this.selections.all::<usize>(cx);
let buffer = this.buffer.read(cx).snapshot(cx);
let multi_buffer = this.buffer.read(cx);
let buffer = multi_buffer.snapshot(cx);
selections
.iter()
.map(|selection| {
@ -2181,70 +2181,74 @@ impl Editor {
let end = selection.end;
let is_cursor = start == end;
let language_scope = buffer.language_scope_at(start);
let (comment_delimiter, insert_extra_newline) =
if let Some(language) = &language_scope {
let leading_whitespace_len = buffer
.reversed_chars_at(start)
.take_while(|c| c.is_whitespace() && *c != '\n')
.map(|c| c.len_utf8())
.sum::<usize>();
let (comment_delimiter, insert_extra_newline) = if let Some(language) =
&language_scope
{
let leading_whitespace_len = buffer
.reversed_chars_at(start)
.take_while(|c| c.is_whitespace() && *c != '\n')
.map(|c| c.len_utf8())
.sum::<usize>();
let trailing_whitespace_len = buffer
.chars_at(end)
.take_while(|c| c.is_whitespace() && *c != '\n')
.map(|c| c.len_utf8())
.sum::<usize>();
let trailing_whitespace_len = buffer
.chars_at(end)
.take_while(|c| c.is_whitespace() && *c != '\n')
.map(|c| c.len_utf8())
.sum::<usize>();
let insert_extra_newline =
language.brackets().any(|(pair, enabled)| {
let pair_start = pair.start.trim_end();
let pair_end = pair.end.trim_start();
let insert_extra_newline =
language.brackets().any(|(pair, enabled)| {
let pair_start = pair.start.trim_end();
let pair_end = pair.end.trim_start();
enabled
&& pair.newline
&& buffer.contains_str_at(
end + trailing_whitespace_len,
pair_end,
)
&& buffer.contains_str_at(
(start - leading_whitespace_len)
.saturating_sub(pair_start.len()),
pair_start,
)
});
// Comment extension on newline is allowed only for cursor selections
let comment_delimiter =
language.line_comment_prefix().filter(|_| is_cursor);
let comment_delimiter = if let Some(delimiter) = comment_delimiter {
buffer
.buffer_line_for_row(start_point.row)
.is_some_and(|(snapshot, range)| {
let mut index_of_first_non_whitespace = 0;
let line_starts_with_comment = snapshot
.chars_for_range(range)
.skip_while(|c| {
let should_skip = c.is_whitespace();
if should_skip {
index_of_first_non_whitespace += 1;
}
should_skip
})
.take(delimiter.len())
.eq(delimiter.chars());
let cursor_is_placed_after_comment_marker =
index_of_first_non_whitespace + delimiter.len()
<= start_point.column as usize;
line_starts_with_comment
&& cursor_is_placed_after_comment_marker
})
.then(|| delimiter.clone())
} else {
None
};
(comment_delimiter, insert_extra_newline)
enabled
&& pair.newline
&& buffer.contains_str_at(
end + trailing_whitespace_len,
pair_end,
)
&& buffer.contains_str_at(
(start - leading_whitespace_len)
.saturating_sub(pair_start.len()),
pair_start,
)
});
// Comment extension on newline is allowed only for cursor selections
let comment_delimiter = language.line_comment_prefix().filter(|_| {
let is_comment_extension_enabled =
multi_buffer.settings_at(0, cx).extend_comment_on_newline;
is_cursor && is_comment_extension_enabled
});
let comment_delimiter = if let Some(delimiter) = comment_delimiter {
buffer
.buffer_line_for_row(start_point.row)
.is_some_and(|(snapshot, range)| {
let mut index_of_first_non_whitespace = 0;
let line_starts_with_comment = snapshot
.chars_for_range(range)
.skip_while(|c| {
let should_skip = c.is_whitespace();
if should_skip {
index_of_first_non_whitespace += 1;
}
should_skip
})
.take(delimiter.len())
.eq(delimiter.chars());
let cursor_is_placed_after_comment_marker =
index_of_first_non_whitespace + delimiter.len()
<= start_point.column as usize;
line_starts_with_comment
&& cursor_is_placed_after_comment_marker
})
.then(|| delimiter.clone())
} else {
(None, false)
None
};
(comment_delimiter, insert_extra_newline)
} else {
(None, false)
};
let capacity_for_delimiter = comment_delimiter
.as_deref()
@ -5492,7 +5496,7 @@ impl Editor {
let mut all_selection_lines_are_comments = true;
for row in start_row..=end_row {
if snapshot.is_line_blank(row) {
if snapshot.is_line_blank(row) && start_row < end_row {
continue;
}
@ -7102,7 +7106,7 @@ impl Editor {
let mut new_selections_by_buffer = HashMap::default();
for selection in editor.selections.all::<usize>(cx) {
for (buffer, mut range) in
for (buffer, mut range, _) in
buffer.range_to_buffer_ranges(selection.start..selection.end, cx)
{
if selection.reversed {
@ -7272,7 +7276,7 @@ impl Editor {
let vim_mode = cx
.global::<SettingsStore>()
.untyped_user_settings()
.raw_user_settings()
.get("vim_mode")
== Some(&serde_json::Value::Bool(true));
let telemetry_settings = *settings::get::<TelemetrySettings>(cx);

View File

@ -1732,26 +1732,40 @@ async fn test_newline_comments(cx: &mut gpui::TestAppContext) {
},
None,
));
let mut cx = EditorTestContext::new(cx).await;
cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
cx.set_state(indoc! {"
{
let mut cx = EditorTestContext::new(cx).await;
cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
cx.set_state(indoc! {"
// Fooˇ
"});
cx.update_editor(|e, cx| e.newline(&Newline, cx));
cx.assert_editor_state(indoc! {"
cx.update_editor(|e, cx| e.newline(&Newline, cx));
cx.assert_editor_state(indoc! {"
// Foo
//ˇ
"});
// Ensure that if cursor is before the comment start, we do not actually insert a comment prefix.
cx.set_state(indoc! {"
// Ensure that if cursor is before the comment start, we do not actually insert a comment prefix.
cx.set_state(indoc! {"
ˇ// Foo
"});
cx.update_editor(|e, cx| e.newline(&Newline, cx));
cx.assert_editor_state(indoc! {"
ˇ// Foo
"});
}
// Ensure that comment continuations can be disabled.
update_test_settings(cx, |settings| {
settings.defaults.extend_comment_on_newline = Some(false);
});
let mut cx = EditorTestContext::new(cx).await;
cx.set_state(indoc! {"
// Fooˇ
"});
cx.update_editor(|e, cx| e.newline(&Newline, cx));
cx.assert_editor_state(indoc! {"
ˇ// Foo
// Foo
ˇ
"});
}
@ -4930,7 +4944,7 @@ async fn test_completion(cx: &mut gpui::TestAppContext) {
#[gpui::test]
async fn test_toggle_comment(cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {});
let mut cx = EditorTestContext::new(cx).await;
let language = Arc::new(Language::new(
LanguageConfig {
line_comment: Some("// ".into()),
@ -4938,77 +4952,95 @@ async fn test_toggle_comment(cx: &mut gpui::TestAppContext) {
},
Some(tree_sitter_rust::language()),
));
cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
let text = "
// If multiple selections intersect a line, the line is only toggled once.
cx.set_state(indoc! {"
fn a() {
//b();
// c();
// d();
«//b();
ˇ»// «c();
//ˇ» d();
}
"
.unindent();
"});
let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, cx));
let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
let (_, view) = cx.add_window(|cx| build_editor(buffer, cx));
cx.update_editor(|e, cx| e.toggle_comments(&ToggleComments::default(), cx));
view.update(cx, |editor, cx| {
// If multiple selections intersect a line, the line is only
// toggled once.
editor.change_selections(None, cx, |s| {
s.select_display_ranges([
DisplayPoint::new(1, 3)..DisplayPoint::new(2, 3),
DisplayPoint::new(3, 5)..DisplayPoint::new(3, 6),
])
});
editor.toggle_comments(&ToggleComments::default(), cx);
assert_eq!(
editor.text(cx),
"
fn a() {
b();
c();
d();
}
"
.unindent()
);
cx.assert_editor_state(indoc! {"
fn a() {
«b();
c();
ˇ» d();
}
"});
// The comment prefix is inserted at the same column for every line
// in a selection.
editor.change_selections(None, cx, |s| {
s.select_display_ranges([DisplayPoint::new(1, 3)..DisplayPoint::new(3, 6)])
});
editor.toggle_comments(&ToggleComments::default(), cx);
assert_eq!(
editor.text(cx),
"
fn a() {
// b();
// c();
// d();
}
"
.unindent()
);
// The comment prefix is inserted at the same column for every line in a
// selection.
cx.update_editor(|e, cx| e.toggle_comments(&ToggleComments::default(), cx));
// If a selection ends at the beginning of a line, that line is not toggled.
editor.change_selections(None, cx, |s| {
s.select_display_ranges([DisplayPoint::new(2, 0)..DisplayPoint::new(3, 0)])
});
editor.toggle_comments(&ToggleComments::default(), cx);
assert_eq!(
editor.text(cx),
"
fn a() {
// b();
c();
// d();
}
"
.unindent()
);
});
cx.assert_editor_state(indoc! {"
fn a() {
// «b();
// c();
ˇ»// d();
}
"});
// If a selection ends at the beginning of a line, that line is not toggled.
cx.set_selections_state(indoc! {"
fn a() {
// b();
«// c();
ˇ» // d();
}
"});
cx.update_editor(|e, cx| e.toggle_comments(&ToggleComments::default(), cx));
cx.assert_editor_state(indoc! {"
fn a() {
// b();
«c();
ˇ» // d();
}
"});
// If a selection span a single line and is empty, the line is toggled.
cx.set_state(indoc! {"
fn a() {
a();
b();
ˇ
}
"});
cx.update_editor(|e, cx| e.toggle_comments(&ToggleComments::default(), cx));
cx.assert_editor_state(indoc! {"
fn a() {
a();
b();
//•ˇ
}
"});
// If a selection span multiple lines, empty lines are not toggled.
cx.set_state(indoc! {"
fn a() {
«a();
c();ˇ»
}
"});
cx.update_editor(|e, cx| e.toggle_comments(&ToggleComments::default(), cx));
cx.assert_editor_state(indoc! {"
fn a() {
// «a();
// c();ˇ»
}
"});
}
#[gpui::test]

View File

@ -1118,7 +1118,7 @@ impl MultiBuffer {
&self,
point: T,
cx: &AppContext,
) -> Option<(ModelHandle<Buffer>, usize)> {
) -> Option<(ModelHandle<Buffer>, usize, ExcerptId)> {
let snapshot = self.read(cx);
let offset = point.to_offset(&snapshot);
let mut cursor = snapshot.excerpts.cursor::<usize>();
@ -1132,7 +1132,7 @@ impl MultiBuffer {
let buffer_point = excerpt_start + offset - *cursor.start();
let buffer = self.buffers.borrow()[&excerpt.buffer_id].buffer.clone();
(buffer, buffer_point)
(buffer, buffer_point, excerpt.id)
})
}
@ -1140,7 +1140,7 @@ impl MultiBuffer {
&self,
range: Range<T>,
cx: &AppContext,
) -> Vec<(ModelHandle<Buffer>, Range<usize>)> {
) -> Vec<(ModelHandle<Buffer>, Range<usize>, ExcerptId)> {
let snapshot = self.read(cx);
let start = range.start.to_offset(&snapshot);
let end = range.end.to_offset(&snapshot);
@ -1165,7 +1165,7 @@ impl MultiBuffer {
let start = excerpt_start + (cmp::max(start, *cursor.start()) - *cursor.start());
let end = excerpt_start + (cmp::min(end, end_before_newline) - *cursor.start());
let buffer = self.buffers.borrow()[&excerpt.buffer_id].buffer.clone();
result.push((buffer, start..end));
result.push((buffer, start..end, excerpt.id));
cursor.next(&());
}
@ -1387,7 +1387,7 @@ impl MultiBuffer {
cx: &'a AppContext,
) -> Option<Arc<Language>> {
self.point_to_buffer_offset(point, cx)
.and_then(|(buffer, offset)| buffer.read(cx).language_at(offset))
.and_then(|(buffer, offset, _)| buffer.read(cx).language_at(offset))
}
pub fn settings_at<'a, T: ToOffset>(
@ -1397,7 +1397,7 @@ impl MultiBuffer {
) -> &'a LanguageSettings {
let mut language = None;
let mut file = None;
if let Some((buffer, offset)) = self.point_to_buffer_offset(point, cx) {
if let Some((buffer, offset, _)) = self.point_to_buffer_offset(point, cx) {
let buffer = buffer.read(cx);
language = buffer.language_at(offset);
file = buffer.file();
@ -5196,7 +5196,7 @@ mod tests {
.range_to_buffer_ranges(start_ix..end_ix, cx);
let excerpted_buffers_text = excerpted_buffer_ranges
.iter()
.map(|(buffer, buffer_range)| {
.map(|(buffer, buffer_range, _)| {
buffer
.read(cx)
.text_for_range(buffer_range.clone())

View File

@ -55,7 +55,7 @@ serde_json.workspace = true
similar = "1.3"
smallvec.workspace = true
smol.workspace = true
tree-sitter = "0.20"
tree-sitter.workspace = true
tree-sitter-rust = { version = "*", optional = true }
tree-sitter-typescript = { version = "*", optional = true }
unicase = "2.6"
@ -72,6 +72,8 @@ ctor.workspace = true
env_logger.workspace = true
indoc.workspace = true
rand.workspace = true
unindent.workspace = true
tree-sitter-embedded-template = "*"
tree-sitter-html = "*"
tree-sitter-javascript = "*"
@ -81,4 +83,3 @@ tree-sitter-rust = "*"
tree-sitter-python = "*"
tree-sitter-typescript = "*"
tree-sitter-ruby = "*"
unindent.workspace = true

View File

@ -8,7 +8,8 @@ use crate::{
language_settings::{language_settings, LanguageSettings},
outline::OutlineItem,
syntax_map::{
SyntaxMap, SyntaxMapCapture, SyntaxMapCaptures, SyntaxSnapshot, ToTreeSitterPoint,
SyntaxLayerInfo, SyntaxMap, SyntaxMapCapture, SyntaxMapCaptures, SyntaxSnapshot,
ToTreeSitterPoint,
},
CodeLabel, LanguageScope, Outline,
};
@ -2116,12 +2117,20 @@ impl BufferSnapshot {
}
}
pub fn language_at<D: ToOffset>(&self, position: D) -> Option<&Arc<Language>> {
pub fn syntax_layers(&self) -> impl Iterator<Item = SyntaxLayerInfo> + '_ {
self.syntax.layers_for_range(0..self.len(), &self.text)
}
pub fn syntax_layer_at<D: ToOffset>(&self, position: D) -> Option<SyntaxLayerInfo> {
let offset = position.to_offset(self);
self.syntax
.layers_for_range(offset..offset, &self.text)
.filter(|l| l.node.end_byte() > offset)
.filter(|l| l.node().end_byte() > offset)
.last()
}
pub fn language_at<D: ToOffset>(&self, position: D) -> Option<&Arc<Language>> {
self.syntax_layer_at(position)
.map(|info| info.language)
.or(self.language.as_ref())
}
@ -2140,7 +2149,7 @@ impl BufferSnapshot {
if let Some(layer_info) = self
.syntax
.layers_for_range(offset..offset, &self.text)
.filter(|l| l.node.end_byte() > offset)
.filter(|l| l.node().end_byte() > offset)
.last()
{
Some(LanguageScope {
@ -2188,7 +2197,7 @@ impl BufferSnapshot {
let range = range.start.to_offset(self)..range.end.to_offset(self);
let mut result: Option<Range<usize>> = None;
'outer: for layer in self.syntax.layers_for_range(range.clone(), &self.text) {
let mut cursor = layer.node.walk();
let mut cursor = layer.node().walk();
// Descend to the first leaf that touches the start of the range,
// and if the range is non-empty, extends beyond the start.

View File

@ -2242,7 +2242,7 @@ fn get_tree_sexp(buffer: &ModelHandle<Buffer>, cx: &gpui::TestAppContext) -> Str
buffer.read_with(cx, |buffer, _| {
let snapshot = buffer.snapshot();
let layers = snapshot.syntax.layers(buffer.as_text_snapshot());
layers[0].node.to_sexp()
layers[0].node().to_sexp()
})
}

View File

@ -57,6 +57,7 @@ pub use buffer::*;
pub use diagnostic_set::DiagnosticEntry;
pub use lsp::LanguageServerId;
pub use outline::{Outline, OutlineItem};
pub use syntax_map::{OwnedSyntaxLayerInfo, SyntaxLayerInfo};
pub use tree_sitter::{Parser, Tree};
pub fn init(cx: &mut AppContext) {

View File

@ -51,6 +51,7 @@ pub struct LanguageSettings {
pub enable_language_server: bool,
pub show_copilot_suggestions: bool,
pub show_whitespaces: ShowWhitespaceSetting,
pub extend_comment_on_newline: bool,
}
#[derive(Clone, Debug, Default)]
@ -95,6 +96,8 @@ pub struct LanguageSettingsContent {
pub show_copilot_suggestions: Option<bool>,
#[serde(default)]
pub show_whitespaces: Option<ShowWhitespaceSetting>,
#[serde(default)]
pub extend_comment_on_newline: Option<bool>,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
@ -340,7 +343,10 @@ fn merge_settings(settings: &mut LanguageSettings, src: &LanguageSettingsContent
src.show_copilot_suggestions,
);
merge(&mut settings.show_whitespaces, src.show_whitespaces);
merge(
&mut settings.extend_comment_on_newline,
src.extend_comment_on_newline,
);
fn merge<T>(target: &mut T, value: Option<T>) {
if let Some(value) = value {
*target = value;

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,11 +1,11 @@
[package]
name = "lsp_log"
name = "language_tools"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
path = "src/lsp_log.rs"
path = "src/language_tools.rs"
doctest = false
[dependencies]
@ -22,6 +22,7 @@ lsp = { path = "../lsp" }
futures.workspace = true
serde.workspace = true
anyhow.workspace = true
tree-sitter.workspace = true
[dev-dependencies]
client = { path = "../client", features = ["test-support"] }

View File

@ -0,0 +1,15 @@
mod lsp_log;
mod syntax_tree_view;
#[cfg(test)]
mod lsp_log_tests;
use gpui::AppContext;
pub use lsp_log::{LogStore, LspLogToolbarItemView, LspLogView};
pub use syntax_tree_view::{SyntaxTreeToolbarItemView, SyntaxTreeView};
pub fn init(cx: &mut AppContext) {
lsp_log::init(cx);
syntax_tree_view::init(cx);
}

View File

@ -1,6 +1,3 @@
#[cfg(test)]
mod lsp_log_tests;
use collections::HashMap;
use editor::Editor;
use futures::{channel::mpsc, StreamExt};
@ -27,7 +24,7 @@ use workspace::{
const SEND_LINE: &str = "// Send:\n";
const RECEIVE_LINE: &str = "// Receive:\n";
struct LogStore {
pub struct LogStore {
projects: HashMap<WeakModelHandle<Project>, ProjectState>,
io_tx: mpsc::UnboundedSender<(WeakModelHandle<Project>, LanguageServerId, bool, String)>,
}
@ -49,10 +46,10 @@ struct LanguageServerRpcState {
}
pub struct LspLogView {
pub(crate) editor: ViewHandle<Editor>,
log_store: ModelHandle<LogStore>,
current_server_id: Option<LanguageServerId>,
is_showing_rpc_trace: bool,
editor: ViewHandle<Editor>,
project: ModelHandle<Project>,
}
@ -68,16 +65,16 @@ enum MessageKind {
}
#[derive(Clone, Debug, PartialEq)]
struct LogMenuItem {
server_id: LanguageServerId,
server_name: LanguageServerName,
worktree: ModelHandle<Worktree>,
rpc_trace_enabled: bool,
rpc_trace_selected: bool,
logs_selected: bool,
pub(crate) struct LogMenuItem {
pub server_id: LanguageServerId,
pub server_name: LanguageServerName,
pub worktree: ModelHandle<Worktree>,
pub rpc_trace_enabled: bool,
pub rpc_trace_selected: bool,
pub logs_selected: bool,
}
actions!(log, [OpenLanguageServerLogs]);
actions!(debug, [OpenLanguageServerLogs]);
pub fn init(cx: &mut AppContext) {
let log_store = cx.add_model(|cx| LogStore::new(cx));
@ -114,7 +111,7 @@ pub fn init(cx: &mut AppContext) {
}
impl LogStore {
fn new(cx: &mut ModelContext<Self>) -> Self {
pub fn new(cx: &mut ModelContext<Self>) -> Self {
let (io_tx, mut io_rx) = mpsc::unbounded();
let this = Self {
projects: HashMap::default(),
@ -320,7 +317,7 @@ impl LogStore {
}
impl LspLogView {
fn new(
pub fn new(
project: ModelHandle<Project>,
log_store: ModelHandle<LogStore>,
cx: &mut ViewContext<Self>,
@ -360,7 +357,7 @@ impl LspLogView {
editor
}
fn menu_items<'a>(&'a self, cx: &'a AppContext) -> Option<Vec<LogMenuItem>> {
pub(crate) fn menu_items<'a>(&'a self, cx: &'a AppContext) -> Option<Vec<LogMenuItem>> {
let log_store = self.log_store.read(cx);
let state = log_store.projects.get(&self.project.downgrade())?;
let mut rows = self
@ -544,12 +541,7 @@ impl View for LspLogToolbarItemView {
let theme = theme::current(cx).clone();
let Some(log_view) = self.log_view.as_ref() else { return Empty::new().into_any() };
let log_view = log_view.read(cx);
let menu_rows = self
.log_view
.as_ref()
.and_then(|view| view.read(cx).menu_items(cx))
.unwrap_or_default();
let menu_rows = log_view.menu_items(cx).unwrap_or_default();
let current_server_id = log_view.current_server_id;
let current_server = current_server_id.and_then(|current_server_id| {
@ -586,7 +578,7 @@ impl View for LspLogToolbarItemView {
)
}))
.contained()
.with_style(theme.lsp_log_menu.container)
.with_style(theme.toolbar_dropdown_menu.container)
.constrained()
.with_width(400.)
.with_height(400.)
@ -596,6 +588,7 @@ impl View for LspLogToolbarItemView {
cx.notify()
}),
)
.with_hoverable(true)
.with_fit_mode(OverlayFitMode::SwitchAnchor)
.with_anchor_corner(AnchorCorner::TopLeft)
.with_z_index(999)
@ -688,7 +681,7 @@ impl LspLogToolbarItemView {
)
})
.unwrap_or_else(|| "No server selected".into());
let style = theme.lsp_log_menu.header.style_for(state, false);
let style = theme.toolbar_dropdown_menu.header.style_for(state, false);
Label::new(label, style.text.clone())
.contained()
.with_style(style.container)
@ -714,7 +707,7 @@ impl LspLogToolbarItemView {
Flex::column()
.with_child({
let style = &theme.lsp_log_menu.server;
let style = &theme.toolbar_dropdown_menu.section_header;
Label::new(
format!("{} ({})", name.0, worktree.read(cx).root_name()),
style.text.clone(),
@ -722,16 +715,19 @@ impl LspLogToolbarItemView {
.contained()
.with_style(style.container)
.constrained()
.with_height(theme.lsp_log_menu.row_height)
.with_height(theme.toolbar_dropdown_menu.row_height)
})
.with_child(
MouseEventHandler::<ActivateLog, _>::new(id.0, cx, move |state, _| {
let style = theme.lsp_log_menu.item.style_for(state, logs_selected);
let style = theme
.toolbar_dropdown_menu
.item
.style_for(state, logs_selected);
Label::new(SERVER_LOGS, style.text.clone())
.contained()
.with_style(style.container)
.constrained()
.with_height(theme.lsp_log_menu.row_height)
.with_height(theme.toolbar_dropdown_menu.row_height)
})
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, move |_, view, cx| {
@ -740,12 +736,15 @@ impl LspLogToolbarItemView {
)
.with_child(
MouseEventHandler::<ActivateRpcTrace, _>::new(id.0, cx, move |state, cx| {
let style = theme.lsp_log_menu.item.style_for(state, rpc_trace_selected);
let style = theme
.toolbar_dropdown_menu
.item
.style_for(state, rpc_trace_selected);
Flex::row()
.with_child(
Label::new(RPC_MESSAGES, style.text.clone())
.constrained()
.with_height(theme.lsp_log_menu.row_height),
.with_height(theme.toolbar_dropdown_menu.row_height),
)
.with_child(
ui::checkbox_with_label::<Self, _, Self, _>(
@ -764,7 +763,7 @@ impl LspLogToolbarItemView {
.contained()
.with_style(style.container)
.constrained()
.with_height(theme.lsp_log_menu.row_height)
.with_height(theme.toolbar_dropdown_menu.row_height)
})
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, move |_, view, cx| {

View File

@ -1,7 +1,12 @@
use std::sync::Arc;
use crate::lsp_log::LogMenuItem;
use super::*;
use futures::StreamExt;
use gpui::{serde_json::json, TestAppContext};
use language::{tree_sitter_rust, FakeLspAdapter, Language, LanguageConfig};
use project::FakeFs;
use language::{tree_sitter_rust, FakeLspAdapter, Language, LanguageConfig, LanguageServerName};
use project::{FakeFs, Project};
use settings::SettingsStore;
#[gpui::test]

View File

@ -0,0 +1,675 @@
use editor::{scroll::autoscroll::Autoscroll, Anchor, Editor, ExcerptId};
use gpui::{
actions,
elements::{
AnchorCorner, Empty, Flex, Label, MouseEventHandler, Overlay, OverlayFitMode,
ParentElement, ScrollTarget, Stack, UniformList, UniformListState,
},
fonts::TextStyle,
platform::{CursorStyle, MouseButton},
AppContext, Element, Entity, ModelHandle, View, ViewContext, ViewHandle, WeakViewHandle,
};
use language::{Buffer, OwnedSyntaxLayerInfo, SyntaxLayerInfo};
use std::{mem, ops::Range, sync::Arc};
use theme::{Theme, ThemeSettings};
use tree_sitter::{Node, TreeCursor};
use workspace::{
item::{Item, ItemHandle},
ToolbarItemLocation, ToolbarItemView, Workspace,
};
actions!(debug, [OpenSyntaxTreeView]);
pub fn init(cx: &mut AppContext) {
cx.add_action(
move |workspace: &mut Workspace, _: &OpenSyntaxTreeView, cx: _| {
let active_item = workspace.active_item(cx);
let workspace_handle = workspace.weak_handle();
let syntax_tree_view =
cx.add_view(|cx| SyntaxTreeView::new(workspace_handle, active_item, cx));
workspace.add_item(Box::new(syntax_tree_view), cx);
},
);
}
pub struct SyntaxTreeView {
workspace_handle: WeakViewHandle<Workspace>,
editor: Option<EditorState>,
mouse_y: Option<f32>,
line_height: Option<f32>,
list_state: UniformListState,
selected_descendant_ix: Option<usize>,
hovered_descendant_ix: Option<usize>,
}
pub struct SyntaxTreeToolbarItemView {
tree_view: Option<ViewHandle<SyntaxTreeView>>,
subscription: Option<gpui::Subscription>,
menu_open: bool,
}
struct EditorState {
editor: ViewHandle<Editor>,
active_buffer: Option<BufferState>,
_subscription: gpui::Subscription,
}
#[derive(Clone)]
struct BufferState {
buffer: ModelHandle<Buffer>,
excerpt_id: ExcerptId,
active_layer: Option<OwnedSyntaxLayerInfo>,
}
impl SyntaxTreeView {
pub fn new(
workspace_handle: WeakViewHandle<Workspace>,
active_item: Option<Box<dyn ItemHandle>>,
cx: &mut ViewContext<Self>,
) -> Self {
let mut this = Self {
workspace_handle: workspace_handle.clone(),
list_state: UniformListState::default(),
editor: None,
mouse_y: None,
line_height: None,
hovered_descendant_ix: None,
selected_descendant_ix: None,
};
this.workspace_updated(active_item, cx);
cx.observe(
&workspace_handle.upgrade(cx).unwrap(),
|this, workspace, cx| {
this.workspace_updated(workspace.read(cx).active_item(cx), cx);
},
)
.detach();
this
}
fn workspace_updated(
&mut self,
active_item: Option<Box<dyn ItemHandle>>,
cx: &mut ViewContext<Self>,
) {
if let Some(item) = active_item {
if item.id() != cx.view_id() {
if let Some(editor) = item.act_as::<Editor>(cx) {
self.set_editor(editor, cx);
}
}
}
}
fn set_editor(&mut self, editor: ViewHandle<Editor>, cx: &mut ViewContext<Self>) {
if let Some(state) = &self.editor {
if state.editor == editor {
return;
}
editor.update(cx, |editor, cx| {
editor.clear_background_highlights::<Self>(cx)
});
}
let subscription = cx.subscribe(&editor, |this, _, event, cx| {
let did_reparse = match event {
editor::Event::Reparsed => true,
editor::Event::SelectionsChanged { .. } => false,
_ => return,
};
this.editor_updated(did_reparse, cx);
});
self.editor = Some(EditorState {
editor,
_subscription: subscription,
active_buffer: None,
});
self.editor_updated(true, cx);
}
fn editor_updated(&mut self, did_reparse: bool, cx: &mut ViewContext<Self>) -> Option<()> {
// Find which excerpt the cursor is in, and the position within that excerpted buffer.
let editor_state = self.editor.as_mut()?;
let editor = &editor_state.editor.read(cx);
let selection_range = editor.selections.last::<usize>(cx).range();
let multibuffer = editor.buffer().read(cx);
let (buffer, range, excerpt_id) = multibuffer
.range_to_buffer_ranges(selection_range, cx)
.pop()?;
// If the cursor has moved into a different excerpt, retrieve a new syntax layer
// from that buffer.
let buffer_state = editor_state
.active_buffer
.get_or_insert_with(|| BufferState {
buffer: buffer.clone(),
excerpt_id,
active_layer: None,
});
let mut prev_layer = None;
if did_reparse {
prev_layer = buffer_state.active_layer.take();
}
if buffer_state.buffer != buffer || buffer_state.excerpt_id != buffer_state.excerpt_id {
buffer_state.buffer = buffer.clone();
buffer_state.excerpt_id = excerpt_id;
buffer_state.active_layer = None;
}
let layer = match &mut buffer_state.active_layer {
Some(layer) => layer,
None => {
let snapshot = buffer.read(cx).snapshot();
let layer = if let Some(prev_layer) = prev_layer {
let prev_range = prev_layer.node().byte_range();
snapshot
.syntax_layers()
.filter(|layer| layer.language == &prev_layer.language)
.min_by_key(|layer| {
let range = layer.node().byte_range();
((range.start as i64) - (prev_range.start as i64)).abs()
+ ((range.end as i64) - (prev_range.end as i64)).abs()
})?
} else {
snapshot.syntax_layers().next()?
};
buffer_state.active_layer.insert(layer.to_owned())
}
};
// Within the active layer, find the syntax node under the cursor,
// and scroll to it.
let mut cursor = layer.node().walk();
while cursor.goto_first_child_for_byte(range.start).is_some() {
if !range.is_empty() && cursor.node().end_byte() == range.start {
cursor.goto_next_sibling();
}
}
// Ascend to the smallest ancestor that contains the range.
loop {
let node_range = cursor.node().byte_range();
if node_range.start <= range.start && node_range.end >= range.end {
break;
}
if !cursor.goto_parent() {
break;
}
}
let descendant_ix = cursor.descendant_index();
self.selected_descendant_ix = Some(descendant_ix);
self.list_state.scroll_to(ScrollTarget::Show(descendant_ix));
cx.notify();
Some(())
}
fn handle_click(&mut self, y: f32, cx: &mut ViewContext<SyntaxTreeView>) -> Option<()> {
let line_height = self.line_height?;
let ix = ((self.list_state.scroll_top() + y) / line_height) as usize;
self.update_editor_with_range_for_descendant_ix(ix, cx, |editor, mut range, cx| {
// Put the cursor at the beginning of the node.
mem::swap(&mut range.start, &mut range.end);
editor.change_selections(Some(Autoscroll::newest()), cx, |selections| {
selections.select_ranges(vec![range]);
});
});
Some(())
}
fn hover_state_changed(&mut self, cx: &mut ViewContext<SyntaxTreeView>) {
if let Some((y, line_height)) = self.mouse_y.zip(self.line_height) {
let ix = ((self.list_state.scroll_top() + y) / line_height) as usize;
if self.hovered_descendant_ix != Some(ix) {
self.hovered_descendant_ix = Some(ix);
self.update_editor_with_range_for_descendant_ix(ix, cx, |editor, range, cx| {
editor.clear_background_highlights::<Self>(cx);
editor.highlight_background::<Self>(
vec![range],
|theme| theme.editor.document_highlight_write_background,
cx,
);
});
cx.notify();
}
}
}
fn update_editor_with_range_for_descendant_ix(
&self,
descendant_ix: usize,
cx: &mut ViewContext<Self>,
mut f: impl FnMut(&mut Editor, Range<Anchor>, &mut ViewContext<Editor>),
) -> Option<()> {
let editor_state = self.editor.as_ref()?;
let buffer_state = editor_state.active_buffer.as_ref()?;
let layer = buffer_state.active_layer.as_ref()?;
// Find the node.
let mut cursor = layer.node().walk();
cursor.goto_descendant(descendant_ix);
let node = cursor.node();
let range = node.byte_range();
// Build a text anchor range.
let buffer = buffer_state.buffer.read(cx);
let range = buffer.anchor_before(range.start)..buffer.anchor_after(range.end);
// Build a multibuffer anchor range.
let multibuffer = editor_state.editor.read(cx).buffer();
let multibuffer = multibuffer.read(cx).snapshot(cx);
let excerpt_id = buffer_state.excerpt_id;
let range = multibuffer.anchor_in_excerpt(excerpt_id, range.start)
..multibuffer.anchor_in_excerpt(excerpt_id, range.end);
// Update the editor with the anchor range.
editor_state.editor.update(cx, |editor, cx| {
f(editor, range, cx);
});
Some(())
}
fn render_node(
cursor: &TreeCursor,
depth: u32,
selected: bool,
hovered: bool,
list_hovered: bool,
style: &TextStyle,
editor_theme: &theme::Editor,
cx: &AppContext,
) -> gpui::AnyElement<SyntaxTreeView> {
let node = cursor.node();
let mut range_style = style.clone();
let em_width = style.em_width(cx.font_cache());
let gutter_padding = (em_width * editor_theme.gutter_padding_factor).round();
range_style.color = editor_theme.line_number;
let mut anonymous_node_style = style.clone();
let string_color = editor_theme
.syntax
.highlights
.iter()
.find_map(|(name, style)| (name == "string").then(|| style.color)?);
let property_color = editor_theme
.syntax
.highlights
.iter()
.find_map(|(name, style)| (name == "property").then(|| style.color)?);
if let Some(color) = string_color {
anonymous_node_style.color = color;
}
let mut row = Flex::row();
if let Some(field_name) = cursor.field_name() {
let mut field_style = style.clone();
if let Some(color) = property_color {
field_style.color = color;
}
row.add_children([
Label::new(field_name, field_style),
Label::new(": ", style.clone()),
]);
}
return row
.with_child(
if node.is_named() {
Label::new(node.kind(), style.clone())
} else {
Label::new(format!("\"{}\"", node.kind()), anonymous_node_style)
}
.contained()
.with_margin_right(em_width),
)
.with_child(Label::new(format_node_range(node), range_style))
.contained()
.with_background_color(if selected {
editor_theme.selection.selection
} else if hovered && list_hovered {
editor_theme.active_line_background
} else {
Default::default()
})
.with_padding_left(gutter_padding + depth as f32 * 18.0)
.into_any();
}
}
impl Entity for SyntaxTreeView {
type Event = ();
}
impl View for SyntaxTreeView {
fn ui_name() -> &'static str {
"SyntaxTreeView"
}
fn render(&mut self, cx: &mut gpui::ViewContext<'_, '_, Self>) -> gpui::AnyElement<Self> {
let settings = settings::get::<ThemeSettings>(cx);
let font_family_id = settings.buffer_font_family;
let font_family_name = cx.font_cache().family_name(font_family_id).unwrap();
let font_properties = Default::default();
let font_id = cx
.font_cache()
.select_font(font_family_id, &font_properties)
.unwrap();
let font_size = settings.buffer_font_size(cx);
let editor_theme = settings.theme.editor.clone();
let style = TextStyle {
color: editor_theme.text_color,
font_family_name,
font_family_id,
font_id,
font_size,
font_properties: Default::default(),
underline: Default::default(),
};
let line_height = cx.font_cache().line_height(font_size);
if Some(line_height) != self.line_height {
self.line_height = Some(line_height);
self.hover_state_changed(cx);
}
if let Some(layer) = self
.editor
.as_ref()
.and_then(|editor| editor.active_buffer.as_ref())
.and_then(|buffer| buffer.active_layer.as_ref())
{
let layer = layer.clone();
let theme = editor_theme.clone();
return MouseEventHandler::<Self, Self>::new(0, cx, move |state, cx| {
let list_hovered = state.hovered();
UniformList::new(
self.list_state.clone(),
layer.node().descendant_count(),
cx,
move |this, range, items, cx| {
let mut cursor = layer.node().walk();
let mut descendant_ix = range.start as usize;
cursor.goto_descendant(descendant_ix);
let mut depth = cursor.depth();
let mut visited_children = false;
while descendant_ix < range.end {
if visited_children {
if cursor.goto_next_sibling() {
visited_children = false;
} else if cursor.goto_parent() {
depth -= 1;
} else {
break;
}
} else {
items.push(Self::render_node(
&cursor,
depth,
Some(descendant_ix) == this.selected_descendant_ix,
Some(descendant_ix) == this.hovered_descendant_ix,
list_hovered,
&style,
&theme,
cx,
));
descendant_ix += 1;
if cursor.goto_first_child() {
depth += 1;
} else {
visited_children = true;
}
}
}
},
)
})
.on_move(move |event, this, cx| {
let y = event.position.y() - event.region.origin_y();
this.mouse_y = Some(y);
this.hover_state_changed(cx);
})
.on_click(MouseButton::Left, move |event, this, cx| {
let y = event.position.y() - event.region.origin_y();
this.handle_click(y, cx);
})
.contained()
.with_background_color(editor_theme.background)
.into_any();
}
Empty::new().into_any()
}
}
impl Item for SyntaxTreeView {
fn tab_content<V: View>(
&self,
_: Option<usize>,
style: &theme::Tab,
_: &AppContext,
) -> gpui::AnyElement<V> {
Label::new("Syntax Tree", style.label.clone()).into_any()
}
fn clone_on_split(
&self,
_workspace_id: workspace::WorkspaceId,
cx: &mut ViewContext<Self>,
) -> Option<Self>
where
Self: Sized,
{
let mut clone = Self::new(self.workspace_handle.clone(), None, cx);
if let Some(editor) = &self.editor {
clone.set_editor(editor.editor.clone(), cx)
}
Some(clone)
}
}
impl SyntaxTreeToolbarItemView {
pub fn new() -> Self {
Self {
menu_open: false,
tree_view: None,
subscription: None,
}
}
fn render_menu(
&mut self,
cx: &mut ViewContext<'_, '_, Self>,
) -> Option<gpui::AnyElement<Self>> {
let theme = theme::current(cx).clone();
let tree_view = self.tree_view.as_ref()?;
let tree_view = tree_view.read(cx);
let editor_state = tree_view.editor.as_ref()?;
let buffer_state = editor_state.active_buffer.as_ref()?;
let active_layer = buffer_state.active_layer.clone()?;
let active_buffer = buffer_state.buffer.read(cx).snapshot();
enum Menu {}
Some(
Stack::new()
.with_child(Self::render_header(&theme, &active_layer, cx))
.with_children(self.menu_open.then(|| {
Overlay::new(
MouseEventHandler::<Menu, _>::new(0, cx, move |_, cx| {
Flex::column()
.with_children(active_buffer.syntax_layers().enumerate().map(
|(ix, layer)| {
Self::render_menu_item(&theme, &active_layer, layer, ix, cx)
},
))
.contained()
.with_style(theme.toolbar_dropdown_menu.container)
.constrained()
.with_width(400.)
.with_height(400.)
})
.on_down_out(MouseButton::Left, |_, this, cx| {
this.menu_open = false;
cx.notify()
}),
)
.with_hoverable(true)
.with_fit_mode(OverlayFitMode::SwitchAnchor)
.with_anchor_corner(AnchorCorner::TopLeft)
.with_z_index(999)
.aligned()
.bottom()
.left()
}))
.aligned()
.left()
.clipped()
.into_any(),
)
}
fn toggle_menu(&mut self, cx: &mut ViewContext<Self>) {
self.menu_open = !self.menu_open;
cx.notify();
}
fn select_layer(&mut self, layer_ix: usize, cx: &mut ViewContext<Self>) -> Option<()> {
let tree_view = self.tree_view.as_ref()?;
tree_view.update(cx, |view, cx| {
let editor_state = view.editor.as_mut()?;
let buffer_state = editor_state.active_buffer.as_mut()?;
let snapshot = buffer_state.buffer.read(cx).snapshot();
let layer = snapshot.syntax_layers().nth(layer_ix)?;
buffer_state.active_layer = Some(layer.to_owned());
view.selected_descendant_ix = None;
self.menu_open = false;
cx.notify();
Some(())
})
}
fn render_header(
theme: &Arc<Theme>,
active_layer: &OwnedSyntaxLayerInfo,
cx: &mut ViewContext<Self>,
) -> impl Element<Self> {
enum ToggleMenu {}
MouseEventHandler::<ToggleMenu, Self>::new(0, cx, move |state, _| {
let style = theme.toolbar_dropdown_menu.header.style_for(state, false);
Flex::row()
.with_child(
Label::new(active_layer.language.name().to_string(), style.text.clone())
.contained()
.with_margin_right(style.secondary_text_spacing),
)
.with_child(Label::new(
format_node_range(active_layer.node()),
style
.secondary_text
.clone()
.unwrap_or_else(|| style.text.clone()),
))
.contained()
.with_style(style.container)
})
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, move |_, view, cx| {
view.toggle_menu(cx);
})
}
fn render_menu_item(
theme: &Arc<Theme>,
active_layer: &OwnedSyntaxLayerInfo,
layer: SyntaxLayerInfo,
layer_ix: usize,
cx: &mut ViewContext<Self>,
) -> impl Element<Self> {
enum ActivateLayer {}
MouseEventHandler::<ActivateLayer, _>::new(layer_ix, cx, move |state, _| {
let is_selected = layer.node() == active_layer.node();
let style = theme
.toolbar_dropdown_menu
.item
.style_for(state, is_selected);
Flex::row()
.with_child(
Label::new(layer.language.name().to_string(), style.text.clone())
.contained()
.with_margin_right(style.secondary_text_spacing),
)
.with_child(Label::new(
format_node_range(layer.node()),
style
.secondary_text
.clone()
.unwrap_or_else(|| style.text.clone()),
))
.contained()
.with_style(style.container)
})
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, move |_, view, cx| {
view.select_layer(layer_ix, cx);
})
}
}
fn format_node_range(node: Node) -> String {
let start = node.start_position();
let end = node.end_position();
format!(
"[{}:{} - {}:{}]",
start.row + 1,
start.column + 1,
end.row + 1,
end.column + 1,
)
}
impl Entity for SyntaxTreeToolbarItemView {
type Event = ();
}
impl View for SyntaxTreeToolbarItemView {
fn ui_name() -> &'static str {
"SyntaxTreeToolbarItemView"
}
fn render(&mut self, cx: &mut ViewContext<'_, '_, Self>) -> gpui::AnyElement<Self> {
self.render_menu(cx)
.unwrap_or_else(|| Empty::new().into_any())
}
}
impl ToolbarItemView for SyntaxTreeToolbarItemView {
fn set_active_pane_item(
&mut self,
active_pane_item: Option<&dyn ItemHandle>,
cx: &mut ViewContext<Self>,
) -> workspace::ToolbarItemLocation {
self.menu_open = false;
if let Some(item) = active_pane_item {
if let Some(view) = item.downcast::<SyntaxTreeView>() {
self.tree_view = Some(view.clone());
self.subscription = Some(cx.observe(&view, |_, _, cx| cx.notify()));
return ToolbarItemLocation::PrimaryLeft {
flex: Some((1., false)),
};
}
}
self.tree_view = None;
self.subscription = None;
ToolbarItemLocation::Hidden
}
}

View File

@ -7,6 +7,8 @@ pub mod worktree;
#[cfg(test)]
mod project_tests;
#[cfg(test)]
mod worktree_tests;
use anyhow::{anyhow, Context, Result};
use client::{proto, Client, TypedEnvelope, UserStore};

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -31,7 +31,7 @@ serde_derive.workspace = true
serde_json.workspace = true
smallvec.workspace = true
toml.workspace = true
tree-sitter = "*"
tree-sitter.workspace = true
tree-sitter-json = "*"
[dev-dependencies]

View File

@ -86,9 +86,9 @@ pub struct SettingsJsonSchemaParams<'a> {
/// A set of strongly-typed setting values defined via multiple JSON files.
pub struct SettingsStore {
setting_values: HashMap<TypeId, Box<dyn AnySettingValue>>,
default_deserialized_settings: serde_json::Value,
user_deserialized_settings: serde_json::Value,
local_deserialized_settings: BTreeMap<(usize, Arc<Path>), serde_json::Value>,
raw_default_settings: serde_json::Value,
raw_user_settings: serde_json::Value,
raw_local_settings: BTreeMap<(usize, Arc<Path>), serde_json::Value>,
tab_size_callback: Option<(TypeId, Box<dyn Fn(&dyn Any) -> Option<usize>>)>,
}
@ -96,9 +96,9 @@ impl Default for SettingsStore {
fn default() -> Self {
SettingsStore {
setting_values: Default::default(),
default_deserialized_settings: serde_json::json!({}),
user_deserialized_settings: serde_json::json!({}),
local_deserialized_settings: Default::default(),
raw_default_settings: serde_json::json!({}),
raw_user_settings: serde_json::json!({}),
raw_local_settings: Default::default(),
tab_size_callback: Default::default(),
}
}
@ -148,13 +148,13 @@ impl SettingsStore {
}));
if let Some(default_settings) = setting_value
.deserialize_setting(&self.default_deserialized_settings)
.deserialize_setting(&self.raw_default_settings)
.log_err()
{
let mut user_values_stack = Vec::new();
if let Some(user_settings) = setting_value
.deserialize_setting(&self.user_deserialized_settings)
.deserialize_setting(&self.raw_user_settings)
.log_err()
{
user_values_stack = vec![user_settings];
@ -196,8 +196,8 @@ impl SettingsStore {
///
/// This is only for debugging and reporting. For user-facing functionality,
/// use the typed setting interface.
pub fn untyped_user_settings(&self) -> &serde_json::Value {
&self.user_deserialized_settings
pub fn raw_user_settings(&self) -> &serde_json::Value {
&self.raw_user_settings
}
#[cfg(any(test, feature = "test-support"))]
@ -219,7 +219,7 @@ impl SettingsStore {
cx: &AppContext,
update: impl FnOnce(&mut T::FileContent),
) {
let old_text = serde_json::to_string(&self.user_deserialized_settings).unwrap();
let old_text = serde_json::to_string(&self.raw_user_settings).unwrap();
let new_text = self.new_text_for_update::<T>(old_text, update);
self.set_user_settings(&new_text, cx).unwrap();
}
@ -248,25 +248,19 @@ impl SettingsStore {
) -> Vec<(Range<usize>, String)> {
let setting_type_id = TypeId::of::<T>();
let old_content = self
let setting = self
.setting_values
.get(&setting_type_id)
.unwrap_or_else(|| panic!("unregistered setting type {}", type_name::<T>()))
.deserialize_setting(&self.user_deserialized_settings)
.unwrap_or_else(|e| {
panic!(
"could not deserialize setting type {} from user settings: {}",
type_name::<T>(),
e
)
})
.0
.downcast::<T::FileContent>()
.unwrap();
.unwrap_or_else(|| panic!("unregistered setting type {}", type_name::<T>()));
let raw_settings = parse_json_with_comments::<serde_json::Value>(text).unwrap_or_default();
let old_content = match setting.deserialize_setting(&raw_settings) {
Ok(content) => content.0.downcast::<T::FileContent>().unwrap(),
Err(_) => Box::new(T::FileContent::default()),
};
let mut new_content = old_content.clone();
update(&mut new_content);
let old_value = &serde_json::to_value(&old_content).unwrap();
let old_value = serde_json::to_value(&old_content).unwrap();
let new_value = serde_json::to_value(new_content).unwrap();
let mut key_path = Vec::new();
@ -323,7 +317,7 @@ impl SettingsStore {
) -> Result<()> {
let settings: serde_json::Value = parse_json_with_comments(default_settings_content)?;
if settings.is_object() {
self.default_deserialized_settings = settings;
self.raw_default_settings = settings;
self.recompute_values(None, cx)?;
Ok(())
} else {
@ -339,7 +333,7 @@ impl SettingsStore {
) -> Result<()> {
let settings: serde_json::Value = parse_json_with_comments(user_settings_content)?;
if settings.is_object() {
self.user_deserialized_settings = settings;
self.raw_user_settings = settings;
self.recompute_values(None, cx)?;
Ok(())
} else {
@ -356,11 +350,10 @@ impl SettingsStore {
cx: &AppContext,
) -> Result<()> {
if let Some(content) = settings_content {
self.local_deserialized_settings
self.raw_local_settings
.insert((root_id, path.clone()), parse_json_with_comments(content)?);
} else {
self.local_deserialized_settings
.remove(&(root_id, path.clone()));
self.raw_local_settings.remove(&(root_id, path.clone()));
}
self.recompute_values(Some((root_id, &path)), cx)?;
Ok(())
@ -368,14 +361,13 @@ impl SettingsStore {
/// Add or remove a set of local settings via a JSON string.
pub fn clear_local_settings(&mut self, root_id: usize, cx: &AppContext) -> Result<()> {
self.local_deserialized_settings
.retain(|k, _| k.0 != root_id);
self.raw_local_settings.retain(|k, _| k.0 != root_id);
self.recompute_values(Some((root_id, "".as_ref())), cx)?;
Ok(())
}
pub fn local_settings(&self, root_id: usize) -> impl '_ + Iterator<Item = (Arc<Path>, String)> {
self.local_deserialized_settings
self.raw_local_settings
.range((root_id, Path::new("").into())..(root_id + 1, Path::new("").into()))
.map(|((_, path), content)| (path.clone(), serde_json::to_string(content).unwrap()))
}
@ -466,14 +458,13 @@ impl SettingsStore {
let mut user_settings_stack = Vec::<DeserializedSetting>::new();
let mut paths_stack = Vec::<Option<(usize, &Path)>>::new();
for setting_value in self.setting_values.values_mut() {
let default_settings =
setting_value.deserialize_setting(&self.default_deserialized_settings)?;
let default_settings = setting_value.deserialize_setting(&self.raw_default_settings)?;
user_settings_stack.clear();
paths_stack.clear();
if let Some(user_settings) = setting_value
.deserialize_setting(&self.user_deserialized_settings)
.deserialize_setting(&self.raw_user_settings)
.log_err()
{
user_settings_stack.push(user_settings);
@ -491,7 +482,7 @@ impl SettingsStore {
}
// Reload the local values for the setting.
for ((root_id, path), local_settings) in &self.local_deserialized_settings {
for ((root_id, path), local_settings) in &self.raw_local_settings {
// Build a stack of all of the local values for that setting.
while let Some(prev_entry) = paths_stack.last() {
if let Some((prev_root_id, prev_path)) = prev_entry {
@ -542,9 +533,9 @@ impl Debug for SettingsStore {
.map(|value| value.setting_type_name())
.collect::<Vec<_>>(),
)
.field("default_settings", &self.default_deserialized_settings)
.field("user_settings", &self.user_deserialized_settings)
.field("local_settings", &self.local_deserialized_settings)
.field("default_settings", &self.raw_default_settings)
.field("user_settings", &self.raw_user_settings)
.field("local_settings", &self.raw_local_settings)
.finish_non_exhaustive()
}
}

View File

@ -44,7 +44,7 @@ pub struct Theme {
pub context_menu: ContextMenu,
pub contacts_popover: ContactsPopover,
pub contact_list: ContactList,
pub lsp_log_menu: LspLogMenu,
pub toolbar_dropdown_menu: DropdownMenu,
pub copilot: Copilot,
pub contact_finder: ContactFinder,
pub project_panel: ProjectPanel,
@ -246,15 +246,26 @@ pub struct ContactFinder {
}
#[derive(Deserialize, Default)]
pub struct LspLogMenu {
pub struct DropdownMenu {
#[serde(flatten)]
pub container: ContainerStyle,
pub header: Interactive<ContainedText>,
pub server: ContainedText,
pub item: Interactive<ContainedText>,
pub header: Interactive<DropdownMenuItem>,
pub section_header: ContainedText,
pub item: Interactive<DropdownMenuItem>,
pub row_height: f32,
}
#[derive(Deserialize, Default)]
pub struct DropdownMenuItem {
#[serde(flatten)]
pub container: ContainerStyle,
#[serde(flatten)]
pub text: TextStyle,
pub secondary_text: Option<TextStyle>,
#[serde(default)]
pub secondary_text_spacing: f32,
}
#[derive(Clone, Deserialize, Default)]
pub struct TabBar {
#[serde(flatten)]

View File

@ -45,11 +45,11 @@ journal = { path = "../journal" }
language = { path = "../language" }
language_selector = { path = "../language_selector" }
lsp = { path = "../lsp" }
lsp_log = { path = "../lsp_log" }
language_tools = { path = "../language_tools" }
node_runtime = { path = "../node_runtime" }
ai = { path = "../ai" }
outline = { path = "../outline" }
plugin_runtime = { path = "../plugin_runtime" }
plugin_runtime = { path = "../plugin_runtime",optional = true }
project = { path = "../project" }
project_panel = { path = "../project_panel" }
project_symbols = { path = "../project_symbols" }
@ -102,13 +102,14 @@ tempdir.workspace = true
thiserror.workspace = true
tiny_http = "0.8"
toml.workspace = true
tree-sitter = "0.20"
tree-sitter.workspace = true
tree-sitter-c = "0.20.1"
tree-sitter-cpp = "0.20.0"
tree-sitter-css = { git = "https://github.com/tree-sitter/tree-sitter-css", rev = "769203d0f9abe1a9a691ac2b9fe4bb4397a73c51" }
tree-sitter-elixir = { git = "https://github.com/elixir-lang/tree-sitter-elixir", rev = "4ba9dab6e2602960d95b2b625f3386c27e08084e" }
tree-sitter-embedded-template = "0.20.0"
tree-sitter-go = { git = "https://github.com/tree-sitter/tree-sitter-go", rev = "aeb2f33b366fd78d5789ff104956ce23508b85db" }
tree-sitter-heex = { git = "https://github.com/phoenixframework/tree-sitter-heex", rev = "2e1348c3cf2c9323e87c2744796cf3f3868aa82a" }
tree-sitter-json = { git = "https://github.com/tree-sitter/tree-sitter-json", rev = "40a81c01a40ac48744e0c8ccabbaba1920441199" }
tree-sitter-rust = "0.20.3"
tree-sitter-markdown = { git = "https://github.com/MDeiml/tree-sitter-markdown", rev = "330ecab87a3e3a7211ac69bbadc19eabecdb1cca" }

View File

@ -10,6 +10,7 @@ mod elixir;
mod go;
mod html;
mod json;
#[cfg(feature = "plugin_runtime")]
mod language_plugin;
mod lua;
mod python;
@ -33,110 +34,109 @@ mod yaml;
struct LanguageDir;
pub fn init(languages: Arc<LanguageRegistry>, node_runtime: Arc<NodeRuntime>) {
fn adapter_arc(adapter: impl LspAdapter) -> Arc<dyn LspAdapter> {
Arc::new(adapter)
}
let language = |name, grammar, adapters| {
languages.register(name, load_config(name), grammar, adapters, load_queries)
};
let languages_list = [
(
"c",
tree_sitter_c::language(),
vec![adapter_arc(c::CLspAdapter)],
),
(
"cpp",
tree_sitter_cpp::language(),
vec![adapter_arc(c::CLspAdapter)],
),
("css", tree_sitter_css::language(), vec![]),
(
"elixir",
tree_sitter_elixir::language(),
vec![adapter_arc(elixir::ElixirLspAdapter)],
),
(
"go",
tree_sitter_go::language(),
vec![adapter_arc(go::GoLspAdapter)],
),
(
"json",
tree_sitter_json::language(),
vec![adapter_arc(json::JsonLspAdapter::new(
node_runtime.clone(),
languages.clone(),
))],
),
("markdown", tree_sitter_markdown::language(), vec![]),
(
"python",
tree_sitter_python::language(),
vec![adapter_arc(python::PythonLspAdapter::new(
node_runtime.clone(),
))],
),
(
"rust",
tree_sitter_rust::language(),
vec![adapter_arc(rust::RustLspAdapter)],
),
("toml", tree_sitter_toml::language(), vec![]),
(
"tsx",
tree_sitter_typescript::language_tsx(),
vec![
adapter_arc(typescript::TypeScriptLspAdapter::new(node_runtime.clone())),
adapter_arc(typescript::EsLintLspAdapter::new(node_runtime.clone())),
],
),
(
"typescript",
tree_sitter_typescript::language_typescript(),
vec![
adapter_arc(typescript::TypeScriptLspAdapter::new(node_runtime.clone())),
adapter_arc(typescript::EsLintLspAdapter::new(node_runtime.clone())),
],
),
(
"javascript",
tree_sitter_typescript::language_tsx(),
vec![
adapter_arc(typescript::TypeScriptLspAdapter::new(node_runtime.clone())),
adapter_arc(typescript::EsLintLspAdapter::new(node_runtime.clone())),
],
),
(
"html",
tree_sitter_html::language(),
vec![adapter_arc(html::HtmlLspAdapter::new(node_runtime.clone()))],
),
(
"ruby",
tree_sitter_ruby::language(),
vec![adapter_arc(ruby::RubyLanguageServer)],
),
(
"erb",
tree_sitter_embedded_template::language(),
vec![adapter_arc(ruby::RubyLanguageServer)],
),
("scheme", tree_sitter_scheme::language(), vec![]),
("racket", tree_sitter_racket::language(), vec![]),
(
"lua",
tree_sitter_lua::language(),
vec![adapter_arc(lua::LuaLspAdapter)],
),
(
"yaml",
tree_sitter_yaml::language(),
vec![adapter_arc(yaml::YamlLspAdapter::new(node_runtime))],
),
];
for (name, grammar, lsp_adapters) in languages_list {
languages.register(name, load_config(name), grammar, lsp_adapters, load_queries);
}
language(
"c",
tree_sitter_c::language(),
vec![Arc::new(c::CLspAdapter) as Arc<dyn LspAdapter>],
);
language(
"cpp",
tree_sitter_cpp::language(),
vec![Arc::new(c::CLspAdapter)],
);
language("css", tree_sitter_css::language(), vec![]);
language(
"elixir",
tree_sitter_elixir::language(),
vec![Arc::new(elixir::ElixirLspAdapter)],
);
language(
"go",
tree_sitter_go::language(),
vec![Arc::new(go::GoLspAdapter)],
);
language(
"heex",
tree_sitter_heex::language(),
vec![Arc::new(elixir::ElixirLspAdapter)],
);
language(
"json",
tree_sitter_json::language(),
vec![Arc::new(json::JsonLspAdapter::new(
node_runtime.clone(),
languages.clone(),
))],
);
language("markdown", tree_sitter_markdown::language(), vec![]);
language(
"python",
tree_sitter_python::language(),
vec![Arc::new(python::PythonLspAdapter::new(
node_runtime.clone(),
))],
);
language(
"rust",
tree_sitter_rust::language(),
vec![Arc::new(rust::RustLspAdapter)],
);
language("toml", tree_sitter_toml::language(), vec![]);
language(
"tsx",
tree_sitter_typescript::language_tsx(),
vec![
Arc::new(typescript::TypeScriptLspAdapter::new(node_runtime.clone())),
Arc::new(typescript::EsLintLspAdapter::new(node_runtime.clone())),
],
);
language(
"typescript",
tree_sitter_typescript::language_typescript(),
vec![
Arc::new(typescript::TypeScriptLspAdapter::new(node_runtime.clone())),
Arc::new(typescript::EsLintLspAdapter::new(node_runtime.clone())),
],
);
language(
"javascript",
tree_sitter_typescript::language_tsx(),
vec![
Arc::new(typescript::TypeScriptLspAdapter::new(node_runtime.clone())),
Arc::new(typescript::EsLintLspAdapter::new(node_runtime.clone())),
],
);
language(
"html",
tree_sitter_html::language(),
vec![Arc::new(html::HtmlLspAdapter::new(node_runtime.clone()))],
);
language(
"ruby",
tree_sitter_ruby::language(),
vec![Arc::new(ruby::RubyLanguageServer)],
);
language(
"erb",
tree_sitter_embedded_template::language(),
vec![Arc::new(ruby::RubyLanguageServer)],
);
language("scheme", tree_sitter_scheme::language(), vec![]);
language("racket", tree_sitter_racket::language(), vec![]);
language(
"lua",
tree_sitter_lua::language(),
vec![Arc::new(lua::LuaLspAdapter)],
);
language(
"yaml",
tree_sitter_yaml::language(),
vec![Arc::new(yaml::YamlLspAdapter::new(node_runtime))],
);
}
#[cfg(any(test, feature = "test-support"))]

View File

@ -0,0 +1,7 @@
; Phoenix HTML template
((sigil
(sigil_name) @_sigil_name
(quoted_content) @content)
(#eq? @_sigil_name "H")
(#set! language "heex"))

View File

@ -9,4 +9,4 @@
"%>"
"-%>"
"_%>"
] @keyword
] @keyword

View File

@ -0,0 +1,7 @@
name = "HEEX"
path_suffixes = ["heex"]
autoclose_before = ">})"
brackets = [
{ start = "<", end = ">", close = true, newline = true },
]
block_comment = ["<%#", "%>"]

View File

@ -0,0 +1,54 @@
; HEEx delimiters
[
"%>"
"--%>"
"-->"
"/>"
"<!"
"<!--"
"<"
"<%!--"
"<%"
"<%#"
"<%%="
"<%="
"</"
"</:"
"<:"
">"
"{"
"}"
] @punctuation.bracket
; HEEx operators are highlighted as such
"=" @operator
; HEEx inherits the DOCTYPE tag from HTML
(doctype) @constant
(comment) @comment
; HEEx tags and slots are highlighted as HTML
[
(tag_name)
(slot_name)
] @tag
; HEEx attributes are highlighted as HTML attributes
(attribute_name) @attribute
; HEEx special attributes are highlighted as keywords
(special_attribute_name) @keyword
[
(attribute_value)
(quoted_attribute_value)
] @string
; HEEx components are highlighted as Elixir modules and functions
(component_name
[
(module) @module
(function) @function
"." @punctuation.delimiter
])

View File

@ -0,0 +1,13 @@
((directive (partial_expression_value) @content)
(#set! language "elixir")
(#set! include-children)
(#set! combined))
; Regular expression_values do not need to be combined
((directive (expression_value) @content)
(#set! language "elixir"))
; expressions live within HTML tags, and do not need to be combined
; <link href={ Routes.static_path(..) } />
((expression (expression_value) @content)
(#set! language "elixir"))

View File

@ -191,7 +191,7 @@ fn main() {
language_selector::init(cx);
theme_selector::init(cx);
activity_indicator::init(cx);
lsp_log::init(cx);
language_tools::init(cx);
call::init(app_state.client.clone(), app_state.user_store.clone(), cx);
collab_ui::init(&app_state, cx);
feedback::init(cx);

View File

@ -312,8 +312,11 @@ pub fn initialize_workspace(
let feedback_info_text = cx.add_view(|_| FeedbackInfoText::new());
toolbar.add_item(feedback_info_text, cx);
let lsp_log_item =
cx.add_view(|_| lsp_log::LspLogToolbarItemView::new());
cx.add_view(|_| language_tools::LspLogToolbarItemView::new());
toolbar.add_item(lsp_log_item, cx);
let syntax_tree_item = cx
.add_view(|_| language_tools::SyntaxTreeToolbarItemView::new());
toolbar.add_item(syntax_tree_item, cx);
})
});
}

View File

@ -17,7 +17,7 @@ import projectSharedNotification from "./projectSharedNotification"
import tooltip from "./tooltip"
import terminal from "./terminal"
import contactList from "./contactList"
import lspLogMenu from "./lspLogMenu"
import toolbarDropdownMenu from "./toolbarDropdownMenu"
import incomingCallNotification from "./incomingCallNotification"
import { ColorScheme } from "../theme/colorScheme"
import feedback from "./feedback"
@ -46,7 +46,7 @@ export default function app(colorScheme: ColorScheme): Object {
contactsPopover: contactsPopover(colorScheme),
contactFinder: contactFinder(colorScheme),
contactList: contactList(colorScheme),
lspLogMenu: lspLogMenu(colorScheme),
toolbarDropdownMenu: toolbarDropdownMenu(colorScheme),
search: search(colorScheme),
sharedScreen: sharedScreen(colorScheme),
updateNotification: updateNotification(colorScheme),

View File

@ -1,7 +1,7 @@
import { ColorScheme } from "../theme/colorScheme"
import { background, border, text } from "./components"
export default function contactsPanel(colorScheme: ColorScheme) {
export default function dropdownMenu(colorScheme: ColorScheme) {
let layer = colorScheme.middle
return {
@ -11,6 +11,8 @@ export default function contactsPanel(colorScheme: ColorScheme) {
shadow: colorScheme.popoverShadow,
header: {
...text(layer, "sans", { size: "sm" }),
secondaryText: text(layer, "sans", { size: "sm", color: "#aaaaaa" }),
secondaryTextSpacing: 10,
padding: { left: 8, right: 8, top: 2, bottom: 2 },
cornerRadius: 6,
background: background(layer, "on"),
@ -20,12 +22,14 @@ export default function contactsPanel(colorScheme: ColorScheme) {
...text(layer, "sans", "hovered", { size: "sm" }),
}
},
server: {
sectionHeader: {
...text(layer, "sans", { size: "sm" }),
padding: { left: 8, right: 8, top: 8, bottom: 8 },
},
item: {
...text(layer, "sans", { size: "sm" }),
secondaryTextSpacing: 10,
secondaryText: text(layer, "sans", { size: "sm" }),
padding: { left: 18, right: 18, top: 2, bottom: 2 },
hover: {
background: background(layer, "hovered"),