mirror of
https://github.com/zed-industries/zed.git
synced 2024-11-07 20:39:04 +03:00
Merge branch 'main' into notifications
This commit is contained in:
commit
e590b43545
14
Cargo.lock
generated
14
Cargo.lock
generated
@ -2407,7 +2407,6 @@ dependencies = [
|
||||
"parking_lot 0.11.2",
|
||||
"postage",
|
||||
"project",
|
||||
"pulldown-cmark",
|
||||
"rand 0.8.5",
|
||||
"rich_text",
|
||||
"rpc",
|
||||
@ -3992,6 +3991,7 @@ dependencies = [
|
||||
"lsp",
|
||||
"parking_lot 0.11.2",
|
||||
"postage",
|
||||
"pulldown-cmark",
|
||||
"rand 0.8.5",
|
||||
"regex",
|
||||
"rpc",
|
||||
@ -6987,7 +6987,6 @@ dependencies = [
|
||||
"unindent",
|
||||
"util",
|
||||
"workspace",
|
||||
"zed",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -8840,6 +8839,15 @@ dependencies = [
|
||||
"tree-sitter",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tree-sitter-vue"
|
||||
version = "0.0.1"
|
||||
source = "git+https://github.com/zed-industries/tree-sitter-vue?rev=95b2890#95b28908d90e928c308866f7631e73ef6e1d4b5f"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"tree-sitter",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tree-sitter-yaml"
|
||||
version = "0.0.1"
|
||||
@ -10096,6 +10104,7 @@ name = "zed"
|
||||
version = "0.109.0"
|
||||
dependencies = [
|
||||
"activity_indicator",
|
||||
"ai",
|
||||
"anyhow",
|
||||
"assistant",
|
||||
"async-compression",
|
||||
@ -10209,6 +10218,7 @@ dependencies = [
|
||||
"tree-sitter-svelte",
|
||||
"tree-sitter-toml",
|
||||
"tree-sitter-typescript",
|
||||
"tree-sitter-vue",
|
||||
"tree-sitter-yaml",
|
||||
"unindent",
|
||||
"url",
|
||||
|
@ -151,7 +151,7 @@ tree-sitter-yaml = { git = "https://github.com/zed-industries/tree-sitter-yaml",
|
||||
tree-sitter-lua = "0.0.14"
|
||||
tree-sitter-nix = { git = "https://github.com/nix-community/tree-sitter-nix", rev = "66e3e9ce9180ae08fc57372061006ef83f0abde7" }
|
||||
tree-sitter-nu = { git = "https://github.com/nushell/tree-sitter-nu", rev = "786689b0562b9799ce53e824cb45a1a2a04dc673"}
|
||||
|
||||
tree-sitter-vue = {git = "https://github.com/zed-industries/tree-sitter-vue", rev = "95b2890"}
|
||||
[patch.crates-io]
|
||||
tree-sitter = { git = "https://github.com/tree-sitter/tree-sitter", rev = "35a6052fbcafc5e5fc0f9415b8652be7dcaf7222" }
|
||||
async-task = { git = "https://github.com/zed-industries/async-task", rev = "341b57d6de98cdfd7b418567b8de2022ca993a6e" }
|
||||
|
@ -50,6 +50,9 @@
|
||||
// Whether to pop the completions menu while typing in an editor without
|
||||
// explicitly requesting it.
|
||||
"show_completions_on_input": true,
|
||||
// Whether to display inline and alongside documentation for items in the
|
||||
// completions menu
|
||||
"show_completion_documentation": true,
|
||||
// Whether to show wrap guides in the editor. Setting this to true will
|
||||
// show a guide at the 'preferred_line_length' value if softwrap is set to
|
||||
// 'preferred_line_length', and will show any additional guides as specified
|
||||
|
@ -226,6 +226,7 @@ impl Server {
|
||||
.add_request_handler(forward_project_request::<proto::OpenBufferByPath>)
|
||||
.add_request_handler(forward_project_request::<proto::GetCompletions>)
|
||||
.add_request_handler(forward_project_request::<proto::ApplyCompletionAdditionalEdits>)
|
||||
.add_request_handler(forward_project_request::<proto::ResolveCompletionDocumentation>)
|
||||
.add_request_handler(forward_project_request::<proto::GetCodeActions>)
|
||||
.add_request_handler(forward_project_request::<proto::ApplyCodeAction>)
|
||||
.add_request_handler(forward_project_request::<proto::PrepareRename>)
|
||||
|
@ -38,6 +38,10 @@ impl DiagnosticIndicator {
|
||||
this.in_progress_checks.remove(language_server_id);
|
||||
cx.notify();
|
||||
}
|
||||
project::Event::DiagnosticsUpdated { .. } => {
|
||||
this.summary = project.read(cx).diagnostic_summary(cx);
|
||||
cx.notify();
|
||||
}
|
||||
_ => {}
|
||||
})
|
||||
.detach();
|
||||
|
@ -57,7 +57,6 @@ log.workspace = true
|
||||
ordered-float.workspace = true
|
||||
parking_lot.workspace = true
|
||||
postage.workspace = true
|
||||
pulldown-cmark = { version = "0.9.2", default-features = false }
|
||||
rand.workspace = true
|
||||
schemars.workspace = true
|
||||
serde.workspace = true
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -7,6 +7,7 @@ pub struct EditorSettings {
|
||||
pub cursor_blink: bool,
|
||||
pub hover_popover_enabled: bool,
|
||||
pub show_completions_on_input: bool,
|
||||
pub show_completion_documentation: bool,
|
||||
pub use_on_type_format: bool,
|
||||
pub scrollbar: Scrollbar,
|
||||
pub relative_line_numbers: bool,
|
||||
@ -33,6 +34,7 @@ pub struct EditorSettingsContent {
|
||||
pub cursor_blink: Option<bool>,
|
||||
pub hover_popover_enabled: Option<bool>,
|
||||
pub show_completions_on_input: Option<bool>,
|
||||
pub show_completion_documentation: Option<bool>,
|
||||
pub use_on_type_format: Option<bool>,
|
||||
pub scrollbar: Option<ScrollbarContent>,
|
||||
pub relative_line_numbers: Option<bool>,
|
||||
|
@ -5430,9 +5430,9 @@ async fn test_completion(cx: &mut gpui::TestAppContext) {
|
||||
additional edit
|
||||
"});
|
||||
cx.simulate_keystroke(" ");
|
||||
assert!(cx.editor(|e, _| e.context_menu.is_none()));
|
||||
assert!(cx.editor(|e, _| e.context_menu.read().is_none()));
|
||||
cx.simulate_keystroke("s");
|
||||
assert!(cx.editor(|e, _| e.context_menu.is_none()));
|
||||
assert!(cx.editor(|e, _| e.context_menu.read().is_none()));
|
||||
|
||||
cx.assert_editor_state(indoc! {"
|
||||
one.second_completion
|
||||
@ -5494,12 +5494,12 @@ async fn test_completion(cx: &mut gpui::TestAppContext) {
|
||||
});
|
||||
cx.set_state("editorˇ");
|
||||
cx.simulate_keystroke(".");
|
||||
assert!(cx.editor(|e, _| e.context_menu.is_none()));
|
||||
assert!(cx.editor(|e, _| e.context_menu.read().is_none()));
|
||||
cx.simulate_keystroke("c");
|
||||
cx.simulate_keystroke("l");
|
||||
cx.simulate_keystroke("o");
|
||||
cx.assert_editor_state("editor.cloˇ");
|
||||
assert!(cx.editor(|e, _| e.context_menu.is_none()));
|
||||
assert!(cx.editor(|e, _| e.context_menu.read().is_none()));
|
||||
cx.update_editor(|editor, cx| {
|
||||
editor.show_completions(&ShowCompletions, cx);
|
||||
});
|
||||
@ -7788,7 +7788,7 @@ async fn test_completions_in_languages_with_extra_word_characters(cx: &mut gpui:
|
||||
cx.simulate_keystroke("-");
|
||||
cx.foreground().run_until_parked();
|
||||
cx.update_editor(|editor, _| {
|
||||
if let Some(ContextMenu::Completions(menu)) = &editor.context_menu {
|
||||
if let Some(ContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() {
|
||||
assert_eq!(
|
||||
menu.matches.iter().map(|m| &m.string).collect::<Vec<_>>(),
|
||||
&["bg-red", "bg-blue", "bg-yellow"]
|
||||
@ -7801,7 +7801,7 @@ async fn test_completions_in_languages_with_extra_word_characters(cx: &mut gpui:
|
||||
cx.simulate_keystroke("l");
|
||||
cx.foreground().run_until_parked();
|
||||
cx.update_editor(|editor, _| {
|
||||
if let Some(ContextMenu::Completions(menu)) = &editor.context_menu {
|
||||
if let Some(ContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() {
|
||||
assert_eq!(
|
||||
menu.matches.iter().map(|m| &m.string).collect::<Vec<_>>(),
|
||||
&["bg-blue", "bg-yellow"]
|
||||
@ -7817,7 +7817,7 @@ async fn test_completions_in_languages_with_extra_word_characters(cx: &mut gpui:
|
||||
cx.simulate_keystroke("l");
|
||||
cx.foreground().run_until_parked();
|
||||
cx.update_editor(|editor, _| {
|
||||
if let Some(ContextMenu::Completions(menu)) = &editor.context_menu {
|
||||
if let Some(ContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() {
|
||||
assert_eq!(
|
||||
menu.matches.iter().map(|m| &m.string).collect::<Vec<_>>(),
|
||||
&["bg-yellow"]
|
||||
|
@ -2428,7 +2428,7 @@ impl Element<Editor> for EditorElement {
|
||||
}
|
||||
|
||||
let active = matches!(
|
||||
editor.context_menu,
|
||||
editor.context_menu.read().as_ref(),
|
||||
Some(crate::ContextMenu::CodeActions(_))
|
||||
);
|
||||
|
||||
@ -2439,9 +2439,13 @@ impl Element<Editor> for EditorElement {
|
||||
}
|
||||
|
||||
let visible_rows = start_row..start_row + line_layouts.len() as u32;
|
||||
let mut hover = editor
|
||||
.hover_state
|
||||
.render(&snapshot, &style, visible_rows, cx);
|
||||
let mut hover = editor.hover_state.render(
|
||||
&snapshot,
|
||||
&style,
|
||||
visible_rows,
|
||||
editor.workspace.as_ref().map(|(w, _)| w.clone()),
|
||||
cx,
|
||||
);
|
||||
let mode = editor.mode;
|
||||
|
||||
let mut fold_indicators = editor.render_fold_indicators(
|
||||
|
@ -9,13 +9,15 @@ use gpui::{
|
||||
actions,
|
||||
elements::{Flex, MouseEventHandler, Padding, ParentElement, Text},
|
||||
platform::{CursorStyle, MouseButton},
|
||||
AnyElement, AppContext, Element, ModelHandle, Task, ViewContext,
|
||||
AnyElement, AppContext, Element, ModelHandle, Task, ViewContext, WeakViewHandle,
|
||||
};
|
||||
use language::{
|
||||
markdown, Bias, DiagnosticEntry, DiagnosticSeverity, Language, LanguageRegistry, ParsedMarkdown,
|
||||
};
|
||||
use language::{Bias, DiagnosticEntry, DiagnosticSeverity, Language, LanguageRegistry};
|
||||
use project::{HoverBlock, HoverBlockKind, InlayHintLabelPart, Project};
|
||||
use rich_text::{new_paragraph, render_code, render_markdown_mut, RichText};
|
||||
use std::{ops::Range, sync::Arc, time::Duration};
|
||||
use util::TryFutureExt;
|
||||
use workspace::Workspace;
|
||||
|
||||
pub const HOVER_DELAY_MILLIS: u64 = 350;
|
||||
pub const HOVER_REQUEST_DELAY_MILLIS: u64 = 200;
|
||||
@ -105,12 +107,15 @@ pub fn hover_at_inlay(editor: &mut Editor, inlay_hover: InlayHover, cx: &mut Vie
|
||||
this.hover_state.diagnostic_popover = None;
|
||||
})?;
|
||||
|
||||
let language_registry = project.update(&mut cx, |p, _| p.languages().clone());
|
||||
let blocks = vec![inlay_hover.tooltip];
|
||||
let parsed_content = parse_blocks(&blocks, &language_registry, None).await;
|
||||
|
||||
let hover_popover = InfoPopover {
|
||||
project: project.clone(),
|
||||
symbol_range: RangeInEditor::Inlay(inlay_hover.range.clone()),
|
||||
blocks: vec![inlay_hover.tooltip],
|
||||
language: None,
|
||||
rendered_content: None,
|
||||
blocks,
|
||||
parsed_content,
|
||||
};
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
@ -288,35 +293,38 @@ fn show_hover(
|
||||
});
|
||||
})?;
|
||||
|
||||
// Construct new hover popover from hover request
|
||||
let hover_popover = hover_request.await.ok().flatten().and_then(|hover_result| {
|
||||
if hover_result.is_empty() {
|
||||
return None;
|
||||
let hover_result = hover_request.await.ok().flatten();
|
||||
let hover_popover = match hover_result {
|
||||
Some(hover_result) if !hover_result.is_empty() => {
|
||||
// Create symbol range of anchors for highlighting and filtering of future requests.
|
||||
let range = if let Some(range) = hover_result.range {
|
||||
let start = snapshot
|
||||
.buffer_snapshot
|
||||
.anchor_in_excerpt(excerpt_id.clone(), range.start);
|
||||
let end = snapshot
|
||||
.buffer_snapshot
|
||||
.anchor_in_excerpt(excerpt_id.clone(), range.end);
|
||||
|
||||
start..end
|
||||
} else {
|
||||
anchor..anchor
|
||||
};
|
||||
|
||||
let language_registry = project.update(&mut cx, |p, _| p.languages().clone());
|
||||
let blocks = hover_result.contents;
|
||||
let language = hover_result.language;
|
||||
let parsed_content = parse_blocks(&blocks, &language_registry, language).await;
|
||||
|
||||
Some(InfoPopover {
|
||||
project: project.clone(),
|
||||
symbol_range: RangeInEditor::Text(range),
|
||||
blocks,
|
||||
parsed_content,
|
||||
})
|
||||
}
|
||||
|
||||
// Create symbol range of anchors for highlighting and filtering
|
||||
// of future requests.
|
||||
let range = if let Some(range) = hover_result.range {
|
||||
let start = snapshot
|
||||
.buffer_snapshot
|
||||
.anchor_in_excerpt(excerpt_id.clone(), range.start);
|
||||
let end = snapshot
|
||||
.buffer_snapshot
|
||||
.anchor_in_excerpt(excerpt_id.clone(), range.end);
|
||||
|
||||
start..end
|
||||
} else {
|
||||
anchor..anchor
|
||||
};
|
||||
|
||||
Some(InfoPopover {
|
||||
project: project.clone(),
|
||||
symbol_range: RangeInEditor::Text(range),
|
||||
blocks: hover_result.contents,
|
||||
language: hover_result.language,
|
||||
rendered_content: None,
|
||||
})
|
||||
});
|
||||
_ => None,
|
||||
};
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
if let Some(symbol_range) = hover_popover
|
||||
@ -345,44 +353,56 @@ fn show_hover(
|
||||
editor.hover_state.info_task = Some(task);
|
||||
}
|
||||
|
||||
fn render_blocks(
|
||||
async fn parse_blocks(
|
||||
blocks: &[HoverBlock],
|
||||
language_registry: &Arc<LanguageRegistry>,
|
||||
language: Option<&Arc<Language>>,
|
||||
) -> RichText {
|
||||
let mut data = RichText {
|
||||
text: Default::default(),
|
||||
highlights: Default::default(),
|
||||
region_ranges: Default::default(),
|
||||
regions: Default::default(),
|
||||
};
|
||||
language: Option<Arc<Language>>,
|
||||
) -> markdown::ParsedMarkdown {
|
||||
let mut text = String::new();
|
||||
let mut highlights = Vec::new();
|
||||
let mut region_ranges = Vec::new();
|
||||
let mut regions = Vec::new();
|
||||
|
||||
for block in blocks {
|
||||
match &block.kind {
|
||||
HoverBlockKind::PlainText => {
|
||||
new_paragraph(&mut data.text, &mut Vec::new());
|
||||
data.text.push_str(&block.text);
|
||||
markdown::new_paragraph(&mut text, &mut Vec::new());
|
||||
text.push_str(&block.text);
|
||||
}
|
||||
|
||||
HoverBlockKind::Markdown => {
|
||||
render_markdown_mut(&block.text, language_registry, language, &mut data)
|
||||
markdown::parse_markdown_block(
|
||||
&block.text,
|
||||
language_registry,
|
||||
language.clone(),
|
||||
&mut text,
|
||||
&mut highlights,
|
||||
&mut region_ranges,
|
||||
&mut regions,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
HoverBlockKind::Code { language } => {
|
||||
if let Some(language) = language_registry
|
||||
.language_for_name(language)
|
||||
.now_or_never()
|
||||
.and_then(Result::ok)
|
||||
{
|
||||
render_code(&mut data.text, &mut data.highlights, &block.text, &language);
|
||||
markdown::highlight_code(&mut text, &mut highlights, &block.text, &language);
|
||||
} else {
|
||||
data.text.push_str(&block.text);
|
||||
text.push_str(&block.text);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data.text = data.text.trim().to_string();
|
||||
|
||||
data
|
||||
ParsedMarkdown {
|
||||
text: text.trim().to_string(),
|
||||
highlights,
|
||||
region_ranges,
|
||||
regions,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
@ -403,6 +423,7 @@ impl HoverState {
|
||||
snapshot: &EditorSnapshot,
|
||||
style: &EditorStyle,
|
||||
visible_rows: Range<u32>,
|
||||
workspace: Option<WeakViewHandle<Workspace>>,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) -> Option<(DisplayPoint, Vec<AnyElement<Editor>>)> {
|
||||
// If there is a diagnostic, position the popovers based on that.
|
||||
@ -432,7 +453,7 @@ impl HoverState {
|
||||
elements.push(diagnostic_popover.render(style, cx));
|
||||
}
|
||||
if let Some(info_popover) = self.info_popover.as_mut() {
|
||||
elements.push(info_popover.render(style, cx));
|
||||
elements.push(info_popover.render(style, workspace, cx));
|
||||
}
|
||||
|
||||
Some((point, elements))
|
||||
@ -444,32 +465,23 @@ pub struct InfoPopover {
|
||||
pub project: ModelHandle<Project>,
|
||||
symbol_range: RangeInEditor,
|
||||
pub blocks: Vec<HoverBlock>,
|
||||
language: Option<Arc<Language>>,
|
||||
rendered_content: Option<RichText>,
|
||||
parsed_content: ParsedMarkdown,
|
||||
}
|
||||
|
||||
impl InfoPopover {
|
||||
pub fn render(
|
||||
&mut self,
|
||||
style: &EditorStyle,
|
||||
workspace: Option<WeakViewHandle<Workspace>>,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) -> AnyElement<Editor> {
|
||||
let rendered_content = self.rendered_content.get_or_insert_with(|| {
|
||||
render_blocks(
|
||||
&self.blocks,
|
||||
self.project.read(cx).languages(),
|
||||
self.language.as_ref(),
|
||||
)
|
||||
});
|
||||
|
||||
MouseEventHandler::new::<InfoPopover, _>(0, cx, move |_, cx| {
|
||||
let code_span_background_color = style.document_highlight_read_background;
|
||||
MouseEventHandler::new::<InfoPopover, _>(0, cx, |_, cx| {
|
||||
Flex::column()
|
||||
.scrollable::<HoverBlock>(1, None, cx)
|
||||
.with_child(rendered_content.element(
|
||||
style.syntax.clone(),
|
||||
style.text.clone(),
|
||||
code_span_background_color,
|
||||
.scrollable::<HoverBlock>(0, None, cx)
|
||||
.with_child(crate::render_parsed_markdown::<HoverBlock>(
|
||||
&self.parsed_content,
|
||||
style,
|
||||
workspace,
|
||||
cx,
|
||||
))
|
||||
.contained()
|
||||
@ -572,7 +584,6 @@ mod tests {
|
||||
use language::{language_settings::InlayHintSettings, Diagnostic, DiagnosticSet};
|
||||
use lsp::LanguageServerId;
|
||||
use project::{HoverBlock, HoverBlockKind};
|
||||
use rich_text::Highlight;
|
||||
use smol::stream::StreamExt;
|
||||
use unindent::Unindent;
|
||||
use util::test::marked_text_ranges;
|
||||
@ -793,7 +804,7 @@ mod tests {
|
||||
}],
|
||||
);
|
||||
|
||||
let rendered = render_blocks(&blocks, &Default::default(), None);
|
||||
let rendered = smol::block_on(parse_blocks(&blocks, &Default::default(), None));
|
||||
assert_eq!(
|
||||
rendered.text,
|
||||
code_str.trim(),
|
||||
@ -900,7 +911,7 @@ mod tests {
|
||||
// Links
|
||||
Row {
|
||||
blocks: vec![HoverBlock {
|
||||
text: "one [two](the-url) three".to_string(),
|
||||
text: "one [two](https://the-url) three".to_string(),
|
||||
kind: HoverBlockKind::Markdown,
|
||||
}],
|
||||
expected_marked_text: "one «two» three".to_string(),
|
||||
@ -921,7 +932,7 @@ mod tests {
|
||||
- a
|
||||
- b
|
||||
* two
|
||||
- [c](the-url)
|
||||
- [c](https://the-url)
|
||||
- d"
|
||||
.unindent(),
|
||||
kind: HoverBlockKind::Markdown,
|
||||
@ -985,7 +996,7 @@ mod tests {
|
||||
expected_styles,
|
||||
} in &rows[0..]
|
||||
{
|
||||
let rendered = render_blocks(&blocks, &Default::default(), None);
|
||||
let rendered = smol::block_on(parse_blocks(&blocks, &Default::default(), None));
|
||||
|
||||
let (expected_text, ranges) = marked_text_ranges(expected_marked_text, false);
|
||||
let expected_highlights = ranges
|
||||
@ -1001,11 +1012,8 @@ mod tests {
|
||||
.highlights
|
||||
.iter()
|
||||
.filter_map(|(range, highlight)| {
|
||||
let style = match highlight {
|
||||
Highlight::Id(id) => id.style(&style.syntax)?,
|
||||
Highlight::Highlight(style) => style.clone(),
|
||||
};
|
||||
Some((range.clone(), style))
|
||||
let highlight = highlight.to_highlight_style(&style.syntax)?;
|
||||
Some((range.clone(), highlight))
|
||||
})
|
||||
.collect();
|
||||
|
||||
@ -1258,11 +1266,7 @@ mod tests {
|
||||
"Popover range should match the new type label part"
|
||||
);
|
||||
assert_eq!(
|
||||
popover
|
||||
.rendered_content
|
||||
.as_ref()
|
||||
.expect("should have label text for new type hint")
|
||||
.text,
|
||||
popover.parsed_content.text,
|
||||
format!("A tooltip for `{new_type_label}`"),
|
||||
"Rendered text should not anyhow alter backticks"
|
||||
);
|
||||
@ -1316,11 +1320,7 @@ mod tests {
|
||||
"Popover range should match the struct label part"
|
||||
);
|
||||
assert_eq!(
|
||||
popover
|
||||
.rendered_content
|
||||
.as_ref()
|
||||
.expect("should have label text for struct hint")
|
||||
.text,
|
||||
popover.parsed_content.text,
|
||||
format!("A tooltip for {struct_label}"),
|
||||
"Rendered markdown element should remove backticks from text"
|
||||
);
|
||||
|
@ -2,7 +2,8 @@ use std::{any::Any, cell::Cell, f32::INFINITY, ops::Range, rc::Rc};
|
||||
|
||||
use crate::{
|
||||
json::{self, ToJson, Value},
|
||||
AnyElement, Axis, Element, ElementStateHandle, SizeConstraint, Vector2FExt, ViewContext,
|
||||
AnyElement, Axis, Element, ElementStateHandle, SizeConstraint, TypeTag, Vector2FExt,
|
||||
ViewContext,
|
||||
};
|
||||
use pathfinder_geometry::{
|
||||
rect::RectF,
|
||||
@ -10,10 +11,10 @@ use pathfinder_geometry::{
|
||||
};
|
||||
use serde_json::json;
|
||||
|
||||
#[derive(Default)]
|
||||
struct ScrollState {
|
||||
scroll_to: Cell<Option<usize>>,
|
||||
scroll_position: Cell<f32>,
|
||||
type_tag: TypeTag,
|
||||
}
|
||||
|
||||
pub struct Flex<V> {
|
||||
@ -66,8 +67,14 @@ impl<V: 'static> Flex<V> {
|
||||
where
|
||||
Tag: 'static,
|
||||
{
|
||||
let scroll_state = cx.default_element_state::<Tag, Rc<ScrollState>>(element_id);
|
||||
scroll_state.read(cx).scroll_to.set(scroll_to);
|
||||
let scroll_state = cx.element_state::<Tag, Rc<ScrollState>>(
|
||||
element_id,
|
||||
Rc::new(ScrollState {
|
||||
scroll_to: Cell::new(scroll_to),
|
||||
scroll_position: Default::default(),
|
||||
type_tag: TypeTag::new::<Tag>(),
|
||||
}),
|
||||
);
|
||||
self.scroll_state = Some((scroll_state, cx.handle().id()));
|
||||
self
|
||||
}
|
||||
@ -276,38 +283,44 @@ impl<V: 'static> Element<V> for Flex<V> {
|
||||
if let Some((scroll_state, id)) = &self.scroll_state {
|
||||
let scroll_state = scroll_state.read(cx).clone();
|
||||
cx.scene().push_mouse_region(
|
||||
crate::MouseRegion::new::<Self>(*id, 0, bounds)
|
||||
.on_scroll({
|
||||
let axis = self.axis;
|
||||
move |e, _: &mut V, cx| {
|
||||
if remaining_space < 0. {
|
||||
let scroll_delta = e.delta.raw();
|
||||
crate::MouseRegion::from_handlers(
|
||||
scroll_state.type_tag,
|
||||
*id,
|
||||
0,
|
||||
bounds,
|
||||
Default::default(),
|
||||
)
|
||||
.on_scroll({
|
||||
let axis = self.axis;
|
||||
move |e, _: &mut V, cx| {
|
||||
if remaining_space < 0. {
|
||||
let scroll_delta = e.delta.raw();
|
||||
|
||||
let mut delta = match axis {
|
||||
Axis::Horizontal => {
|
||||
if scroll_delta.x().abs() >= scroll_delta.y().abs() {
|
||||
scroll_delta.x()
|
||||
} else {
|
||||
scroll_delta.y()
|
||||
}
|
||||
let mut delta = match axis {
|
||||
Axis::Horizontal => {
|
||||
if scroll_delta.x().abs() >= scroll_delta.y().abs() {
|
||||
scroll_delta.x()
|
||||
} else {
|
||||
scroll_delta.y()
|
||||
}
|
||||
Axis::Vertical => scroll_delta.y(),
|
||||
};
|
||||
if !e.delta.precise() {
|
||||
delta *= 20.;
|
||||
}
|
||||
|
||||
scroll_state
|
||||
.scroll_position
|
||||
.set(scroll_state.scroll_position.get() - delta);
|
||||
|
||||
cx.notify();
|
||||
} else {
|
||||
cx.propagate_event();
|
||||
Axis::Vertical => scroll_delta.y(),
|
||||
};
|
||||
if !e.delta.precise() {
|
||||
delta *= 20.;
|
||||
}
|
||||
|
||||
scroll_state
|
||||
.scroll_position
|
||||
.set(scroll_state.scroll_position.get() - delta);
|
||||
|
||||
cx.notify();
|
||||
} else {
|
||||
cx.propagate_event();
|
||||
}
|
||||
})
|
||||
.on_move(|_, _: &mut V, _| { /* Capture move events */ }),
|
||||
}
|
||||
})
|
||||
.on_move(|_, _: &mut V, _| { /* Capture move events */ }),
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -45,6 +45,7 @@ lazy_static.workspace = true
|
||||
log.workspace = true
|
||||
parking_lot.workspace = true
|
||||
postage.workspace = true
|
||||
pulldown-cmark = { version = "0.9.2", default-features = false }
|
||||
regex.workspace = true
|
||||
schemars.workspace = true
|
||||
serde.workspace = true
|
||||
|
@ -1,11 +1,13 @@
|
||||
pub use crate::{
|
||||
diagnostic_set::DiagnosticSet,
|
||||
highlight_map::{HighlightId, HighlightMap},
|
||||
markdown::ParsedMarkdown,
|
||||
proto, BracketPair, Grammar, Language, LanguageConfig, LanguageRegistry, PLAIN_TEXT,
|
||||
};
|
||||
use crate::{
|
||||
diagnostic_set::{DiagnosticEntry, DiagnosticGroup},
|
||||
language_settings::{language_settings, LanguageSettings},
|
||||
markdown::parse_markdown,
|
||||
outline::OutlineItem,
|
||||
syntax_map::{
|
||||
SyntaxLayerInfo, SyntaxMap, SyntaxMapCapture, SyntaxMapCaptures, SyntaxMapMatches,
|
||||
@ -143,11 +145,51 @@ pub struct Diagnostic {
|
||||
pub is_unnecessary: bool,
|
||||
}
|
||||
|
||||
pub async fn prepare_completion_documentation(
|
||||
documentation: &lsp::Documentation,
|
||||
language_registry: &Arc<LanguageRegistry>,
|
||||
language: Option<Arc<Language>>,
|
||||
) -> Documentation {
|
||||
match documentation {
|
||||
lsp::Documentation::String(text) => {
|
||||
if text.lines().count() <= 1 {
|
||||
Documentation::SingleLine(text.clone())
|
||||
} else {
|
||||
Documentation::MultiLinePlainText(text.clone())
|
||||
}
|
||||
}
|
||||
|
||||
lsp::Documentation::MarkupContent(lsp::MarkupContent { kind, value }) => match kind {
|
||||
lsp::MarkupKind::PlainText => {
|
||||
if value.lines().count() <= 1 {
|
||||
Documentation::SingleLine(value.clone())
|
||||
} else {
|
||||
Documentation::MultiLinePlainText(value.clone())
|
||||
}
|
||||
}
|
||||
|
||||
lsp::MarkupKind::Markdown => {
|
||||
let parsed = parse_markdown(value, language_registry, language).await;
|
||||
Documentation::MultiLineMarkdown(parsed)
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum Documentation {
|
||||
Undocumented,
|
||||
SingleLine(String),
|
||||
MultiLinePlainText(String),
|
||||
MultiLineMarkdown(ParsedMarkdown),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Completion {
|
||||
pub old_range: Range<Anchor>,
|
||||
pub new_text: String,
|
||||
pub label: CodeLabel,
|
||||
pub documentation: Option<Documentation>,
|
||||
pub server_id: LanguageServerId,
|
||||
pub lsp_completion: lsp::CompletionItem,
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ mod buffer;
|
||||
mod diagnostic_set;
|
||||
mod highlight_map;
|
||||
pub mod language_settings;
|
||||
pub mod markdown;
|
||||
mod outline;
|
||||
pub mod proto;
|
||||
mod syntax_map;
|
||||
@ -110,7 +111,6 @@ pub struct LanguageServerName(pub Arc<str>);
|
||||
pub struct CachedLspAdapter {
|
||||
pub name: LanguageServerName,
|
||||
pub short_name: &'static str,
|
||||
pub initialization_options: Option<Value>,
|
||||
pub disk_based_diagnostic_sources: Vec<String>,
|
||||
pub disk_based_diagnostics_progress_token: Option<String>,
|
||||
pub language_ids: HashMap<String, String>,
|
||||
@ -121,7 +121,6 @@ impl CachedLspAdapter {
|
||||
pub async fn new(adapter: Arc<dyn LspAdapter>) -> Arc<Self> {
|
||||
let name = adapter.name().await;
|
||||
let short_name = adapter.short_name();
|
||||
let initialization_options = adapter.initialization_options().await;
|
||||
let disk_based_diagnostic_sources = adapter.disk_based_diagnostic_sources().await;
|
||||
let disk_based_diagnostics_progress_token =
|
||||
adapter.disk_based_diagnostics_progress_token().await;
|
||||
@ -130,7 +129,6 @@ impl CachedLspAdapter {
|
||||
Arc::new(CachedLspAdapter {
|
||||
name,
|
||||
short_name,
|
||||
initialization_options,
|
||||
disk_based_diagnostic_sources,
|
||||
disk_based_diagnostics_progress_token,
|
||||
language_ids,
|
||||
|
301
crates/language/src/markdown.rs
Normal file
301
crates/language/src/markdown.rs
Normal file
@ -0,0 +1,301 @@
|
||||
use std::sync::Arc;
|
||||
use std::{ops::Range, path::PathBuf};
|
||||
|
||||
use crate::{HighlightId, Language, LanguageRegistry};
|
||||
use gpui::fonts::{self, HighlightStyle, Weight};
|
||||
use pulldown_cmark::{CodeBlockKind, Event, Options, Parser, Tag};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ParsedMarkdown {
|
||||
pub text: String,
|
||||
pub highlights: Vec<(Range<usize>, MarkdownHighlight)>,
|
||||
pub region_ranges: Vec<Range<usize>>,
|
||||
pub regions: Vec<ParsedRegion>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum MarkdownHighlight {
|
||||
Style(MarkdownHighlightStyle),
|
||||
Code(HighlightId),
|
||||
}
|
||||
|
||||
impl MarkdownHighlight {
|
||||
pub fn to_highlight_style(&self, theme: &theme::SyntaxTheme) -> Option<HighlightStyle> {
|
||||
match self {
|
||||
MarkdownHighlight::Style(style) => {
|
||||
let mut highlight = HighlightStyle::default();
|
||||
|
||||
if style.italic {
|
||||
highlight.italic = Some(true);
|
||||
}
|
||||
|
||||
if style.underline {
|
||||
highlight.underline = Some(fonts::Underline {
|
||||
thickness: 1.0.into(),
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
|
||||
if style.weight != fonts::Weight::default() {
|
||||
highlight.weight = Some(style.weight);
|
||||
}
|
||||
|
||||
Some(highlight)
|
||||
}
|
||||
|
||||
MarkdownHighlight::Code(id) => id.style(theme),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
||||
pub struct MarkdownHighlightStyle {
|
||||
pub italic: bool,
|
||||
pub underline: bool,
|
||||
pub weight: Weight,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ParsedRegion {
|
||||
pub code: bool,
|
||||
pub link: Option<Link>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Link {
|
||||
Web { url: String },
|
||||
Path { path: PathBuf },
|
||||
}
|
||||
|
||||
impl Link {
|
||||
fn identify(text: String) -> Option<Link> {
|
||||
if text.starts_with("http") {
|
||||
return Some(Link::Web { url: text });
|
||||
}
|
||||
|
||||
let path = PathBuf::from(text);
|
||||
if path.is_absolute() {
|
||||
return Some(Link::Path { path });
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn parse_markdown(
|
||||
markdown: &str,
|
||||
language_registry: &Arc<LanguageRegistry>,
|
||||
language: Option<Arc<Language>>,
|
||||
) -> ParsedMarkdown {
|
||||
let mut text = String::new();
|
||||
let mut highlights = Vec::new();
|
||||
let mut region_ranges = Vec::new();
|
||||
let mut regions = Vec::new();
|
||||
|
||||
parse_markdown_block(
|
||||
markdown,
|
||||
language_registry,
|
||||
language,
|
||||
&mut text,
|
||||
&mut highlights,
|
||||
&mut region_ranges,
|
||||
&mut regions,
|
||||
)
|
||||
.await;
|
||||
|
||||
ParsedMarkdown {
|
||||
text,
|
||||
highlights,
|
||||
region_ranges,
|
||||
regions,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn parse_markdown_block(
|
||||
markdown: &str,
|
||||
language_registry: &Arc<LanguageRegistry>,
|
||||
language: Option<Arc<Language>>,
|
||||
text: &mut String,
|
||||
highlights: &mut Vec<(Range<usize>, MarkdownHighlight)>,
|
||||
region_ranges: &mut Vec<Range<usize>>,
|
||||
regions: &mut Vec<ParsedRegion>,
|
||||
) {
|
||||
let mut bold_depth = 0;
|
||||
let mut italic_depth = 0;
|
||||
let mut link_url = None;
|
||||
let mut current_language = None;
|
||||
let mut list_stack = Vec::new();
|
||||
|
||||
for event in Parser::new_ext(&markdown, Options::all()) {
|
||||
let prev_len = text.len();
|
||||
match event {
|
||||
Event::Text(t) => {
|
||||
if let Some(language) = ¤t_language {
|
||||
highlight_code(text, highlights, t.as_ref(), language);
|
||||
} else {
|
||||
text.push_str(t.as_ref());
|
||||
|
||||
let mut style = MarkdownHighlightStyle::default();
|
||||
|
||||
if bold_depth > 0 {
|
||||
style.weight = Weight::BOLD;
|
||||
}
|
||||
|
||||
if italic_depth > 0 {
|
||||
style.italic = true;
|
||||
}
|
||||
|
||||
if let Some(link) = link_url.clone().and_then(|u| Link::identify(u)) {
|
||||
region_ranges.push(prev_len..text.len());
|
||||
regions.push(ParsedRegion {
|
||||
code: false,
|
||||
link: Some(link),
|
||||
});
|
||||
style.underline = true;
|
||||
}
|
||||
|
||||
if style != MarkdownHighlightStyle::default() {
|
||||
let mut new_highlight = true;
|
||||
if let Some((last_range, MarkdownHighlight::Style(last_style))) =
|
||||
highlights.last_mut()
|
||||
{
|
||||
if last_range.end == prev_len && last_style == &style {
|
||||
last_range.end = text.len();
|
||||
new_highlight = false;
|
||||
}
|
||||
}
|
||||
if new_highlight {
|
||||
let range = prev_len..text.len();
|
||||
highlights.push((range, MarkdownHighlight::Style(style)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Event::Code(t) => {
|
||||
text.push_str(t.as_ref());
|
||||
region_ranges.push(prev_len..text.len());
|
||||
|
||||
let link = link_url.clone().and_then(|u| Link::identify(u));
|
||||
if link.is_some() {
|
||||
highlights.push((
|
||||
prev_len..text.len(),
|
||||
MarkdownHighlight::Style(MarkdownHighlightStyle {
|
||||
underline: true,
|
||||
..Default::default()
|
||||
}),
|
||||
));
|
||||
}
|
||||
regions.push(ParsedRegion { code: true, link });
|
||||
}
|
||||
|
||||
Event::Start(tag) => match tag {
|
||||
Tag::Paragraph => new_paragraph(text, &mut list_stack),
|
||||
|
||||
Tag::Heading(_, _, _) => {
|
||||
new_paragraph(text, &mut list_stack);
|
||||
bold_depth += 1;
|
||||
}
|
||||
|
||||
Tag::CodeBlock(kind) => {
|
||||
new_paragraph(text, &mut list_stack);
|
||||
current_language = if let CodeBlockKind::Fenced(language) = kind {
|
||||
language_registry
|
||||
.language_for_name(language.as_ref())
|
||||
.await
|
||||
.ok()
|
||||
} else {
|
||||
language.clone()
|
||||
}
|
||||
}
|
||||
|
||||
Tag::Emphasis => italic_depth += 1,
|
||||
|
||||
Tag::Strong => bold_depth += 1,
|
||||
|
||||
Tag::Link(_, url, _) => link_url = Some(url.to_string()),
|
||||
|
||||
Tag::List(number) => {
|
||||
list_stack.push((number, false));
|
||||
}
|
||||
|
||||
Tag::Item => {
|
||||
let len = list_stack.len();
|
||||
if let Some((list_number, has_content)) = list_stack.last_mut() {
|
||||
*has_content = false;
|
||||
if !text.is_empty() && !text.ends_with('\n') {
|
||||
text.push('\n');
|
||||
}
|
||||
for _ in 0..len - 1 {
|
||||
text.push_str(" ");
|
||||
}
|
||||
if let Some(number) = list_number {
|
||||
text.push_str(&format!("{}. ", number));
|
||||
*number += 1;
|
||||
*has_content = false;
|
||||
} else {
|
||||
text.push_str("- ");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_ => {}
|
||||
},
|
||||
|
||||
Event::End(tag) => match tag {
|
||||
Tag::Heading(_, _, _) => bold_depth -= 1,
|
||||
Tag::CodeBlock(_) => current_language = None,
|
||||
Tag::Emphasis => italic_depth -= 1,
|
||||
Tag::Strong => bold_depth -= 1,
|
||||
Tag::Link(_, _, _) => link_url = None,
|
||||
Tag::List(_) => drop(list_stack.pop()),
|
||||
_ => {}
|
||||
},
|
||||
|
||||
Event::HardBreak => text.push('\n'),
|
||||
|
||||
Event::SoftBreak => text.push(' '),
|
||||
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn highlight_code(
|
||||
text: &mut String,
|
||||
highlights: &mut Vec<(Range<usize>, MarkdownHighlight)>,
|
||||
content: &str,
|
||||
language: &Arc<Language>,
|
||||
) {
|
||||
let prev_len = text.len();
|
||||
text.push_str(content);
|
||||
for (range, highlight_id) in language.highlight_text(&content.into(), 0..content.len()) {
|
||||
let highlight = MarkdownHighlight::Code(highlight_id);
|
||||
highlights.push((prev_len + range.start..prev_len + range.end, highlight));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_paragraph(text: &mut String, list_stack: &mut Vec<(Option<u64>, bool)>) {
|
||||
let mut is_subsequent_paragraph_of_list = false;
|
||||
if let Some((_, has_content)) = list_stack.last_mut() {
|
||||
if *has_content {
|
||||
is_subsequent_paragraph_of_list = true;
|
||||
} else {
|
||||
*has_content = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if !text.is_empty() {
|
||||
if !text.ends_with('\n') {
|
||||
text.push('\n');
|
||||
}
|
||||
text.push('\n');
|
||||
}
|
||||
for _ in 0..list_stack.len().saturating_sub(1) {
|
||||
text.push_str(" ");
|
||||
}
|
||||
if is_subsequent_paragraph_of_list {
|
||||
text.push_str(" ");
|
||||
}
|
||||
}
|
@ -482,6 +482,7 @@ pub async fn deserialize_completion(
|
||||
lsp_completion.filter_text.as_deref(),
|
||||
)
|
||||
}),
|
||||
documentation: None,
|
||||
server_id: LanguageServerId(completion.server_id as usize),
|
||||
lsp_completion,
|
||||
})
|
||||
|
@ -466,7 +466,10 @@ impl LanguageServer {
|
||||
completion_item: Some(CompletionItemCapability {
|
||||
snippet_support: Some(true),
|
||||
resolve_support: Some(CompletionItemCapabilityResolveSupport {
|
||||
properties: vec!["additionalTextEdits".to_string()],
|
||||
properties: vec![
|
||||
"documentation".to_string(),
|
||||
"additionalTextEdits".to_string(),
|
||||
],
|
||||
}),
|
||||
..Default::default()
|
||||
}),
|
||||
@ -748,6 +751,15 @@ impl LanguageServer {
|
||||
)
|
||||
}
|
||||
|
||||
// some child of string literal (be it "" or ``) which is the child of an attribute
|
||||
|
||||
// <Foo className="bar" />
|
||||
// <Foo className={`bar`} />
|
||||
// <Foo className={something + "bar"} />
|
||||
// <Foo className={something + "bar"} />
|
||||
// const classes = "awesome ";
|
||||
// <Foo className={classes} />
|
||||
|
||||
fn request_internal<T: request::Request>(
|
||||
next_id: &AtomicUsize,
|
||||
response_handlers: &Mutex<Option<HashMap<usize, ResponseHandler>>>,
|
||||
|
@ -10,7 +10,7 @@ use futures::future;
|
||||
use gpui::{AppContext, AsyncAppContext, ModelHandle};
|
||||
use language::{
|
||||
language_settings::{language_settings, InlayHintKind},
|
||||
point_from_lsp, point_to_lsp,
|
||||
point_from_lsp, point_to_lsp, prepare_completion_documentation,
|
||||
proto::{deserialize_anchor, deserialize_version, serialize_anchor, serialize_version},
|
||||
range_from_lsp, range_to_lsp, Anchor, Bias, Buffer, BufferSnapshot, CachedLspAdapter, CharKind,
|
||||
CodeAction, Completion, OffsetRangeExt, PointUtf16, ToOffset, ToPointUtf16, Transaction,
|
||||
@ -1341,7 +1341,7 @@ impl LspCommand for GetCompletions {
|
||||
async fn response_from_lsp(
|
||||
self,
|
||||
completions: Option<lsp::CompletionResponse>,
|
||||
_: ModelHandle<Project>,
|
||||
project: ModelHandle<Project>,
|
||||
buffer: ModelHandle<Buffer>,
|
||||
server_id: LanguageServerId,
|
||||
cx: AsyncAppContext,
|
||||
@ -1358,10 +1358,11 @@ impl LspCommand for GetCompletions {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Default::default()
|
||||
Vec::new()
|
||||
};
|
||||
|
||||
let completions = buffer.read_with(&cx, |buffer, _| {
|
||||
let completions = buffer.read_with(&cx, |buffer, cx| {
|
||||
let language_registry = project.read(cx).languages().clone();
|
||||
let language = buffer.language().cloned();
|
||||
let snapshot = buffer.snapshot();
|
||||
let clipped_position = buffer.clip_point_utf16(Unclipped(self.position), Bias::Left);
|
||||
@ -1370,6 +1371,14 @@ impl LspCommand for GetCompletions {
|
||||
completions
|
||||
.into_iter()
|
||||
.filter_map(move |mut lsp_completion| {
|
||||
if let Some(response_list) = &response_list {
|
||||
if let Some(item_defaults) = &response_list.item_defaults {
|
||||
if let Some(data) = &item_defaults.data {
|
||||
lsp_completion.data = Some(data.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let (old_range, mut new_text) = match lsp_completion.text_edit.as_ref() {
|
||||
// If the language server provides a range to overwrite, then
|
||||
// check that the range is valid.
|
||||
@ -1445,14 +1454,30 @@ impl LspCommand for GetCompletions {
|
||||
}
|
||||
};
|
||||
|
||||
let language = language.clone();
|
||||
LineEnding::normalize(&mut new_text);
|
||||
let language_registry = language_registry.clone();
|
||||
let language = language.clone();
|
||||
|
||||
Some(async move {
|
||||
let mut label = None;
|
||||
if let Some(language) = language {
|
||||
if let Some(language) = language.as_ref() {
|
||||
language.process_completion(&mut lsp_completion).await;
|
||||
label = language.label_for_completion(&lsp_completion).await;
|
||||
}
|
||||
|
||||
let documentation = if let Some(lsp_docs) = &lsp_completion.documentation {
|
||||
Some(
|
||||
prepare_completion_documentation(
|
||||
lsp_docs,
|
||||
&language_registry,
|
||||
language.clone(),
|
||||
)
|
||||
.await,
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Completion {
|
||||
old_range,
|
||||
new_text,
|
||||
@ -1462,6 +1487,7 @@ impl LspCommand for GetCompletions {
|
||||
lsp_completion.filter_text.as_deref(),
|
||||
)
|
||||
}),
|
||||
documentation,
|
||||
server_id,
|
||||
lsp_completion,
|
||||
}
|
||||
|
@ -592,6 +592,7 @@ impl Project {
|
||||
client.add_model_request_handler(Self::handle_apply_code_action);
|
||||
client.add_model_request_handler(Self::handle_on_type_formatting);
|
||||
client.add_model_request_handler(Self::handle_inlay_hints);
|
||||
client.add_model_request_handler(Self::handle_resolve_completion_documentation);
|
||||
client.add_model_request_handler(Self::handle_resolve_inlay_hint);
|
||||
client.add_model_request_handler(Self::handle_refresh_inlay_hints);
|
||||
client.add_model_request_handler(Self::handle_reload_buffers);
|
||||
@ -2751,15 +2752,6 @@ impl Project {
|
||||
let lsp = project_settings.lsp.get(&adapter.name.0);
|
||||
let override_options = lsp.map(|s| s.initialization_options.clone()).flatten();
|
||||
|
||||
let mut initialization_options = adapter.initialization_options.clone();
|
||||
match (&mut initialization_options, override_options) {
|
||||
(Some(initialization_options), Some(override_options)) => {
|
||||
merge_json_value_into(override_options, initialization_options);
|
||||
}
|
||||
(None, override_options) => initialization_options = override_options,
|
||||
_ => {}
|
||||
}
|
||||
|
||||
let server_id = pending_server.server_id;
|
||||
let container_dir = pending_server.container_dir.clone();
|
||||
let state = LanguageServerState::Starting({
|
||||
@ -2771,7 +2763,7 @@ impl Project {
|
||||
cx.spawn_weak(|this, mut cx| async move {
|
||||
let result = Self::setup_and_insert_language_server(
|
||||
this,
|
||||
initialization_options,
|
||||
override_options,
|
||||
pending_server,
|
||||
adapter.clone(),
|
||||
language.clone(),
|
||||
@ -2874,7 +2866,7 @@ impl Project {
|
||||
|
||||
async fn setup_and_insert_language_server(
|
||||
this: WeakModelHandle<Self>,
|
||||
initialization_options: Option<serde_json::Value>,
|
||||
override_initialization_options: Option<serde_json::Value>,
|
||||
pending_server: PendingLanguageServer,
|
||||
adapter: Arc<CachedLspAdapter>,
|
||||
language: Arc<Language>,
|
||||
@ -2884,7 +2876,7 @@ impl Project {
|
||||
) -> Result<Option<Arc<LanguageServer>>> {
|
||||
let setup = Self::setup_pending_language_server(
|
||||
this,
|
||||
initialization_options,
|
||||
override_initialization_options,
|
||||
pending_server,
|
||||
adapter.clone(),
|
||||
server_id,
|
||||
@ -2916,7 +2908,7 @@ impl Project {
|
||||
|
||||
async fn setup_pending_language_server(
|
||||
this: WeakModelHandle<Self>,
|
||||
initialization_options: Option<serde_json::Value>,
|
||||
override_options: Option<serde_json::Value>,
|
||||
pending_server: PendingLanguageServer,
|
||||
adapter: Arc<CachedLspAdapter>,
|
||||
server_id: LanguageServerId,
|
||||
@ -2934,8 +2926,8 @@ impl Project {
|
||||
move |mut params, mut cx| {
|
||||
let this = this;
|
||||
let adapter = adapter.clone();
|
||||
adapter.process_diagnostics(&mut params);
|
||||
if let Some(this) = this.upgrade(&cx) {
|
||||
adapter.process_diagnostics(&mut params);
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.update_diagnostics(
|
||||
server_id,
|
||||
@ -3062,6 +3054,14 @@ impl Project {
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
let mut initialization_options = adapter.adapter.initialization_options().await;
|
||||
match (&mut initialization_options, override_options) {
|
||||
(Some(initialization_options), Some(override_options)) => {
|
||||
merge_json_value_into(override_options, initialization_options);
|
||||
}
|
||||
(None, override_options) => initialization_options = override_options,
|
||||
_ => {}
|
||||
}
|
||||
|
||||
let language_server = language_server.initialize(initialization_options).await?;
|
||||
|
||||
@ -7353,6 +7353,40 @@ impl Project {
|
||||
})
|
||||
}
|
||||
|
||||
async fn handle_resolve_completion_documentation(
|
||||
this: ModelHandle<Self>,
|
||||
envelope: TypedEnvelope<proto::ResolveCompletionDocumentation>,
|
||||
_: Arc<Client>,
|
||||
mut cx: AsyncAppContext,
|
||||
) -> Result<proto::ResolveCompletionDocumentationResponse> {
|
||||
let lsp_completion = serde_json::from_slice(&envelope.payload.lsp_completion)?;
|
||||
|
||||
let completion = this
|
||||
.read_with(&mut cx, |this, _| {
|
||||
let id = LanguageServerId(envelope.payload.language_server_id as usize);
|
||||
let Some(server) = this.language_server_for_id(id) else {
|
||||
return Err(anyhow!("No language server {id}"));
|
||||
};
|
||||
|
||||
Ok(server.request::<lsp::request::ResolveCompletionItem>(lsp_completion))
|
||||
})?
|
||||
.await?;
|
||||
|
||||
let mut is_markdown = false;
|
||||
let text = match completion.documentation {
|
||||
Some(lsp::Documentation::String(text)) => text,
|
||||
|
||||
Some(lsp::Documentation::MarkupContent(lsp::MarkupContent { kind, value })) => {
|
||||
is_markdown = kind == lsp::MarkupKind::Markdown;
|
||||
value
|
||||
}
|
||||
|
||||
_ => String::new(),
|
||||
};
|
||||
|
||||
Ok(proto::ResolveCompletionDocumentationResponse { text, is_markdown })
|
||||
}
|
||||
|
||||
async fn handle_apply_code_action(
|
||||
this: ModelHandle<Self>,
|
||||
envelope: TypedEnvelope<proto::ApplyCodeAction>,
|
||||
|
@ -89,94 +89,95 @@ message Envelope {
|
||||
FormatBuffersResponse format_buffers_response = 70;
|
||||
GetCompletions get_completions = 71;
|
||||
GetCompletionsResponse get_completions_response = 72;
|
||||
ApplyCompletionAdditionalEdits apply_completion_additional_edits = 73;
|
||||
ApplyCompletionAdditionalEditsResponse apply_completion_additional_edits_response = 74;
|
||||
GetCodeActions get_code_actions = 75;
|
||||
GetCodeActionsResponse get_code_actions_response = 76;
|
||||
GetHover get_hover = 77;
|
||||
GetHoverResponse get_hover_response = 78;
|
||||
ApplyCodeAction apply_code_action = 79;
|
||||
ApplyCodeActionResponse apply_code_action_response = 80;
|
||||
PrepareRename prepare_rename = 81;
|
||||
PrepareRenameResponse prepare_rename_response = 82;
|
||||
PerformRename perform_rename = 83;
|
||||
PerformRenameResponse perform_rename_response = 84;
|
||||
SearchProject search_project = 85;
|
||||
SearchProjectResponse search_project_response = 86;
|
||||
ResolveCompletionDocumentation resolve_completion_documentation = 73;
|
||||
ResolveCompletionDocumentationResponse resolve_completion_documentation_response = 74;
|
||||
ApplyCompletionAdditionalEdits apply_completion_additional_edits = 75;
|
||||
ApplyCompletionAdditionalEditsResponse apply_completion_additional_edits_response = 76;
|
||||
GetCodeActions get_code_actions = 77;
|
||||
GetCodeActionsResponse get_code_actions_response = 78;
|
||||
GetHover get_hover = 79;
|
||||
GetHoverResponse get_hover_response = 80;
|
||||
ApplyCodeAction apply_code_action = 81;
|
||||
ApplyCodeActionResponse apply_code_action_response = 82;
|
||||
PrepareRename prepare_rename = 83;
|
||||
PrepareRenameResponse prepare_rename_response = 84;
|
||||
PerformRename perform_rename = 85;
|
||||
PerformRenameResponse perform_rename_response = 86;
|
||||
SearchProject search_project = 87;
|
||||
SearchProjectResponse search_project_response = 88;
|
||||
|
||||
UpdateContacts update_contacts = 87;
|
||||
UpdateInviteInfo update_invite_info = 88;
|
||||
ShowContacts show_contacts = 89;
|
||||
UpdateContacts update_contacts = 89;
|
||||
UpdateInviteInfo update_invite_info = 90;
|
||||
ShowContacts show_contacts = 91;
|
||||
|
||||
GetUsers get_users = 90;
|
||||
FuzzySearchUsers fuzzy_search_users = 91;
|
||||
UsersResponse users_response = 92;
|
||||
RequestContact request_contact = 93;
|
||||
RespondToContactRequest respond_to_contact_request = 94;
|
||||
RemoveContact remove_contact = 95;
|
||||
GetUsers get_users = 92;
|
||||
FuzzySearchUsers fuzzy_search_users = 93;
|
||||
UsersResponse users_response = 94;
|
||||
RequestContact request_contact = 95;
|
||||
RespondToContactRequest respond_to_contact_request = 96;
|
||||
RemoveContact remove_contact = 97;
|
||||
|
||||
Follow follow = 96;
|
||||
FollowResponse follow_response = 97;
|
||||
UpdateFollowers update_followers = 98;
|
||||
Unfollow unfollow = 99;
|
||||
GetPrivateUserInfo get_private_user_info = 100;
|
||||
GetPrivateUserInfoResponse get_private_user_info_response = 101;
|
||||
UpdateDiffBase update_diff_base = 102;
|
||||
Follow follow = 98;
|
||||
FollowResponse follow_response = 99;
|
||||
UpdateFollowers update_followers = 100;
|
||||
Unfollow unfollow = 101;
|
||||
GetPrivateUserInfo get_private_user_info = 102;
|
||||
GetPrivateUserInfoResponse get_private_user_info_response = 103;
|
||||
UpdateDiffBase update_diff_base = 104;
|
||||
|
||||
OnTypeFormatting on_type_formatting = 103;
|
||||
OnTypeFormattingResponse on_type_formatting_response = 104;
|
||||
OnTypeFormatting on_type_formatting = 105;
|
||||
OnTypeFormattingResponse on_type_formatting_response = 106;
|
||||
|
||||
UpdateWorktreeSettings update_worktree_settings = 105;
|
||||
UpdateWorktreeSettings update_worktree_settings = 107;
|
||||
|
||||
InlayHints inlay_hints = 106;
|
||||
InlayHintsResponse inlay_hints_response = 107;
|
||||
ResolveInlayHint resolve_inlay_hint = 108;
|
||||
ResolveInlayHintResponse resolve_inlay_hint_response = 109;
|
||||
RefreshInlayHints refresh_inlay_hints = 110;
|
||||
InlayHints inlay_hints = 108;
|
||||
InlayHintsResponse inlay_hints_response = 109;
|
||||
ResolveInlayHint resolve_inlay_hint = 110;
|
||||
ResolveInlayHintResponse resolve_inlay_hint_response = 111;
|
||||
RefreshInlayHints refresh_inlay_hints = 112;
|
||||
|
||||
CreateChannel create_channel = 111;
|
||||
CreateChannelResponse create_channel_response = 112;
|
||||
InviteChannelMember invite_channel_member = 113;
|
||||
RemoveChannelMember remove_channel_member = 114;
|
||||
RespondToChannelInvite respond_to_channel_invite = 115;
|
||||
UpdateChannels update_channels = 116;
|
||||
JoinChannel join_channel = 117;
|
||||
DeleteChannel delete_channel = 118;
|
||||
GetChannelMembers get_channel_members = 119;
|
||||
GetChannelMembersResponse get_channel_members_response = 120;
|
||||
SetChannelMemberAdmin set_channel_member_admin = 121;
|
||||
RenameChannel rename_channel = 122;
|
||||
RenameChannelResponse rename_channel_response = 123;
|
||||
CreateChannel create_channel = 113;
|
||||
CreateChannelResponse create_channel_response = 114;
|
||||
InviteChannelMember invite_channel_member = 115;
|
||||
RemoveChannelMember remove_channel_member = 116;
|
||||
RespondToChannelInvite respond_to_channel_invite = 117;
|
||||
UpdateChannels update_channels = 118;
|
||||
JoinChannel join_channel = 119;
|
||||
DeleteChannel delete_channel = 120;
|
||||
GetChannelMembers get_channel_members = 121;
|
||||
GetChannelMembersResponse get_channel_members_response = 122;
|
||||
SetChannelMemberAdmin set_channel_member_admin = 123;
|
||||
RenameChannel rename_channel = 124;
|
||||
RenameChannelResponse rename_channel_response = 125;
|
||||
|
||||
JoinChannelBuffer join_channel_buffer = 124;
|
||||
JoinChannelBufferResponse join_channel_buffer_response = 125;
|
||||
UpdateChannelBuffer update_channel_buffer = 126;
|
||||
LeaveChannelBuffer leave_channel_buffer = 127;
|
||||
UpdateChannelBufferCollaborators update_channel_buffer_collaborators = 128;
|
||||
RejoinChannelBuffers rejoin_channel_buffers = 129;
|
||||
RejoinChannelBuffersResponse rejoin_channel_buffers_response = 130;
|
||||
AckBufferOperation ack_buffer_operation = 131;
|
||||
JoinChannelBuffer join_channel_buffer = 126;
|
||||
JoinChannelBufferResponse join_channel_buffer_response = 127;
|
||||
UpdateChannelBuffer update_channel_buffer = 128;
|
||||
LeaveChannelBuffer leave_channel_buffer = 129;
|
||||
UpdateChannelBufferCollaborators update_channel_buffer_collaborators = 130;
|
||||
RejoinChannelBuffers rejoin_channel_buffers = 131;
|
||||
RejoinChannelBuffersResponse rejoin_channel_buffers_response = 132;
|
||||
AckBufferOperation ack_buffer_operation = 133;
|
||||
|
||||
JoinChannelChat join_channel_chat = 132;
|
||||
JoinChannelChatResponse join_channel_chat_response = 133;
|
||||
LeaveChannelChat leave_channel_chat = 134;
|
||||
SendChannelMessage send_channel_message = 135;
|
||||
SendChannelMessageResponse send_channel_message_response = 136;
|
||||
ChannelMessageSent channel_message_sent = 137;
|
||||
GetChannelMessages get_channel_messages = 138;
|
||||
GetChannelMessagesResponse get_channel_messages_response = 139;
|
||||
RemoveChannelMessage remove_channel_message = 140;
|
||||
AckChannelMessage ack_channel_message = 141;
|
||||
GetChannelMessagesById get_channel_messages_by_id = 142;
|
||||
JoinChannelChat join_channel_chat = 134;
|
||||
JoinChannelChatResponse join_channel_chat_response = 135;
|
||||
LeaveChannelChat leave_channel_chat = 136;
|
||||
SendChannelMessage send_channel_message = 137;
|
||||
SendChannelMessageResponse send_channel_message_response = 138;
|
||||
ChannelMessageSent channel_message_sent = 139;
|
||||
GetChannelMessages get_channel_messages = 140;
|
||||
GetChannelMessagesResponse get_channel_messages_response = 141;
|
||||
RemoveChannelMessage remove_channel_message = 142;
|
||||
AckChannelMessage ack_channel_message = 143;
|
||||
GetChannelMessagesById get_channel_messages_by_id = 144;
|
||||
|
||||
LinkChannel link_channel = 143;
|
||||
UnlinkChannel unlink_channel = 144;
|
||||
MoveChannel move_channel = 145;
|
||||
|
||||
NewNotification new_notification = 146;
|
||||
GetNotifications get_notifications = 147;
|
||||
GetNotificationsResponse get_notifications_response = 148; // Current max
|
||||
LinkChannel link_channel = 145;
|
||||
UnlinkChannel unlink_channel = 146;
|
||||
MoveChannel move_channel = 147;
|
||||
|
||||
NewNotification new_notification = 148;
|
||||
GetNotifications get_notifications = 149;
|
||||
GetNotificationsResponse get_notifications_response = 150; // Current max
|
||||
}
|
||||
}
|
||||
|
||||
@ -838,6 +839,17 @@ message ResolveState {
|
||||
}
|
||||
}
|
||||
|
||||
message ResolveCompletionDocumentation {
|
||||
uint64 project_id = 1;
|
||||
uint64 language_server_id = 2;
|
||||
bytes lsp_completion = 3;
|
||||
}
|
||||
|
||||
message ResolveCompletionDocumentationResponse {
|
||||
string text = 1;
|
||||
bool is_markdown = 2;
|
||||
}
|
||||
|
||||
message ResolveInlayHint {
|
||||
uint64 project_id = 1;
|
||||
uint64 buffer_id = 2;
|
||||
|
@ -239,6 +239,8 @@ messages!(
|
||||
(RenameChannelResponse, Foreground),
|
||||
(RenameProjectEntry, Foreground),
|
||||
(RequestContact, Foreground),
|
||||
(ResolveCompletionDocumentation, Background),
|
||||
(ResolveCompletionDocumentationResponse, Background),
|
||||
(ResolveInlayHint, Background),
|
||||
(ResolveInlayHintResponse, Background),
|
||||
(RespondToChannelInvite, Foreground),
|
||||
@ -341,6 +343,10 @@ request_messages!(
|
||||
(RenameChannel, RenameChannelResponse),
|
||||
(RenameProjectEntry, ProjectEntryResponse),
|
||||
(RequestContact, Ack),
|
||||
(
|
||||
ResolveCompletionDocumentation,
|
||||
ResolveCompletionDocumentationResponse
|
||||
),
|
||||
(ResolveInlayHint, ResolveInlayHintResponse),
|
||||
(RespondToChannelInvite, Ack),
|
||||
(RespondToContactRequest, Ack),
|
||||
@ -392,6 +398,7 @@ entity_messages!(
|
||||
ReloadBuffers,
|
||||
RemoveProjectCollaborator,
|
||||
RenameProjectEntry,
|
||||
ResolveCompletionDocumentation,
|
||||
ResolveInlayHint,
|
||||
SaveBuffer,
|
||||
SearchProject,
|
||||
|
@ -51,7 +51,6 @@ workspace = { path = "../workspace", features = ["test-support"] }
|
||||
settings = { path = "../settings", features = ["test-support"]}
|
||||
rust-embed = { version = "8.0", features = ["include-exclude"] }
|
||||
client = { path = "../client" }
|
||||
zed = { path = "../zed"}
|
||||
node_runtime = { path = "../node_runtime"}
|
||||
|
||||
pretty_assertions.workspace = true
|
||||
@ -70,6 +69,3 @@ tree-sitter-elixir.workspace = true
|
||||
tree-sitter-lua.workspace = true
|
||||
tree-sitter-ruby.workspace = true
|
||||
tree-sitter-php.workspace = true
|
||||
|
||||
[[example]]
|
||||
name = "eval"
|
||||
|
@ -150,11 +150,14 @@ impl TerminalView {
|
||||
cx.notify();
|
||||
cx.emit(Event::Wakeup);
|
||||
}
|
||||
|
||||
Event::Bell => {
|
||||
this.has_bell = true;
|
||||
cx.emit(Event::Wakeup);
|
||||
}
|
||||
|
||||
Event::BlinkChanged => this.blinking_on = !this.blinking_on,
|
||||
|
||||
Event::TitleChanged => {
|
||||
if let Some(foreground_info) = &this.terminal().read(cx).foreground_process_info {
|
||||
let cwd = foreground_info.cwd.clone();
|
||||
@ -171,6 +174,7 @@ impl TerminalView {
|
||||
.detach();
|
||||
}
|
||||
}
|
||||
|
||||
Event::NewNavigationTarget(maybe_navigation_target) => {
|
||||
this.can_navigate_to_selected_word = match maybe_navigation_target {
|
||||
Some(MaybeNavigationTarget::Url(_)) => true,
|
||||
@ -180,8 +184,10 @@ impl TerminalView {
|
||||
None => false,
|
||||
}
|
||||
}
|
||||
|
||||
Event::Open(maybe_navigation_target) => match maybe_navigation_target {
|
||||
MaybeNavigationTarget::Url(url) => cx.platform().open_url(url),
|
||||
|
||||
MaybeNavigationTarget::PathLike(maybe_path) => {
|
||||
if !this.can_navigate_to_selected_word {
|
||||
return;
|
||||
@ -246,6 +252,7 @@ impl TerminalView {
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
_ => cx.emit(event.clone()),
|
||||
})
|
||||
.detach();
|
||||
|
@ -867,9 +867,13 @@ pub struct AutocompleteStyle {
|
||||
pub selected_item: ContainerStyle,
|
||||
pub hovered_item: ContainerStyle,
|
||||
pub match_highlight: HighlightStyle,
|
||||
pub server_name_container: ContainerStyle,
|
||||
pub server_name_color: Color,
|
||||
pub server_name_size_percent: f32,
|
||||
pub completion_min_width: f32,
|
||||
pub completion_max_width: f32,
|
||||
pub inline_docs_container: ContainerStyle,
|
||||
pub inline_docs_color: Color,
|
||||
pub inline_docs_size_percent: f32,
|
||||
pub alongside_docs_max_width: f32,
|
||||
pub alongside_docs_container: ContainerStyle,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Default, Deserialize, JsonSchema)]
|
||||
|
@ -16,6 +16,7 @@ pub struct GithubRelease {
|
||||
pub pre_release: bool,
|
||||
pub assets: Vec<GithubReleaseAsset>,
|
||||
pub tarball_url: String,
|
||||
pub zipball_url: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
|
@ -15,6 +15,9 @@ doctest = false
|
||||
name = "Zed"
|
||||
path = "src/main.rs"
|
||||
|
||||
[[example]]
|
||||
name = "semantic_index_eval"
|
||||
|
||||
[dependencies]
|
||||
audio = { path = "../audio" }
|
||||
activity_indicator = { path = "../activity_indicator" }
|
||||
@ -136,12 +139,14 @@ tree-sitter-yaml.workspace = true
|
||||
tree-sitter-lua.workspace = true
|
||||
tree-sitter-nix.workspace = true
|
||||
tree-sitter-nu.workspace = true
|
||||
tree-sitter-vue.workspace = true
|
||||
|
||||
url = "2.2"
|
||||
urlencoding = "2.1.2"
|
||||
uuid.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
ai = { path = "../ai" }
|
||||
call = { path = "../call", features = ["test-support"] }
|
||||
client = { path = "../client", features = ["test-support"] }
|
||||
editor = { path = "../editor", features = ["test-support"] }
|
||||
|
@ -24,6 +24,7 @@ mod rust;
|
||||
mod svelte;
|
||||
mod tailwind;
|
||||
mod typescript;
|
||||
mod vue;
|
||||
mod yaml;
|
||||
|
||||
// 1. Add tree-sitter-{language} parser to zed crate
|
||||
@ -190,13 +191,20 @@ pub fn init(
|
||||
language(
|
||||
"php",
|
||||
tree_sitter_php::language(),
|
||||
vec![Arc::new(php::IntelephenseLspAdapter::new(node_runtime))],
|
||||
vec![Arc::new(php::IntelephenseLspAdapter::new(
|
||||
node_runtime.clone(),
|
||||
))],
|
||||
);
|
||||
|
||||
language("elm", tree_sitter_elm::language(), vec![]);
|
||||
language("glsl", tree_sitter_glsl::language(), vec![]);
|
||||
language("nix", tree_sitter_nix::language(), vec![]);
|
||||
language("nu", tree_sitter_nu::language(), vec![]);
|
||||
language(
|
||||
"vue",
|
||||
tree_sitter_vue::language(),
|
||||
vec![Arc::new(vue::VueLspAdapter::new(node_runtime))],
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
|
214
crates/zed/src/languages/vue.rs
Normal file
214
crates/zed/src/languages/vue.rs
Normal file
@ -0,0 +1,214 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use async_trait::async_trait;
|
||||
use futures::StreamExt;
|
||||
pub use language::*;
|
||||
use lsp::{CodeActionKind, LanguageServerBinary};
|
||||
use node_runtime::NodeRuntime;
|
||||
use parking_lot::Mutex;
|
||||
use serde_json::Value;
|
||||
use smol::fs::{self};
|
||||
use std::{
|
||||
any::Any,
|
||||
ffi::OsString,
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
};
|
||||
use util::ResultExt;
|
||||
|
||||
pub struct VueLspVersion {
|
||||
vue_version: String,
|
||||
ts_version: String,
|
||||
}
|
||||
|
||||
pub struct VueLspAdapter {
|
||||
node: Arc<dyn NodeRuntime>,
|
||||
typescript_install_path: Mutex<Option<PathBuf>>,
|
||||
}
|
||||
|
||||
impl VueLspAdapter {
|
||||
const SERVER_PATH: &'static str =
|
||||
"node_modules/@vue/language-server/bin/vue-language-server.js";
|
||||
// TODO: this can't be hardcoded, yet we have to figure out how to pass it in initialization_options.
|
||||
const TYPESCRIPT_PATH: &'static str = "node_modules/typescript/lib";
|
||||
pub fn new(node: Arc<dyn NodeRuntime>) -> Self {
|
||||
let typescript_install_path = Mutex::new(None);
|
||||
Self {
|
||||
node,
|
||||
typescript_install_path,
|
||||
}
|
||||
}
|
||||
}
|
||||
#[async_trait]
|
||||
impl super::LspAdapter for VueLspAdapter {
|
||||
async fn name(&self) -> LanguageServerName {
|
||||
LanguageServerName("vue-language-server".into())
|
||||
}
|
||||
|
||||
fn short_name(&self) -> &'static str {
|
||||
"vue-language-server"
|
||||
}
|
||||
|
||||
async fn fetch_latest_server_version(
|
||||
&self,
|
||||
_: &dyn LspAdapterDelegate,
|
||||
) -> Result<Box<dyn 'static + Send + Any>> {
|
||||
Ok(Box::new(VueLspVersion {
|
||||
vue_version: self
|
||||
.node
|
||||
.npm_package_latest_version("@vue/language-server")
|
||||
.await?,
|
||||
ts_version: self.node.npm_package_latest_version("typescript").await?,
|
||||
}) as Box<_>)
|
||||
}
|
||||
async fn initialization_options(&self) -> Option<Value> {
|
||||
let typescript_sdk_path = self.typescript_install_path.lock();
|
||||
let typescript_sdk_path = typescript_sdk_path
|
||||
.as_ref()
|
||||
.expect("initialization_options called without a container_dir for typescript");
|
||||
|
||||
Some(serde_json::json!({
|
||||
"typescript": {
|
||||
"tsdk": typescript_sdk_path
|
||||
}
|
||||
}))
|
||||
}
|
||||
fn code_action_kinds(&self) -> Option<Vec<CodeActionKind>> {
|
||||
// REFACTOR is explicitly disabled, as vue-lsp does not adhere to LSP protocol for code actions with these - it
|
||||
// sends back a CodeAction with neither `command` nor `edits` fields set, which is against the spec.
|
||||
Some(vec![
|
||||
CodeActionKind::EMPTY,
|
||||
CodeActionKind::QUICKFIX,
|
||||
CodeActionKind::REFACTOR_REWRITE,
|
||||
])
|
||||
}
|
||||
async fn fetch_server_binary(
|
||||
&self,
|
||||
version: Box<dyn 'static + Send + Any>,
|
||||
container_dir: PathBuf,
|
||||
_: &dyn LspAdapterDelegate,
|
||||
) -> Result<LanguageServerBinary> {
|
||||
let version = version.downcast::<VueLspVersion>().unwrap();
|
||||
let server_path = container_dir.join(Self::SERVER_PATH);
|
||||
let ts_path = container_dir.join(Self::TYPESCRIPT_PATH);
|
||||
if fs::metadata(&server_path).await.is_err() {
|
||||
self.node
|
||||
.npm_install_packages(
|
||||
&container_dir,
|
||||
&[("@vue/language-server", version.vue_version.as_str())],
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
assert!(fs::metadata(&server_path).await.is_ok());
|
||||
if fs::metadata(&ts_path).await.is_err() {
|
||||
self.node
|
||||
.npm_install_packages(
|
||||
&container_dir,
|
||||
&[("typescript", version.ts_version.as_str())],
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
assert!(fs::metadata(&ts_path).await.is_ok());
|
||||
*self.typescript_install_path.lock() = Some(ts_path);
|
||||
Ok(LanguageServerBinary {
|
||||
path: self.node.binary_path().await?,
|
||||
arguments: vue_server_binary_arguments(&server_path),
|
||||
})
|
||||
}
|
||||
|
||||
async fn cached_server_binary(
|
||||
&self,
|
||||
container_dir: PathBuf,
|
||||
_: &dyn LspAdapterDelegate,
|
||||
) -> Option<LanguageServerBinary> {
|
||||
let (server, ts_path) = get_cached_server_binary(container_dir, self.node.clone()).await?;
|
||||
*self.typescript_install_path.lock() = Some(ts_path);
|
||||
Some(server)
|
||||
}
|
||||
|
||||
async fn installation_test_binary(
|
||||
&self,
|
||||
container_dir: PathBuf,
|
||||
) -> Option<LanguageServerBinary> {
|
||||
let (server, ts_path) = get_cached_server_binary(container_dir, self.node.clone())
|
||||
.await
|
||||
.map(|(mut binary, ts_path)| {
|
||||
binary.arguments = vec!["--help".into()];
|
||||
(binary, ts_path)
|
||||
})?;
|
||||
*self.typescript_install_path.lock() = Some(ts_path);
|
||||
Some(server)
|
||||
}
|
||||
|
||||
async fn label_for_completion(
|
||||
&self,
|
||||
item: &lsp::CompletionItem,
|
||||
language: &Arc<language::Language>,
|
||||
) -> Option<language::CodeLabel> {
|
||||
use lsp::CompletionItemKind as Kind;
|
||||
let len = item.label.len();
|
||||
let grammar = language.grammar()?;
|
||||
let highlight_id = match item.kind? {
|
||||
Kind::CLASS | Kind::INTERFACE => grammar.highlight_id_for_name("type"),
|
||||
Kind::CONSTRUCTOR => grammar.highlight_id_for_name("type"),
|
||||
Kind::CONSTANT => grammar.highlight_id_for_name("constant"),
|
||||
Kind::FUNCTION | Kind::METHOD => grammar.highlight_id_for_name("function"),
|
||||
Kind::PROPERTY | Kind::FIELD => grammar.highlight_id_for_name("tag"),
|
||||
Kind::VARIABLE => grammar.highlight_id_for_name("type"),
|
||||
Kind::KEYWORD => grammar.highlight_id_for_name("keyword"),
|
||||
Kind::VALUE => grammar.highlight_id_for_name("tag"),
|
||||
_ => None,
|
||||
}?;
|
||||
|
||||
let text = match &item.detail {
|
||||
Some(detail) => format!("{} {}", item.label, detail),
|
||||
None => item.label.clone(),
|
||||
};
|
||||
|
||||
Some(language::CodeLabel {
|
||||
text,
|
||||
runs: vec![(0..len, highlight_id)],
|
||||
filter_range: 0..len,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn vue_server_binary_arguments(server_path: &Path) -> Vec<OsString> {
|
||||
vec![server_path.into(), "--stdio".into()]
|
||||
}
|
||||
|
||||
type TypescriptPath = PathBuf;
|
||||
async fn get_cached_server_binary(
|
||||
container_dir: PathBuf,
|
||||
node: Arc<dyn NodeRuntime>,
|
||||
) -> Option<(LanguageServerBinary, TypescriptPath)> {
|
||||
(|| async move {
|
||||
let mut last_version_dir = None;
|
||||
let mut entries = fs::read_dir(&container_dir).await?;
|
||||
while let Some(entry) = entries.next().await {
|
||||
let entry = entry?;
|
||||
if entry.file_type().await?.is_dir() {
|
||||
last_version_dir = Some(entry.path());
|
||||
}
|
||||
}
|
||||
let last_version_dir = last_version_dir.ok_or_else(|| anyhow!("no cached binary"))?;
|
||||
let server_path = last_version_dir.join(VueLspAdapter::SERVER_PATH);
|
||||
let typescript_path = last_version_dir.join(VueLspAdapter::TYPESCRIPT_PATH);
|
||||
if server_path.exists() && typescript_path.exists() {
|
||||
Ok((
|
||||
LanguageServerBinary {
|
||||
path: node.binary_path().await?,
|
||||
arguments: vue_server_binary_arguments(&server_path),
|
||||
},
|
||||
typescript_path,
|
||||
))
|
||||
} else {
|
||||
Err(anyhow!(
|
||||
"missing executable in directory {:?}",
|
||||
last_version_dir
|
||||
))
|
||||
}
|
||||
})()
|
||||
.await
|
||||
.log_err()
|
||||
}
|
2
crates/zed/src/languages/vue/brackets.scm
Normal file
2
crates/zed/src/languages/vue/brackets.scm
Normal file
@ -0,0 +1,2 @@
|
||||
("<" @open ">" @close)
|
||||
("\"" @open "\"" @close)
|
14
crates/zed/src/languages/vue/config.toml
Normal file
14
crates/zed/src/languages/vue/config.toml
Normal file
@ -0,0 +1,14 @@
|
||||
name = "Vue.js"
|
||||
path_suffixes = ["vue"]
|
||||
block_comment = ["<!-- ", " -->"]
|
||||
autoclose_before = ";:.,=}])>"
|
||||
brackets = [
|
||||
{ start = "{", end = "}", close = true, newline = true },
|
||||
{ start = "[", end = "]", close = true, newline = true },
|
||||
{ start = "(", end = ")", close = true, newline = true },
|
||||
{ start = "<", end = ">", close = true, newline = true, not_in = ["string", "comment"] },
|
||||
{ start = "\"", end = "\"", close = true, newline = false, not_in = ["string"] },
|
||||
{ start = "'", end = "'", close = true, newline = false, not_in = ["string", "comment"] },
|
||||
{ start = "`", end = "`", close = true, newline = false, not_in = ["string"] },
|
||||
]
|
||||
word_characters = ["-"]
|
15
crates/zed/src/languages/vue/highlights.scm
Normal file
15
crates/zed/src/languages/vue/highlights.scm
Normal file
@ -0,0 +1,15 @@
|
||||
(attribute) @property
|
||||
(directive_attribute) @property
|
||||
(quoted_attribute_value) @string
|
||||
(interpolation) @punctuation.special
|
||||
(raw_text) @embedded
|
||||
|
||||
((tag_name) @type
|
||||
(#match? @type "^[A-Z]"))
|
||||
|
||||
((directive_name) @keyword
|
||||
(#match? @keyword "^v-"))
|
||||
|
||||
(start_tag) @tag
|
||||
(end_tag) @tag
|
||||
(self_closing_tag) @tag
|
7
crates/zed/src/languages/vue/injections.scm
Normal file
7
crates/zed/src/languages/vue/injections.scm
Normal file
@ -0,0 +1,7 @@
|
||||
(script_element
|
||||
(raw_text) @content
|
||||
(#set! "language" "javascript"))
|
||||
|
||||
(style_element
|
||||
(raw_text) @content
|
||||
(#set! "language" "css"))
|
@ -206,9 +206,13 @@ export default function editor(): any {
|
||||
match_highlight: foreground(theme.middle, "accent", "active"),
|
||||
background: background(theme.middle, "active"),
|
||||
},
|
||||
server_name_container: { padding: { left: 40 } },
|
||||
server_name_color: text(theme.middle, "sans", "disabled", {}).color,
|
||||
server_name_size_percent: 0.75,
|
||||
completion_min_width: 300,
|
||||
completion_max_width: 700,
|
||||
inline_docs_container: { padding: { left: 40 } },
|
||||
inline_docs_color: text(theme.middle, "sans", "disabled", {}).color,
|
||||
inline_docs_size_percent: 0.75,
|
||||
alongside_docs_max_width: 700,
|
||||
alongside_docs_container: { padding: autocomplete_item.padding }
|
||||
},
|
||||
diagnostic_header: {
|
||||
background: background(theme.middle),
|
||||
|
Loading…
Reference in New Issue
Block a user