Detailed changes
@@ -4177,8 +4177,7 @@ dependencies = [
[[package]]
name = "lsp-types"
version = "0.94.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c66bfd44a06ae10647fe3f8214762e9369fd4248df1350924b4ef9e770a85ea1"
+source = "git+https://github.com/zed-industries/lsp-types?branch=updated-completion-list-item-defaults#90a040a1d195687bd19e1df47463320a44e93d7a"
dependencies = [
"bitflags 1.3.2",
"serde",
@@ -44,7 +44,7 @@ use gpui::{
elements::*,
executor,
fonts::{self, HighlightStyle, TextStyle},
- geometry::vector::Vector2F,
+ geometry::vector::{vec2f, Vector2F},
impl_actions,
keymap_matcher::KeymapContext,
platform::{CursorStyle, MouseButton},
@@ -820,6 +820,7 @@ struct CompletionsMenu {
id: CompletionId,
initial_position: Anchor,
buffer: ModelHandle<Buffer>,
+ project: Option<ModelHandle<Project>>,
completions: Arc<[Completion]>,
match_candidates: Vec<StringMatchCandidate>,
matches: Arc<[StringMatch]>,
@@ -863,6 +864,48 @@ impl CompletionsMenu {
fn render(&self, style: EditorStyle, cx: &mut ViewContext<Editor>) -> AnyElement<Editor> {
enum CompletionTag {}
+ let language_servers = self.project.as_ref().map(|project| {
+ project
+ .read(cx)
+ .language_servers_for_buffer(self.buffer.read(cx), cx)
+ .filter(|(_, server)| server.capabilities().completion_provider.is_some())
+ .map(|(adapter, server)| (server.server_id(), adapter.short_name))
+ .collect::<Vec<_>>()
+ });
+ let needs_server_name = language_servers
+ .as_ref()
+ .map_or(false, |servers| servers.len() > 1);
+
+ let get_server_name =
+ move |lookup_server_id: lsp::LanguageServerId| -> Option<&'static str> {
+ language_servers
+ .iter()
+ .flatten()
+ .find_map(|(server_id, server_name)| {
+ if *server_id == lookup_server_id {
+ Some(*server_name)
+ } else {
+ None
+ }
+ })
+ };
+
+ let widest_completion_ix = self
+ .matches
+ .iter()
+ .enumerate()
+ .max_by_key(|(_, mat)| {
+ let completion = &self.completions[mat.candidate_id];
+ let mut len = completion.label.text.chars().count();
+
+ if let Some(server_name) = get_server_name(completion.server_id) {
+ len += server_name.chars().count();
+ }
+
+ len
+ })
+ .map(|(ix, _)| ix);
+
let completions = self.completions.clone();
let matches = self.matches.clone();
let selected_item = self.selected_item;
@@ -889,19 +932,83 @@ impl CompletionsMenu {
style.autocomplete.item
};
- Text::new(completion.label.text.clone(), style.text.clone())
- .with_soft_wrap(false)
- .with_highlights(combine_syntax_and_fuzzy_match_highlights(
- &completion.label.text,
- style.text.color.into(),
- styled_runs_for_code_label(
- &completion.label,
- &style.syntax,
- ),
- &mat.positions,
- ))
- .contained()
- .with_style(item_style)
+ let completion_label =
+ Text::new(completion.label.text.clone(), style.text.clone())
+ .with_soft_wrap(false)
+ .with_highlights(
+ combine_syntax_and_fuzzy_match_highlights(
+ &completion.label.text,
+ style.text.color.into(),
+ styled_runs_for_code_label(
+ &completion.label,
+ &style.syntax,
+ ),
+ &mat.positions,
+ ),
+ );
+
+ if let Some(server_name) = get_server_name(completion.server_id) {
+ Flex::row()
+ .with_child(completion_label)
+ .with_children((|| {
+ if !needs_server_name {
+ return None;
+ }
+
+ let text_style = TextStyle {
+ color: style.autocomplete.server_name_color,
+ font_size: style.text.font_size
+ * style.autocomplete.server_name_size_percent,
+ ..style.text.clone()
+ };
+
+ let label = Text::new(server_name, text_style)
+ .aligned()
+ .constrained()
+ .dynamically(move |constraint, _, _| {
+ gpui::SizeConstraint {
+ min: constraint.min,
+ max: vec2f(
+ constraint.max.x(),
+ constraint.min.y(),
+ ),
+ }
+ });
+
+ if Some(item_ix) == widest_completion_ix {
+ Some(
+ label
+ .contained()
+ .with_style(
+ style
+ .autocomplete
+ .server_name_container,
+ )
+ .into_any(),
+ )
+ } else {
+ Some(label.flex_float().into_any())
+ }
+ })())
+ .into_any()
+ } else {
+ completion_label.into_any()
+ }
+ .contained()
+ .with_style(item_style)
+ .constrained()
+ .dynamically(
+ move |constraint, _, _| {
+ if Some(item_ix) == widest_completion_ix {
+ constraint
+ } else {
+ gpui::SizeConstraint {
+ min: constraint.min,
+ max: constraint.min,
+ }
+ }
+ },
+ )
},
)
.with_cursor_style(CursorStyle::PointingHand)
@@ -918,19 +1025,7 @@ impl CompletionsMenu {
}
},
)
- .with_width_from_item(
- self.matches
- .iter()
- .enumerate()
- .max_by_key(|(_, mat)| {
- self.completions[mat.candidate_id]
- .label
- .text
- .chars()
- .count()
- })
- .map(|(ix, _)| ix),
- )
+ .with_width_from_item(widest_completion_ix)
.contained()
.with_style(container_style)
.into_any()
@@ -2983,6 +3078,7 @@ impl Editor {
});
let id = post_inc(&mut self.next_completion_id);
+ let project = self.project.clone();
let task = cx.spawn(|this, mut cx| {
async move {
let menu = if let Some(completions) = completions.await.log_err() {
@@ -3001,6 +3097,7 @@ impl Editor {
})
.collect(),
buffer,
+ project,
completions: completions.into(),
matches: Vec::new().into(),
selected_item: 0,
@@ -9186,6 +9283,7 @@ pub fn split_words<'a>(text: &'a str) -> impl std::iter::Iterator<Item = &'a str
None
})
.flat_map(|word| word.split_inclusive('_'))
+ .flat_map(|word| word.split_inclusive('-'))
}
trait RangeToAnchorExt {
@@ -19,7 +19,8 @@ use gpui::{
use indoc::indoc;
use language::{
language_settings::{AllLanguageSettings, AllLanguageSettingsContent, LanguageSettingsContent},
- BracketPairConfig, FakeLspAdapter, LanguageConfig, LanguageRegistry, Point,
+ BracketPairConfig, FakeLspAdapter, LanguageConfig, LanguageConfigOverride, LanguageRegistry,
+ Override, Point,
};
use parking_lot::Mutex;
use project::project_settings::{LspSettings, ProjectSettings};
@@ -7688,6 +7689,105 @@ async fn test_completions_with_additional_edits(cx: &mut gpui::TestAppContext) {
cx.assert_editor_state(indoc! {"fn main() { let a = Some(2)ห; }"});
}
+#[gpui::test]
+async fn test_completions_in_languages_with_extra_word_characters(cx: &mut gpui::TestAppContext) {
+ init_test(cx, |_| {});
+
+ let mut cx = EditorLspTestContext::new(
+ Language::new(
+ LanguageConfig {
+ path_suffixes: vec!["jsx".into()],
+ overrides: [(
+ "element".into(),
+ LanguageConfigOverride {
+ word_characters: Override::Set(['-'].into_iter().collect()),
+ ..Default::default()
+ },
+ )]
+ .into_iter()
+ .collect(),
+ ..Default::default()
+ },
+ Some(tree_sitter_typescript::language_tsx()),
+ )
+ .with_override_query("(jsx_self_closing_element) @element")
+ .unwrap(),
+ lsp::ServerCapabilities {
+ completion_provider: Some(lsp::CompletionOptions {
+ trigger_characters: Some(vec![":".to_string()]),
+ ..Default::default()
+ }),
+ ..Default::default()
+ },
+ cx,
+ )
+ .await;
+
+ cx.lsp
+ .handle_request::<lsp::request::Completion, _, _>(move |_, _| async move {
+ Ok(Some(lsp::CompletionResponse::Array(vec![
+ lsp::CompletionItem {
+ label: "bg-blue".into(),
+ ..Default::default()
+ },
+ lsp::CompletionItem {
+ label: "bg-red".into(),
+ ..Default::default()
+ },
+ lsp::CompletionItem {
+ label: "bg-yellow".into(),
+ ..Default::default()
+ },
+ ])))
+ });
+
+ cx.set_state(r#"<p class="bgห" />"#);
+
+ // Trigger completion when typing a dash, because the dash is an extra
+ // word character in the 'element' scope, which contains the cursor.
+ cx.simulate_keystroke("-");
+ cx.foreground().run_until_parked();
+ cx.update_editor(|editor, _| {
+ if let Some(ContextMenu::Completions(menu)) = &editor.context_menu {
+ assert_eq!(
+ menu.matches.iter().map(|m| &m.string).collect::<Vec<_>>(),
+ &["bg-red", "bg-blue", "bg-yellow"]
+ );
+ } else {
+ panic!("expected completion menu to be open");
+ }
+ });
+
+ cx.simulate_keystroke("l");
+ cx.foreground().run_until_parked();
+ cx.update_editor(|editor, _| {
+ if let Some(ContextMenu::Completions(menu)) = &editor.context_menu {
+ assert_eq!(
+ menu.matches.iter().map(|m| &m.string).collect::<Vec<_>>(),
+ &["bg-blue", "bg-yellow"]
+ );
+ } else {
+ panic!("expected completion menu to be open");
+ }
+ });
+
+ // When filtering completions, consider the character after the '-' to
+ // be the start of a subword.
+ cx.set_state(r#"<p class="yelห" />"#);
+ cx.simulate_keystroke("l");
+ cx.foreground().run_until_parked();
+ cx.update_editor(|editor, _| {
+ if let Some(ContextMenu::Completions(menu)) = &editor.context_menu {
+ assert_eq!(
+ menu.matches.iter().map(|m| &m.string).collect::<Vec<_>>(),
+ &["bg-yellow"]
+ );
+ } else {
+ panic!("expected completion menu to be open");
+ }
+ });
+}
+
fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
let point = DisplayPoint::new(row as u32, column as u32);
point..point
@@ -177,20 +177,20 @@ pub fn line_end(
pub fn previous_word_start(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
let raw_point = point.to_point(map);
- let language = map.buffer_snapshot.language_at(raw_point);
+ let scope = map.buffer_snapshot.language_scope_at(raw_point);
find_preceding_boundary(map, point, |left, right| {
- (char_kind(language, left) != char_kind(language, right) && !right.is_whitespace())
+ (char_kind(&scope, left) != char_kind(&scope, right) && !right.is_whitespace())
|| left == '\n'
})
}
pub fn previous_subword_start(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
let raw_point = point.to_point(map);
- let language = map.buffer_snapshot.language_at(raw_point);
+ let scope = map.buffer_snapshot.language_scope_at(raw_point);
find_preceding_boundary(map, point, |left, right| {
let is_word_start =
- char_kind(language, left) != char_kind(language, right) && !right.is_whitespace();
+ char_kind(&scope, left) != char_kind(&scope, right) && !right.is_whitespace();
let is_subword_start =
left == '_' && right != '_' || left.is_lowercase() && right.is_uppercase();
is_word_start || is_subword_start || left == '\n'
@@ -199,19 +199,19 @@ pub fn previous_subword_start(map: &DisplaySnapshot, point: DisplayPoint) -> Dis
pub fn next_word_end(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
let raw_point = point.to_point(map);
- let language = map.buffer_snapshot.language_at(raw_point);
+ let scope = map.buffer_snapshot.language_scope_at(raw_point);
find_boundary(map, point, |left, right| {
- (char_kind(language, left) != char_kind(language, right) && !left.is_whitespace())
+ (char_kind(&scope, left) != char_kind(&scope, right) && !left.is_whitespace())
|| right == '\n'
})
}
pub fn next_subword_end(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
let raw_point = point.to_point(map);
- let language = map.buffer_snapshot.language_at(raw_point);
+ let scope = map.buffer_snapshot.language_scope_at(raw_point);
find_boundary(map, point, |left, right| {
let is_word_end =
- (char_kind(language, left) != char_kind(language, right)) && !left.is_whitespace();
+ (char_kind(&scope, left) != char_kind(&scope, right)) && !left.is_whitespace();
let is_subword_end =
left != '_' && right == '_' || left.is_lowercase() && right.is_uppercase();
is_word_end || is_subword_end || right == '\n'
@@ -399,14 +399,14 @@ pub fn find_boundary_in_line(
pub fn is_inside_word(map: &DisplaySnapshot, point: DisplayPoint) -> bool {
let raw_point = point.to_point(map);
- let language = map.buffer_snapshot.language_at(raw_point);
+ let scope = map.buffer_snapshot.language_scope_at(raw_point);
let ix = map.clip_point(point, Bias::Left).to_offset(map, Bias::Left);
let text = &map.buffer_snapshot;
- let next_char_kind = text.chars_at(ix).next().map(|c| char_kind(language, c));
+ let next_char_kind = text.chars_at(ix).next().map(|c| char_kind(&scope, c));
let prev_char_kind = text
.reversed_chars_at(ix)
.next()
- .map(|c| char_kind(language, c));
+ .map(|c| char_kind(&scope, c));
prev_char_kind.zip(next_char_kind) == Some((CharKind::Word, CharKind::Word))
}
@@ -1417,13 +1417,13 @@ impl MultiBuffer {
return false;
}
- let language = self.language_at(position.clone(), cx);
-
- if char_kind(language.as_ref(), char) == CharKind::Word {
+ let snapshot = self.snapshot(cx);
+ let position = position.to_offset(&snapshot);
+ let scope = snapshot.language_scope_at(position);
+ if char_kind(&scope, char) == CharKind::Word {
return true;
}
- let snapshot = self.snapshot(cx);
let anchor = snapshot.anchor_before(position);
anchor
.buffer_id
@@ -1925,8 +1925,8 @@ impl MultiBufferSnapshot {
let mut next_chars = self.chars_at(start).peekable();
let mut prev_chars = self.reversed_chars_at(start).peekable();
- let language = self.language_at(start);
- let kind = |c| char_kind(language, c);
+ let scope = self.language_scope_at(start);
+ let kind = |c| char_kind(&scope, c);
let word_kind = cmp::max(
prev_chars.peek().copied().map(kind),
next_chars.peek().copied().map(kind),
@@ -51,7 +51,7 @@ impl<'a> EditorLspTestContext<'a> {
language
.path_suffixes()
.first()
- .unwrap_or(&"txt".to_string())
+ .expect("language must have a path suffix for EditorLspTestContext")
);
let mut fake_servers = language
@@ -148,6 +148,7 @@ pub struct Completion {
pub old_range: Range<Anchor>,
pub new_text: String,
pub label: CodeLabel,
+ pub server_id: LanguageServerId,
pub lsp_completion: lsp::CompletionItem,
}
@@ -2216,8 +2217,8 @@ impl BufferSnapshot {
let mut next_chars = self.chars_at(start).peekable();
let mut prev_chars = self.reversed_chars_at(start).peekable();
- let language = self.language_at(start);
- let kind = |c| char_kind(language, c);
+ let scope = self.language_scope_at(start);
+ let kind = |c| char_kind(&scope, c);
let word_kind = cmp::max(
prev_chars.peek().copied().map(kind),
next_chars.peek().copied().map(kind),
@@ -3031,17 +3032,21 @@ pub fn contiguous_ranges(
})
}
-pub fn char_kind(language: Option<&Arc<Language>>, c: char) -> CharKind {
+pub fn char_kind(scope: &Option<LanguageScope>, c: char) -> CharKind {
if c.is_whitespace() {
return CharKind::Whitespace;
} else if c.is_alphanumeric() || c == '_' {
return CharKind::Word;
}
- if let Some(language) = language {
- if language.config.word_characters.contains(&c) {
- return CharKind::Word;
+
+ if let Some(scope) = scope {
+ if let Some(characters) = scope.word_characters() {
+ if characters.contains(&c) {
+ return CharKind::Word;
+ }
}
}
+
CharKind::Punctuation
}
@@ -46,7 +46,7 @@ use theme::{SyntaxTheme, Theme};
use tree_sitter::{self, Query};
use unicase::UniCase;
use util::{http::HttpClient, paths::PathExt};
-use util::{merge_json_value_into, post_inc, ResultExt, TryFutureExt as _, UnwrapFuture};
+use util::{post_inc, ResultExt, TryFutureExt as _, UnwrapFuture};
#[cfg(any(test, feature = "test-support"))]
use futures::channel::mpsc;
@@ -91,6 +91,7 @@ pub struct LanguageServerName(pub Arc<str>);
/// once at startup, and caches the results.
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>,
@@ -101,6 +102,7 @@ pub struct CachedLspAdapter {
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 =
@@ -109,6 +111,7 @@ impl CachedLspAdapter {
Arc::new(CachedLspAdapter {
name,
+ short_name,
initialization_options,
disk_based_diagnostic_sources,
disk_based_diagnostics_progress_token,
@@ -176,10 +179,7 @@ impl CachedLspAdapter {
self.adapter.code_action_kinds()
}
- pub fn workspace_configuration(
- &self,
- cx: &mut AppContext,
- ) -> Option<BoxFuture<'static, Value>> {
+ pub fn workspace_configuration(&self, cx: &mut AppContext) -> BoxFuture<'static, Value> {
self.adapter.workspace_configuration(cx)
}
@@ -220,6 +220,8 @@ pub trait LspAdapterDelegate: Send + Sync {
pub trait LspAdapter: 'static + Send + Sync {
async fn name(&self) -> LanguageServerName;
+ fn short_name(&self) -> &'static str;
+
async fn fetch_latest_server_version(
&self,
delegate: &dyn LspAdapterDelegate,
@@ -288,8 +290,8 @@ pub trait LspAdapter: 'static + Send + Sync {
None
}
- fn workspace_configuration(&self, _: &mut AppContext) -> Option<BoxFuture<'static, Value>> {
- None
+ fn workspace_configuration(&self, _: &mut AppContext) -> BoxFuture<'static, Value> {
+ futures::future::ready(serde_json::json!({})).boxed()
}
fn code_action_kinds(&self) -> Option<Vec<CodeActionKind>> {
@@ -344,6 +346,8 @@ pub struct LanguageConfig {
#[serde(default)]
pub block_comment: Option<(Arc<str>, Arc<str>)>,
#[serde(default)]
+ pub scope_opt_in_language_servers: Vec<String>,
+ #[serde(default)]
pub overrides: HashMap<String, LanguageConfigOverride>,
#[serde(default)]
pub word_characters: HashSet<char>,
@@ -374,6 +378,10 @@ pub struct LanguageConfigOverride {
pub block_comment: Override<(Arc<str>, Arc<str>)>,
#[serde(skip_deserializing)]
pub disabled_bracket_ixs: Vec<u16>,
+ #[serde(default)]
+ pub word_characters: Override<HashSet<char>>,
+ #[serde(default)]
+ pub opt_into_language_servers: Vec<String>,
}
#[derive(Clone, Deserialize, Debug)]
@@ -412,6 +420,7 @@ impl Default for LanguageConfig {
autoclose_before: Default::default(),
line_comment: Default::default(),
block_comment: Default::default(),
+ scope_opt_in_language_servers: Default::default(),
overrides: Default::default(),
collapsed_placeholder: Default::default(),
word_characters: Default::default(),
@@ -686,41 +695,6 @@ impl LanguageRegistry {
result
}
- pub fn workspace_configuration(&self, cx: &mut AppContext) -> Task<serde_json::Value> {
- let lsp_adapters = {
- let state = self.state.read();
- state
- .available_languages
- .iter()
- .filter(|l| !l.loaded)
- .flat_map(|l| l.lsp_adapters.clone())
- .chain(
- state
- .languages
- .iter()
- .flat_map(|language| &language.adapters)
- .map(|adapter| adapter.adapter.clone()),
- )
- .collect::<Vec<_>>()
- };
-
- let mut language_configs = Vec::new();
- for adapter in &lsp_adapters {
- if let Some(language_config) = adapter.workspace_configuration(cx) {
- language_configs.push(language_config);
- }
- }
-
- cx.background().spawn(async move {
- let mut config = serde_json::json!({});
- let language_configs = futures::future::join_all(language_configs).await;
- for language_config in language_configs {
- merge_json_value_into(language_config, &mut config);
- }
- config
- })
- }
-
pub fn add(&self, language: Arc<Language>) {
self.state.write().add(language);
}
@@ -1384,13 +1358,23 @@ impl Language {
Ok(self)
}
- pub fn with_override_query(mut self, source: &str) -> Result<Self> {
+ pub fn with_override_query(mut self, source: &str) -> anyhow::Result<Self> {
let query = Query::new(self.grammar_mut().ts_language, source)?;
let mut override_configs_by_id = HashMap::default();
for (ix, name) in query.capture_names().iter().enumerate() {
if !name.starts_with('_') {
let value = self.config.overrides.remove(name).unwrap_or_default();
+ for server_name in &value.opt_into_language_servers {
+ if !self
+ .config
+ .scope_opt_in_language_servers
+ .contains(server_name)
+ {
+ util::debug_panic!("Server {server_name:?} has been opted-in by scope {name:?} but has not been marked as an opt-in server");
+ }
+ }
+
override_configs_by_id.insert(ix as u32, (name.clone(), value));
}
}
@@ -1596,6 +1580,13 @@ impl LanguageScope {
.map(|e| (&e.0, &e.1))
}
+ pub fn word_characters(&self) -> Option<&HashSet<char>> {
+ Override::as_option(
+ self.config_override().map(|o| &o.word_characters),
+ Some(&self.language.config.word_characters),
+ )
+ }
+
pub fn brackets(&self) -> impl Iterator<Item = (&BracketPair, bool)> {
let mut disabled_ids = self
.config_override()
@@ -1622,6 +1613,20 @@ impl LanguageScope {
c.is_whitespace() || self.language.config.autoclose_before.contains(c)
}
+ pub fn language_allowed(&self, name: &LanguageServerName) -> bool {
+ let config = &self.language.config;
+ let opt_in_servers = &config.scope_opt_in_language_servers;
+ if opt_in_servers.iter().any(|o| *o == *name.0) {
+ if let Some(over) = self.config_override() {
+ over.opt_into_language_servers.iter().any(|o| *o == *name.0)
+ } else {
+ false
+ }
+ } else {
+ true
+ }
+ }
+
fn config_override(&self) -> Option<&LanguageConfigOverride> {
let id = self.override_id?;
let grammar = self.language.grammar.as_ref()?;
@@ -1726,6 +1731,10 @@ impl LspAdapter for Arc<FakeLspAdapter> {
LanguageServerName(self.name.into())
}
+ fn short_name(&self) -> &'static str {
+ "FakeLspAdapter"
+ }
+
async fn fetch_latest_server_version(
&self,
_: &dyn LspAdapterDelegate,
@@ -434,6 +434,7 @@ pub fn serialize_completion(completion: &Completion) -> proto::Completion {
old_start: Some(serialize_anchor(&completion.old_range.start)),
old_end: Some(serialize_anchor(&completion.old_range.end)),
new_text: completion.new_text.clone(),
+ server_id: completion.server_id.0 as u64,
lsp_completion: serde_json::to_vec(&completion.lsp_completion).unwrap(),
}
}
@@ -466,6 +467,7 @@ pub async fn deserialize_completion(
lsp_completion.filter_text.as_deref(),
)
}),
+ server_id: LanguageServerId(completion.server_id as usize),
lsp_completion,
})
}
@@ -42,8 +42,8 @@
"repositoryURL": "https://github.com/apple/swift-protobuf.git",
"state": {
"branch": null,
- "revision": "0af9125c4eae12a4973fb66574c53a54962a9e1e",
- "version": "1.21.0"
+ "revision": "ce20dc083ee485524b802669890291c0d8090170",
+ "version": "1.22.1"
}
}
]
@@ -20,7 +20,7 @@ anyhow.workspace = true
async-pipe = { git = "https://github.com/zed-industries/async-pipe-rs", rev = "82d00a04211cf4e1236029aa03e6b6ce2a74c553", optional = true }
futures.workspace = true
log.workspace = true
-lsp-types = "0.94"
+lsp-types = { git = "https://github.com/zed-industries/lsp-types", branch = "updated-completion-list-item-defaults" }
parking_lot.workspace = true
postage.workspace = true
serde.workspace = true
@@ -4,7 +4,7 @@ pub use lsp_types::*;
use anyhow::{anyhow, Context, Result};
use collections::HashMap;
-use futures::{channel::oneshot, io::BufWriter, AsyncRead, AsyncWrite};
+use futures::{channel::oneshot, io::BufWriter, AsyncRead, AsyncWrite, FutureExt};
use gpui::{executor, AsyncAppContext, Task};
use parking_lot::Mutex;
use postage::{barrier, prelude::Stream};
@@ -26,12 +26,14 @@ use std::{
atomic::{AtomicUsize, Ordering::SeqCst},
Arc, Weak,
},
+ time::{Duration, Instant},
};
use std::{path::Path, process::Stdio};
use util::{ResultExt, TryFutureExt};
const JSON_RPC_VERSION: &str = "2.0";
const CONTENT_LEN_HEADER: &str = "Content-Length: ";
+const LSP_REQUEST_TIMEOUT: Duration = Duration::from_secs(60 * 2);
type NotificationHandler = Box<dyn Send + FnMut(Option<usize>, &str, AsyncAppContext)>;
type ResponseHandler = Box<dyn Send + FnOnce(Result<String, Error>)>;
@@ -303,7 +305,7 @@ impl LanguageServer {
stdout.read_exact(&mut buffer).await?;
if let Ok(message) = str::from_utf8(&buffer) {
- log::trace!("incoming message:{}", message);
+ log::trace!("incoming message: {}", message);
for handler in io_handlers.lock().values_mut() {
handler(IoKind::StdOut, message);
}
@@ -468,6 +470,14 @@ impl LanguageServer {
}),
..Default::default()
}),
+ completion_list: Some(CompletionListCapability {
+ item_defaults: Some(vec![
+ "commitCharacters".to_owned(),
+ "editRange".to_owned(),
+ "insertTextMode".to_owned(),
+ "data".to_owned(),
+ ]),
+ }),
..Default::default()
}),
rename: Some(RenameClientCapabilities {
@@ -740,7 +750,7 @@ impl LanguageServer {
outbound_tx: &channel::Sender<String>,
executor: &Arc<executor::Background>,
params: T::Params,
- ) -> impl 'static + Future<Output = Result<T::Result>>
+ ) -> impl 'static + Future<Output = anyhow::Result<T::Result>>
where
T::Result: 'static + Send,
{
@@ -781,10 +791,25 @@ impl LanguageServer {
.try_send(message)
.context("failed to write to language server's stdin");
+ let mut timeout = executor.timer(LSP_REQUEST_TIMEOUT).fuse();
+ let started = Instant::now();
async move {
handle_response?;
send?;
- rx.await?
+
+ let method = T::METHOD;
+ futures::select! {
+ response = rx.fuse() => {
+ let elapsed = started.elapsed();
+ log::trace!("Took {elapsed:?} to recieve response to {method:?} id {id}");
+ response?
+ }
+
+ _ = timeout => {
+ log::error!("Cancelled LSP request task for {method:?} id {id} which took over {LSP_REQUEST_TIMEOUT:?}");
+ anyhow::bail!("LSP request timeout");
+ }
+ }
}
}
@@ -16,7 +16,10 @@ use language::{
CodeAction, Completion, OffsetRangeExt, PointUtf16, ToOffset, ToPointUtf16, Transaction,
Unclipped,
};
-use lsp::{DocumentHighlightKind, LanguageServer, LanguageServerId, OneOf, ServerCapabilities};
+use lsp::{
+ CompletionListItemDefaultsEditRange, DocumentHighlightKind, LanguageServer, LanguageServerId,
+ OneOf, ServerCapabilities,
+};
use std::{cmp::Reverse, ops::Range, path::Path, sync::Arc};
use text::LineEnding;
@@ -1340,13 +1343,19 @@ impl LspCommand for GetCompletions {
completions: Option<lsp::CompletionResponse>,
_: ModelHandle<Project>,
buffer: ModelHandle<Buffer>,
- _: LanguageServerId,
+ server_id: LanguageServerId,
cx: AsyncAppContext,
) -> Result<Vec<Completion>> {
+ let mut response_list = None;
let completions = if let Some(completions) = completions {
match completions {
lsp::CompletionResponse::Array(completions) => completions,
- lsp::CompletionResponse::List(list) => list.items,
+
+ lsp::CompletionResponse::List(mut list) => {
+ let items = std::mem::take(&mut list.items);
+ response_list = Some(list);
+ items
+ }
}
} else {
Default::default()
@@ -1356,6 +1365,7 @@ impl LspCommand for GetCompletions {
let language = buffer.language().cloned();
let snapshot = buffer.snapshot();
let clipped_position = buffer.clip_point_utf16(Unclipped(self.position), Bias::Left);
+
let mut range_for_token = None;
completions
.into_iter()
@@ -1376,6 +1386,7 @@ impl LspCommand for GetCompletions {
edit.new_text.clone(),
)
}
+
// If the language server does not provide a range, then infer
// the range based on the syntax tree.
None => {
@@ -1383,27 +1394,51 @@ impl LspCommand for GetCompletions {
log::info!("completion out of expected range");
return None;
}
- let Range { start, end } = range_for_token
- .get_or_insert_with(|| {
- let offset = self.position.to_offset(&snapshot);
- let (range, kind) = snapshot.surrounding_word(offset);
- if kind == Some(CharKind::Word) {
- range
- } else {
- offset..offset
- }
- })
- .clone();
+
+ let default_edit_range = response_list
+ .as_ref()
+ .and_then(|list| list.item_defaults.as_ref())
+ .and_then(|defaults| defaults.edit_range.as_ref())
+ .and_then(|range| match range {
+ CompletionListItemDefaultsEditRange::Range(r) => Some(r),
+ _ => None,
+ });
+
+ let range = if let Some(range) = default_edit_range {
+ let range = range_from_lsp(range.clone());
+ let start = snapshot.clip_point_utf16(range.start, Bias::Left);
+ let end = snapshot.clip_point_utf16(range.end, Bias::Left);
+ if start != range.start.0 || end != range.end.0 {
+ log::info!("completion out of expected range");
+ return None;
+ }
+
+ snapshot.anchor_before(start)..snapshot.anchor_after(end)
+ } else {
+ range_for_token
+ .get_or_insert_with(|| {
+ let offset = self.position.to_offset(&snapshot);
+ let (range, kind) = snapshot.surrounding_word(offset);
+ let range = if kind == Some(CharKind::Word) {
+ range
+ } else {
+ offset..offset
+ };
+
+ snapshot.anchor_before(range.start)
+ ..snapshot.anchor_after(range.end)
+ })
+ .clone()
+ };
+
let text = lsp_completion
.insert_text
.as_ref()
.unwrap_or(&lsp_completion.label)
.clone();
- (
- snapshot.anchor_before(start)..snapshot.anchor_after(end),
- text,
- )
+ (range, text)
}
+
Some(lsp::CompletionTextEdit::InsertAndReplace(_)) => {
log::info!("unsupported insert/replace completion");
return None;
@@ -1427,6 +1462,7 @@ impl LspCommand for GetCompletions {
lsp_completion.filter_text.as_deref(),
)
}),
+ server_id,
lsp_completion,
}
})
@@ -156,6 +156,11 @@ struct DelayedDebounced {
cancel_channel: Option<oneshot::Sender<()>>,
}
+enum LanguageServerToQuery {
+ Primary,
+ Other(LanguageServerId),
+}
+
impl DelayedDebounced {
fn new() -> DelayedDebounced {
DelayedDebounced {
@@ -634,7 +639,7 @@ impl Project {
cx.observe_global::<SettingsStore, _>(Self::on_settings_changed)
],
_maintain_buffer_languages: Self::maintain_buffer_languages(languages.clone(), cx),
- _maintain_workspace_config: Self::maintain_workspace_config(languages.clone(), cx),
+ _maintain_workspace_config: Self::maintain_workspace_config(cx),
active_entry: None,
languages,
client,
@@ -704,7 +709,7 @@ impl Project {
collaborators: Default::default(),
join_project_response_message_id: response.message_id,
_maintain_buffer_languages: Self::maintain_buffer_languages(languages.clone(), cx),
- _maintain_workspace_config: Self::maintain_workspace_config(languages.clone(), cx),
+ _maintain_workspace_config: Self::maintain_workspace_config(cx),
languages,
user_store: user_store.clone(),
fs,
@@ -2472,35 +2477,42 @@ impl Project {
})
}
- fn maintain_workspace_config(
- languages: Arc<LanguageRegistry>,
- cx: &mut ModelContext<Project>,
- ) -> Task<()> {
+ fn maintain_workspace_config(cx: &mut ModelContext<Project>) -> Task<()> {
let (mut settings_changed_tx, mut settings_changed_rx) = watch::channel();
let _ = postage::stream::Stream::try_recv(&mut settings_changed_rx);
let settings_observation = cx.observe_global::<SettingsStore, _>(move |_, _| {
*settings_changed_tx.borrow_mut() = ();
});
+
cx.spawn_weak(|this, mut cx| async move {
while let Some(_) = settings_changed_rx.next().await {
- let workspace_config = cx.update(|cx| languages.workspace_configuration(cx)).await;
- if let Some(this) = this.upgrade(&cx) {
- this.read_with(&cx, |this, _| {
- for server_state in this.language_servers.values() {
- if let LanguageServerState::Running { server, .. } = server_state {
- server
- .notify::<lsp::notification::DidChangeConfiguration>(
- lsp::DidChangeConfigurationParams {
- settings: workspace_config.clone(),
- },
- )
- .ok();
- }
- }
- })
- } else {
+ let Some(this) = this.upgrade(&cx) else {
break;
+ };
+
+ let servers: Vec<_> = this.read_with(&cx, |this, _| {
+ this.language_servers
+ .values()
+ .filter_map(|state| match state {
+ LanguageServerState::Starting(_) => None,
+ LanguageServerState::Running {
+ adapter, server, ..
+ } => Some((adapter.clone(), server.clone())),
+ })
+ .collect()
+ });
+
+ for (adapter, server) in servers {
+ let workspace_config =
+ cx.update(|cx| adapter.workspace_configuration(cx)).await;
+ server
+ .notify::<lsp::notification::DidChangeConfiguration>(
+ lsp::DidChangeConfigurationParams {
+ settings: workspace_config.clone(),
+ },
+ )
+ .ok();
}
}
@@ -2615,7 +2627,6 @@ impl Project {
let state = LanguageServerState::Starting({
let adapter = adapter.clone();
let server_name = adapter.name.0.clone();
- let languages = self.languages.clone();
let language = language.clone();
let key = key.clone();
@@ -2625,7 +2636,6 @@ impl Project {
initialization_options,
pending_server,
adapter.clone(),
- languages,
language.clone(),
server_id,
key,
@@ -2729,7 +2739,6 @@ impl Project {
initialization_options: Option<serde_json::Value>,
pending_server: PendingLanguageServer,
adapter: Arc<CachedLspAdapter>,
- languages: Arc<LanguageRegistry>,
language: Arc<Language>,
server_id: LanguageServerId,
key: (WorktreeId, LanguageServerName),
@@ -2740,7 +2749,6 @@ impl Project {
initialization_options,
pending_server,
adapter.clone(),
- languages,
server_id,
cx,
);
@@ -2773,16 +2781,13 @@ impl Project {
initialization_options: Option<serde_json::Value>,
pending_server: PendingLanguageServer,
adapter: Arc<CachedLspAdapter>,
- languages: Arc<LanguageRegistry>,
server_id: LanguageServerId,
cx: &mut AsyncAppContext,
) -> Result<Option<Arc<LanguageServer>>> {
- let workspace_config = cx.update(|cx| languages.workspace_configuration(cx)).await;
+ let workspace_config = cx.update(|cx| adapter.workspace_configuration(cx)).await;
let language_server = match pending_server.task.await? {
- Some(server) => server.initialize(initialization_options).await?,
- None => {
- return Ok(None);
- }
+ Some(server) => server,
+ None => return Ok(None),
};
language_server
@@ -2821,12 +2826,12 @@ impl Project {
language_server
.on_request::<lsp::request::WorkspaceConfiguration, _, _>({
- let languages = languages.clone();
+ let adapter = adapter.clone();
move |params, mut cx| {
- let languages = languages.clone();
+ let adapter = adapter.clone();
async move {
let workspace_config =
- cx.update(|cx| languages.workspace_configuration(cx)).await;
+ cx.update(|cx| adapter.workspace_configuration(cx)).await;
Ok(params
.items
.into_iter()
@@ -2932,6 +2937,8 @@ impl Project {
})
.detach();
+ let language_server = language_server.initialize(initialization_options).await?;
+
language_server
.notify::<lsp::notification::DidChangeConfiguration>(
lsp::DidChangeConfigurationParams {
@@ -3892,7 +3899,7 @@ impl Project {
let file = File::from_dyn(buffer.file())?;
let buffer_abs_path = file.as_local().map(|f| f.abs_path(cx));
let server = self
- .primary_language_servers_for_buffer(buffer, cx)
+ .primary_language_server_for_buffer(buffer, cx)
.map(|s| s.1.clone());
Some((buffer_handle, buffer_abs_path, server))
})
@@ -4197,7 +4204,12 @@ impl Project {
cx: &mut ModelContext<Self>,
) -> Task<Result<Vec<LocationLink>>> {
let position = position.to_point_utf16(buffer.read(cx));
- self.request_lsp(buffer.clone(), GetDefinition { position }, cx)
+ self.request_lsp(
+ buffer.clone(),
+ LanguageServerToQuery::Primary,
+ GetDefinition { position },
+ cx,
+ )
}
pub fn type_definition<T: ToPointUtf16>(
@@ -4207,7 +4219,12 @@ impl Project {
cx: &mut ModelContext<Self>,
) -> Task<Result<Vec<LocationLink>>> {
let position = position.to_point_utf16(buffer.read(cx));
- self.request_lsp(buffer.clone(), GetTypeDefinition { position }, cx)
+ self.request_lsp(
+ buffer.clone(),
+ LanguageServerToQuery::Primary,
+ GetTypeDefinition { position },
+ cx,
+ )
}
pub fn references<T: ToPointUtf16>(
@@ -4217,7 +4234,12 @@ impl Project {
cx: &mut ModelContext<Self>,
) -> Task<Result<Vec<Location>>> {
let position = position.to_point_utf16(buffer.read(cx));
- self.request_lsp(buffer.clone(), GetReferences { position }, cx)
+ self.request_lsp(
+ buffer.clone(),
+ LanguageServerToQuery::Primary,
+ GetReferences { position },
+ cx,
+ )
}
pub fn document_highlights<T: ToPointUtf16>(
@@ -4227,7 +4249,12 @@ impl Project {
cx: &mut ModelContext<Self>,
) -> Task<Result<Vec<DocumentHighlight>>> {
let position = position.to_point_utf16(buffer.read(cx));
- self.request_lsp(buffer.clone(), GetDocumentHighlights { position }, cx)
+ self.request_lsp(
+ buffer.clone(),
+ LanguageServerToQuery::Primary,
+ GetDocumentHighlights { position },
+ cx,
+ )
}
pub fn symbols(&self, query: &str, cx: &mut ModelContext<Self>) -> Task<Result<Vec<Symbol>>> {
@@ -4455,17 +4482,66 @@ impl Project {
cx: &mut ModelContext<Self>,
) -> Task<Result<Option<Hover>>> {
let position = position.to_point_utf16(buffer.read(cx));
- self.request_lsp(buffer.clone(), GetHover { position }, cx)
+ self.request_lsp(
+ buffer.clone(),
+ LanguageServerToQuery::Primary,
+ GetHover { position },
+ cx,
+ )
}
- pub fn completions<T: ToPointUtf16>(
+ pub fn completions<T: ToOffset + ToPointUtf16>(
&self,
buffer: &ModelHandle<Buffer>,
position: T,
cx: &mut ModelContext<Self>,
) -> Task<Result<Vec<Completion>>> {
let position = position.to_point_utf16(buffer.read(cx));
- self.request_lsp(buffer.clone(), GetCompletions { position }, cx)
+ if self.is_local() {
+ let snapshot = buffer.read(cx).snapshot();
+ let offset = position.to_offset(&snapshot);
+ let scope = snapshot.language_scope_at(offset);
+
+ let server_ids: Vec<_> = self
+ .language_servers_for_buffer(buffer.read(cx), cx)
+ .filter(|(_, server)| server.capabilities().completion_provider.is_some())
+ .filter(|(adapter, _)| {
+ scope
+ .as_ref()
+ .map(|scope| scope.language_allowed(&adapter.name))
+ .unwrap_or(true)
+ })
+ .map(|(_, server)| server.server_id())
+ .collect();
+
+ let buffer = buffer.clone();
+ cx.spawn(|this, mut cx| async move {
+ let mut tasks = Vec::with_capacity(server_ids.len());
+ this.update(&mut cx, |this, cx| {
+ for server_id in server_ids {
+ tasks.push(this.request_lsp(
+ buffer.clone(),
+ LanguageServerToQuery::Other(server_id),
+ GetCompletions { position },
+ cx,
+ ));
+ }
+ });
+
+ let mut completions = Vec::new();
+ for task in tasks {
+ if let Ok(new_completions) = task.await {
+ completions.extend_from_slice(&new_completions);
+ }
+ }
+
+ Ok(completions)
+ })
+ } else if let Some(project_id) = self.remote_id() {
+ self.send_lsp_proto_request(buffer.clone(), project_id, GetCompletions { position }, cx)
+ } else {
+ Task::ready(Ok(Default::default()))
+ }
}
pub fn apply_additional_edits_for_completion(
@@ -4479,7 +4555,8 @@ impl Project {
let buffer_id = buffer.remote_id();
if self.is_local() {
- let lang_server = match self.primary_language_servers_for_buffer(buffer, cx) {
+ let server_id = completion.server_id;
+ let lang_server = match self.language_server_for_buffer(buffer, server_id, cx) {
Some((_, server)) => server.clone(),
_ => return Task::ready(Ok(Default::default())),
};
@@ -4586,7 +4663,12 @@ impl Project {
) -> Task<Result<Vec<CodeAction>>> {
let buffer = buffer_handle.read(cx);
let range = buffer.anchor_before(range.start)..buffer.anchor_before(range.end);
- self.request_lsp(buffer_handle.clone(), GetCodeActions { range }, cx)
+ self.request_lsp(
+ buffer_handle.clone(),
+ LanguageServerToQuery::Primary,
+ GetCodeActions { range },
+ cx,
+ )
}
pub fn apply_code_action(
@@ -4942,7 +5024,12 @@ impl Project {
cx: &mut ModelContext<Self>,
) -> Task<Result<Option<Range<Anchor>>>> {
let position = position.to_point_utf16(buffer.read(cx));
- self.request_lsp(buffer, PrepareRename { position }, cx)
+ self.request_lsp(
+ buffer,
+ LanguageServerToQuery::Primary,
+ PrepareRename { position },
+ cx,
+ )
}
pub fn perform_rename<T: ToPointUtf16>(
@@ -4956,6 +5043,7 @@ impl Project {
let position = position.to_point_utf16(buffer.read(cx));
self.request_lsp(
buffer,
+ LanguageServerToQuery::Primary,
PerformRename {
position,
new_name,
@@ -4983,6 +5071,7 @@ impl Project {
});
self.request_lsp(
buffer.clone(),
+ LanguageServerToQuery::Primary,
OnTypeFormatting {
position,
trigger,
@@ -5008,7 +5097,12 @@ impl Project {
let lsp_request = InlayHints { range };
if self.is_local() {
- let lsp_request_task = self.request_lsp(buffer_handle.clone(), lsp_request, cx);
+ let lsp_request_task = self.request_lsp(
+ buffer_handle.clone(),
+ LanguageServerToQuery::Primary,
+ lsp_request,
+ cx,
+ );
cx.spawn(|_, mut cx| async move {
buffer_handle
.update(&mut cx, |buffer, _| {
@@ -5441,10 +5535,10 @@ impl Project {
.await;
}
- // TODO: Wire this up to allow selecting a server?
fn request_lsp<R: LspCommand>(
&self,
buffer_handle: ModelHandle<Buffer>,
+ server: LanguageServerToQuery,
request: R,
cx: &mut ModelContext<Self>,
) -> Task<Result<R::Response>>
@@ -5453,11 +5547,19 @@ impl Project {
{
let buffer = buffer_handle.read(cx);
if self.is_local() {
+ let language_server = match server {
+ LanguageServerToQuery::Primary => {
+ match self.primary_language_server_for_buffer(buffer, cx) {
+ Some((_, server)) => Some(Arc::clone(server)),
+ None => return Task::ready(Ok(Default::default())),
+ }
+ }
+ LanguageServerToQuery::Other(id) => self
+ .language_server_for_buffer(buffer, id, cx)
+ .map(|(_, server)| Arc::clone(server)),
+ };
let file = File::from_dyn(buffer.file()).and_then(File::as_local);
- if let Some((file, language_server)) = file.zip(
- self.primary_language_servers_for_buffer(buffer, cx)
- .map(|(_, server)| server.clone()),
- ) {
+ if let (Some(file), Some(language_server)) = (file, language_server) {
let lsp_params = request.to_lsp(&file.abs_path(cx), buffer, &language_server, cx);
return cx.spawn(|this, cx| async move {
if !request.check_capabilities(language_server.capabilities()) {
@@ -5490,31 +5592,40 @@ impl Project {
});
}
} else if let Some(project_id) = self.remote_id() {
- let rpc = self.client.clone();
- let message = request.to_proto(project_id, buffer);
- return cx.spawn_weak(|this, cx| async move {
- // Ensure the project is still alive by the time the task
- // is scheduled.
- this.upgrade(&cx)
- .ok_or_else(|| anyhow!("project dropped"))?;
-
- let response = rpc.request(message).await?;
-
- let this = this
- .upgrade(&cx)
- .ok_or_else(|| anyhow!("project dropped"))?;
- if this.read_with(&cx, |this, _| this.is_read_only()) {
- Err(anyhow!("disconnected before completing request"))
- } else {
- request
- .response_from_proto(response, this, buffer_handle, cx)
- .await
- }
- });
+ return self.send_lsp_proto_request(buffer_handle, project_id, request, cx);
}
+
Task::ready(Ok(Default::default()))
}
+ fn send_lsp_proto_request<R: LspCommand>(
+ &self,
+ buffer: ModelHandle<Buffer>,
+ project_id: u64,
+ request: R,
+ cx: &mut ModelContext<'_, Project>,
+ ) -> Task<anyhow::Result<<R as LspCommand>::Response>> {
+ let rpc = self.client.clone();
+ let message = request.to_proto(project_id, buffer.read(cx));
+ cx.spawn_weak(|this, cx| async move {
+ // Ensure the project is still alive by the time the task
+ // is scheduled.
+ this.upgrade(&cx)
+ .ok_or_else(|| anyhow!("project dropped"))?;
+ let response = rpc.request(message).await?;
+ let this = this
+ .upgrade(&cx)
+ .ok_or_else(|| anyhow!("project dropped"))?;
+ if this.read_with(&cx, |this, _| this.is_read_only()) {
+ Err(anyhow!("disconnected before completing request"))
+ } else {
+ request
+ .response_from_proto(response, this, buffer, cx)
+ .await
+ }
+ })
+ }
+
fn sort_candidates_and_open_buffers(
mut matching_paths_rx: Receiver<SearchMatchCandidate>,
cx: &mut ModelContext<Self>,
@@ -7150,7 +7261,7 @@ impl Project {
let buffer_version = buffer_handle.read_with(&cx, |buffer, _| buffer.version());
let response = this
.update(&mut cx, |this, cx| {
- this.request_lsp(buffer_handle, request, cx)
+ this.request_lsp(buffer_handle, LanguageServerToQuery::Primary, request, cx)
})
.await?;
this.update(&mut cx, |this, cx| {
@@ -7867,7 +7978,7 @@ impl Project {
})
}
- fn primary_language_servers_for_buffer(
+ fn primary_language_server_for_buffer(
&self,
buffer: &Buffer,
cx: &AppContext,
@@ -2272,7 +2272,18 @@ async fn test_completions_without_edit_ranges(cx: &mut gpui::TestAppContext) {
},
Some(tree_sitter_typescript::language_typescript()),
);
- let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
+ let mut fake_language_servers = language
+ .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
+ capabilities: lsp::ServerCapabilities {
+ completion_provider: Some(lsp::CompletionOptions {
+ trigger_characters: Some(vec![":".to_string()]),
+ ..Default::default()
+ }),
+ ..Default::default()
+ },
+ ..Default::default()
+ }))
+ .await;
let fs = FakeFs::new(cx.background());
fs.insert_tree(
@@ -2358,7 +2369,18 @@ async fn test_completions_with_carriage_returns(cx: &mut gpui::TestAppContext) {
},
Some(tree_sitter_typescript::language_typescript()),
);
- let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
+ let mut fake_language_servers = language
+ .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
+ capabilities: lsp::ServerCapabilities {
+ completion_provider: Some(lsp::CompletionOptions {
+ trigger_characters: Some(vec![":".to_string()]),
+ ..Default::default()
+ }),
+ ..Default::default()
+ },
+ ..Default::default()
+ }))
+ .await;
let fs = FakeFs::new(cx.background());
fs.insert_tree(
@@ -225,15 +225,14 @@ impl SearchQuery {
if self.as_str().is_empty() {
return Default::default();
}
- let language = buffer.language_at(0);
+
+ let range_offset = subrange.as_ref().map(|r| r.start).unwrap_or(0);
let rope = if let Some(range) = subrange {
buffer.as_rope().slice(range)
} else {
buffer.as_rope().clone()
};
- let kind = |c| char_kind(language, c);
-
let mut matches = Vec::new();
match self {
Self::Text {
@@ -249,6 +248,9 @@ impl SearchQuery {
let mat = mat.unwrap();
if *whole_word {
+ let scope = buffer.language_scope_at(range_offset + mat.start());
+ let kind = |c| char_kind(&scope, c);
+
let prev_kind = rope.reversed_chars_at(mat.start()).next().map(kind);
let start_kind = kind(rope.chars_at(mat.start()).next().unwrap());
let end_kind = kind(rope.reversed_chars_at(mat.end()).next().unwrap());
@@ -657,7 +657,8 @@ message Completion {
Anchor old_start = 1;
Anchor old_end = 2;
string new_text = 3;
- bytes lsp_completion = 4;
+ uint64 server_id = 4;
+ bytes lsp_completion = 5;
}
message GetCodeActions {
@@ -834,6 +834,9 @@ 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,
}
#[derive(Clone, Copy, Default, Deserialize, JsonSchema)]
@@ -589,12 +589,12 @@ pub(crate) fn next_word_start(
ignore_punctuation: bool,
times: usize,
) -> DisplayPoint {
- let language = map.buffer_snapshot.language_at(point.to_point(map));
+ let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
for _ in 0..times {
let mut crossed_newline = false;
point = movement::find_boundary(map, point, |left, right| {
- let left_kind = char_kind(language, left).coerce_punctuation(ignore_punctuation);
- let right_kind = char_kind(language, right).coerce_punctuation(ignore_punctuation);
+ let left_kind = char_kind(&scope, left).coerce_punctuation(ignore_punctuation);
+ let right_kind = char_kind(&scope, right).coerce_punctuation(ignore_punctuation);
let at_newline = right == '\n';
let found = (left_kind != right_kind && right_kind != CharKind::Whitespace)
@@ -614,12 +614,12 @@ fn next_word_end(
ignore_punctuation: bool,
times: usize,
) -> DisplayPoint {
- let language = map.buffer_snapshot.language_at(point.to_point(map));
+ let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
for _ in 0..times {
*point.column_mut() += 1;
point = movement::find_boundary(map, point, |left, right| {
- let left_kind = char_kind(language, left).coerce_punctuation(ignore_punctuation);
- let right_kind = char_kind(language, right).coerce_punctuation(ignore_punctuation);
+ let left_kind = char_kind(&scope, left).coerce_punctuation(ignore_punctuation);
+ let right_kind = char_kind(&scope, right).coerce_punctuation(ignore_punctuation);
left_kind != right_kind && left_kind != CharKind::Whitespace
});
@@ -645,13 +645,13 @@ fn previous_word_start(
ignore_punctuation: bool,
times: usize,
) -> DisplayPoint {
- let language = map.buffer_snapshot.language_at(point.to_point(map));
+ let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
for _ in 0..times {
// This works even though find_preceding_boundary is called for every character in the line containing
// cursor because the newline is checked only once.
point = movement::find_preceding_boundary(map, point, |left, right| {
- let left_kind = char_kind(language, left).coerce_punctuation(ignore_punctuation);
- let right_kind = char_kind(language, right).coerce_punctuation(ignore_punctuation);
+ let left_kind = char_kind(&scope, left).coerce_punctuation(ignore_punctuation);
+ let right_kind = char_kind(&scope, right).coerce_punctuation(ignore_punctuation);
(left_kind != right_kind && !right.is_whitespace()) || left == '\n'
});
@@ -665,7 +665,7 @@ fn first_non_whitespace(
from: DisplayPoint,
) -> DisplayPoint {
let mut last_point = start_of_line(map, display_lines, from);
- let language = map.buffer_snapshot.language_at(from.to_point(map));
+ let scope = map.buffer_snapshot.language_scope_at(from.to_point(map));
for (ch, point) in map.chars_at(last_point) {
if ch == '\n' {
return from;
@@ -673,7 +673,7 @@ fn first_non_whitespace(
last_point = point;
- if char_kind(language, ch) != CharKind::Whitespace {
+ if char_kind(&scope, ch) != CharKind::Whitespace {
break;
}
}
@@ -86,19 +86,19 @@ fn expand_changed_word_selection(
ignore_punctuation: bool,
) -> bool {
if times.is_none() || times.unwrap() == 1 {
- let language = map
+ let scope = map
.buffer_snapshot
- .language_at(selection.start.to_point(map));
+ .language_scope_at(selection.start.to_point(map));
let in_word = map
.chars_at(selection.head())
.next()
- .map(|(c, _)| char_kind(language, c) != CharKind::Whitespace)
+ .map(|(c, _)| char_kind(&scope, c) != CharKind::Whitespace)
.unwrap_or_default();
if in_word {
selection.end = movement::find_boundary(map, selection.end, |left, right| {
- let left_kind = char_kind(language, left).coerce_punctuation(ignore_punctuation);
- let right_kind = char_kind(language, right).coerce_punctuation(ignore_punctuation);
+ let left_kind = char_kind(&scope, left).coerce_punctuation(ignore_punctuation);
+ let right_kind = char_kind(&scope, right).coerce_punctuation(ignore_punctuation);
left_kind != right_kind && left_kind != CharKind::Whitespace
});
@@ -177,18 +177,20 @@ fn in_word(
ignore_punctuation: bool,
) -> Option<Range<DisplayPoint>> {
// Use motion::right so that we consider the character under the cursor when looking for the start
- let language = map.buffer_snapshot.language_at(relative_to.to_point(map));
+ let scope = map
+ .buffer_snapshot
+ .language_scope_at(relative_to.to_point(map));
let start = movement::find_preceding_boundary_in_line(
map,
right(map, relative_to, 1),
|left, right| {
- char_kind(language, left).coerce_punctuation(ignore_punctuation)
- != char_kind(language, right).coerce_punctuation(ignore_punctuation)
+ char_kind(&scope, left).coerce_punctuation(ignore_punctuation)
+ != char_kind(&scope, right).coerce_punctuation(ignore_punctuation)
},
);
let end = movement::find_boundary_in_line(map, relative_to, |left, right| {
- char_kind(language, left).coerce_punctuation(ignore_punctuation)
- != char_kind(language, right).coerce_punctuation(ignore_punctuation)
+ char_kind(&scope, left).coerce_punctuation(ignore_punctuation)
+ != char_kind(&scope, right).coerce_punctuation(ignore_punctuation)
});
Some(start..end)
@@ -211,11 +213,13 @@ fn around_word(
relative_to: DisplayPoint,
ignore_punctuation: bool,
) -> Option<Range<DisplayPoint>> {
- let language = map.buffer_snapshot.language_at(relative_to.to_point(map));
+ let scope = map
+ .buffer_snapshot
+ .language_scope_at(relative_to.to_point(map));
let in_word = map
.chars_at(relative_to)
.next()
- .map(|(c, _)| char_kind(language, c) != CharKind::Whitespace)
+ .map(|(c, _)| char_kind(&scope, c) != CharKind::Whitespace)
.unwrap_or(false);
if in_word {
@@ -239,21 +243,23 @@ fn around_next_word(
relative_to: DisplayPoint,
ignore_punctuation: bool,
) -> Option<Range<DisplayPoint>> {
- let language = map.buffer_snapshot.language_at(relative_to.to_point(map));
+ let scope = map
+ .buffer_snapshot
+ .language_scope_at(relative_to.to_point(map));
// Get the start of the word
let start = movement::find_preceding_boundary_in_line(
map,
right(map, relative_to, 1),
|left, right| {
- char_kind(language, left).coerce_punctuation(ignore_punctuation)
- != char_kind(language, right).coerce_punctuation(ignore_punctuation)
+ char_kind(&scope, left).coerce_punctuation(ignore_punctuation)
+ != char_kind(&scope, right).coerce_punctuation(ignore_punctuation)
},
);
let mut word_found = false;
let end = movement::find_boundary(map, relative_to, |left, right| {
- let left_kind = char_kind(language, left).coerce_punctuation(ignore_punctuation);
- let right_kind = char_kind(language, right).coerce_punctuation(ignore_punctuation);
+ let left_kind = char_kind(&scope, left).coerce_punctuation(ignore_punctuation);
+ let right_kind = char_kind(&scope, right).coerce_punctuation(ignore_punctuation);
let found = (word_found && left_kind != right_kind) || right == '\n' && left == '\n';
@@ -6,6 +6,7 @@ use std::{borrow::Cow, str, sync::Arc};
use util::asset_str;
mod c;
+mod css;
mod elixir;
mod go;
mod html;
@@ -18,6 +19,7 @@ mod python;
mod ruby;
mod rust;
mod svelte;
+mod tailwind;
mod typescript;
mod yaml;
@@ -51,7 +53,14 @@ pub fn init(languages: Arc<LanguageRegistry>, node_runtime: Arc<NodeRuntime>) {
tree_sitter_cpp::language(),
vec![Arc::new(c::CLspAdapter)],
);
- language("css", tree_sitter_css::language(), vec![]);
+ language(
+ "css",
+ tree_sitter_css::language(),
+ vec![
+ Arc::new(css::CssLspAdapter::new(node_runtime.clone())),
+ Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())),
+ ],
+ );
language(
"elixir",
tree_sitter_elixir::language(),
@@ -95,6 +104,7 @@ pub fn init(languages: Arc<LanguageRegistry>, node_runtime: Arc<NodeRuntime>) {
vec![
Arc::new(typescript::TypeScriptLspAdapter::new(node_runtime.clone())),
Arc::new(typescript::EsLintLspAdapter::new(node_runtime.clone())),
+ Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())),
],
);
language(
@@ -111,12 +121,16 @@ pub fn init(languages: Arc<LanguageRegistry>, node_runtime: Arc<NodeRuntime>) {
vec![
Arc::new(typescript::TypeScriptLspAdapter::new(node_runtime.clone())),
Arc::new(typescript::EsLintLspAdapter::new(node_runtime.clone())),
+ Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())),
],
);
language(
"html",
tree_sitter_html::language(),
- vec![Arc::new(html::HtmlLspAdapter::new(node_runtime.clone()))],
+ vec![
+ Arc::new(html::HtmlLspAdapter::new(node_runtime.clone())),
+ Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())),
+ ],
);
language(
"ruby",
@@ -19,6 +19,10 @@ impl super::LspAdapter for CLspAdapter {
LanguageServerName("clangd".into())
}
+ fn short_name(&self) -> &'static str {
+ "clangd"
+ }
+
async fn fetch_latest_server_version(
&self,
delegate: &dyn LspAdapterDelegate,
@@ -0,0 +1,130 @@
+use anyhow::{anyhow, Result};
+use async_trait::async_trait;
+use futures::StreamExt;
+use language::{LanguageServerName, LspAdapter, LspAdapterDelegate};
+use lsp::LanguageServerBinary;
+use node_runtime::NodeRuntime;
+use serde_json::json;
+use smol::fs;
+use std::{
+ any::Any,
+ ffi::OsString,
+ path::{Path, PathBuf},
+ sync::Arc,
+};
+use util::ResultExt;
+
+const SERVER_PATH: &'static str =
+ "node_modules/vscode-langservers-extracted/bin/vscode-css-language-server";
+
+fn server_binary_arguments(server_path: &Path) -> Vec<OsString> {
+ vec![server_path.into(), "--stdio".into()]
+}
+
+pub struct CssLspAdapter {
+ node: Arc<NodeRuntime>,
+}
+
+impl CssLspAdapter {
+ pub fn new(node: Arc<NodeRuntime>) -> Self {
+ CssLspAdapter { node }
+ }
+}
+
+#[async_trait]
+impl LspAdapter for CssLspAdapter {
+ async fn name(&self) -> LanguageServerName {
+ LanguageServerName("vscode-css-language-server".into())
+ }
+
+ fn short_name(&self) -> &'static str {
+ "css"
+ }
+
+ async fn fetch_latest_server_version(
+ &self,
+ _: &dyn LspAdapterDelegate,
+ ) -> Result<Box<dyn 'static + Any + Send>> {
+ Ok(Box::new(
+ self.node
+ .npm_package_latest_version("vscode-langservers-extracted")
+ .await?,
+ ) as Box<_>)
+ }
+
+ async fn fetch_server_binary(
+ &self,
+ version: Box<dyn 'static + Send + Any>,
+ container_dir: PathBuf,
+ _: &dyn LspAdapterDelegate,
+ ) -> Result<LanguageServerBinary> {
+ let version = version.downcast::<String>().unwrap();
+ let server_path = container_dir.join(SERVER_PATH);
+
+ if fs::metadata(&server_path).await.is_err() {
+ self.node
+ .npm_install_packages(
+ &container_dir,
+ [("vscode-langservers-extracted", version.as_str())],
+ )
+ .await?;
+ }
+
+ Ok(LanguageServerBinary {
+ path: self.node.binary_path().await?,
+ arguments: server_binary_arguments(&server_path),
+ })
+ }
+
+ async fn cached_server_binary(
+ &self,
+ container_dir: PathBuf,
+ _: &dyn LspAdapterDelegate,
+ ) -> Option<LanguageServerBinary> {
+ get_cached_server_binary(container_dir, &self.node).await
+ }
+
+ async fn installation_test_binary(
+ &self,
+ container_dir: PathBuf,
+ ) -> Option<LanguageServerBinary> {
+ get_cached_server_binary(container_dir, &self.node).await
+ }
+
+ async fn initialization_options(&self) -> Option<serde_json::Value> {
+ Some(json!({
+ "provideFormatter": true
+ }))
+ }
+}
+
+async fn get_cached_server_binary(
+ container_dir: PathBuf,
+ node: &NodeRuntime,
+) -> Option<LanguageServerBinary> {
+ (|| 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(SERVER_PATH);
+ if server_path.exists() {
+ Ok(LanguageServerBinary {
+ path: node.binary_path().await?,
+ arguments: server_binary_arguments(&server_path),
+ })
+ } else {
+ Err(anyhow!(
+ "missing executable in directory {:?}",
+ last_version_dir
+ ))
+ }
+ })()
+ .await
+ .log_err()
+}
@@ -8,3 +8,4 @@ brackets = [
{ start = "\"", end = "\"", close = true, newline = false, not_in = ["string", "comment"] },
{ start = "'", end = "'", close = true, newline = false, not_in = ["string", "comment"] },
]
+word_characters = ["-"]
@@ -27,6 +27,10 @@ impl LspAdapter for ElixirLspAdapter {
LanguageServerName("elixir-ls".into())
}
+ fn short_name(&self) -> &'static str {
+ "elixir-ls"
+ }
+
fn will_start_server(
&self,
delegate: &Arc<dyn LspAdapterDelegate>,
@@ -37,6 +37,10 @@ impl super::LspAdapter for GoLspAdapter {
LanguageServerName("gopls".into())
}
+ fn short_name(&self) -> &'static str {
+ "gopls"
+ }
+
async fn fetch_latest_server_version(
&self,
delegate: &dyn LspAdapterDelegate,
@@ -37,6 +37,10 @@ impl LspAdapter for HtmlLspAdapter {
LanguageServerName("vscode-html-language-server".into())
}
+ fn short_name(&self) -> &'static str {
+ "html"
+ }
+
async fn fetch_latest_server_version(
&self,
_: &dyn LspAdapterDelegate,
@@ -10,3 +10,4 @@ brackets = [
{ start = "<", end = ">", close = true, newline = true, not_in = ["comment", "string"] },
{ start = "!--", end = " --", close = true, newline = false, not_in = ["comment", "string"] },
]
+word_characters = ["-"]
@@ -14,7 +14,12 @@ brackets = [
{ start = "/*", end = " */", close = true, newline = false, not_in = ["comment", "string"] },
]
word_characters = ["$", "#"]
+scope_opt_in_language_servers = ["tailwindcss-language-server"]
[overrides.element]
line_comment = { remove = true }
block_comment = ["{/* ", " */}"]
+
+[overrides.string]
+word_characters = ["-"]
+opt_into_language_servers = ["tailwindcss-language-server"]
@@ -43,6 +43,10 @@ impl LspAdapter for JsonLspAdapter {
LanguageServerName("json-language-server".into())
}
+ fn short_name(&self) -> &'static str {
+ "json"
+ }
+
async fn fetch_latest_server_version(
&self,
_: &dyn LspAdapterDelegate,
@@ -102,7 +106,7 @@ impl LspAdapter for JsonLspAdapter {
fn workspace_configuration(
&self,
cx: &mut AppContext,
- ) -> Option<BoxFuture<'static, serde_json::Value>> {
+ ) -> BoxFuture<'static, serde_json::Value> {
let action_names = cx.all_action_names().collect::<Vec<_>>();
let staff_mode = cx.is_staff();
let language_names = &self.languages.language_names();
@@ -113,29 +117,28 @@ impl LspAdapter for JsonLspAdapter {
},
cx,
);
- Some(
- future::ready(serde_json::json!({
- "json": {
- "format": {
- "enable": true,
+
+ future::ready(serde_json::json!({
+ "json": {
+ "format": {
+ "enable": true,
+ },
+ "schemas": [
+ {
+ "fileMatch": [
+ schema_file_match(&paths::SETTINGS),
+ &*paths::LOCAL_SETTINGS_RELATIVE_PATH,
+ ],
+ "schema": settings_schema,
},
- "schemas": [
- {
- "fileMatch": [
- schema_file_match(&paths::SETTINGS),
- &*paths::LOCAL_SETTINGS_RELATIVE_PATH,
- ],
- "schema": settings_schema,
- },
- {
- "fileMatch": [schema_file_match(&paths::KEYMAP)],
- "schema": KeymapFile::generate_json_schema(&action_names),
- }
- ]
- }
- }))
- .boxed(),
- )
+ {
+ "fileMatch": [schema_file_match(&paths::KEYMAP)],
+ "schema": KeymapFile::generate_json_schema(&action_names),
+ }
+ ]
+ }
+ }))
+ .boxed()
}
async fn language_ids(&self) -> HashMap<String, String> {
@@ -70,6 +70,10 @@ impl LspAdapter for PluginLspAdapter {
LanguageServerName(name.into())
}
+ fn short_name(&self) -> &'static str {
+ "PluginLspAdapter"
+ }
+
async fn fetch_latest_server_version(
&self,
_: &dyn LspAdapterDelegate,
@@ -22,6 +22,10 @@ impl super::LspAdapter for LuaLspAdapter {
LanguageServerName("lua-language-server".into())
}
+ fn short_name(&self) -> &'static str {
+ "lua"
+ }
+
async fn fetch_latest_server_version(
&self,
delegate: &dyn LspAdapterDelegate,
@@ -41,6 +41,10 @@ impl LspAdapter for IntelephenseLspAdapter {
LanguageServerName("intelephense".into())
}
+ fn short_name(&self) -> &'static str {
+ "php"
+ }
+
async fn fetch_latest_server_version(
&self,
_delegate: &dyn LspAdapterDelegate,
@@ -35,6 +35,10 @@ impl LspAdapter for PythonLspAdapter {
LanguageServerName("pyright".into())
}
+ fn short_name(&self) -> &'static str {
+ "pyright"
+ }
+
async fn fetch_latest_server_version(
&self,
_: &dyn LspAdapterDelegate,
@@ -12,6 +12,10 @@ impl LspAdapter for RubyLanguageServer {
LanguageServerName("solargraph".into())
}
+ fn short_name(&self) -> &'static str {
+ "solargraph"
+ }
+
async fn fetch_latest_server_version(
&self,
_: &dyn LspAdapterDelegate,
@@ -22,6 +22,10 @@ impl LspAdapter for RustLspAdapter {
LanguageServerName("rust-analyzer".into())
}
+ fn short_name(&self) -> &'static str {
+ "rust"
+ }
+
async fn fetch_latest_server_version(
&self,
delegate: &dyn LspAdapterDelegate,
@@ -36,6 +36,10 @@ impl LspAdapter for SvelteLspAdapter {
LanguageServerName("svelte-language-server".into())
}
+ fn short_name(&self) -> &'static str {
+ "svelte"
+ }
+
async fn fetch_latest_server_version(
&self,
_: &dyn LspAdapterDelegate,
@@ -0,0 +1,161 @@
+use anyhow::{anyhow, Result};
+use async_trait::async_trait;
+use collections::HashMap;
+use futures::{
+ future::{self, BoxFuture},
+ FutureExt, StreamExt,
+};
+use gpui::AppContext;
+use language::{LanguageServerName, LspAdapter, LspAdapterDelegate};
+use lsp::LanguageServerBinary;
+use node_runtime::NodeRuntime;
+use serde_json::{json, Value};
+use smol::fs;
+use std::{
+ any::Any,
+ ffi::OsString,
+ path::{Path, PathBuf},
+ sync::Arc,
+};
+use util::ResultExt;
+
+const SERVER_PATH: &'static str = "node_modules/.bin/tailwindcss-language-server";
+
+fn server_binary_arguments(server_path: &Path) -> Vec<OsString> {
+ vec![server_path.into(), "--stdio".into()]
+}
+
+pub struct TailwindLspAdapter {
+ node: Arc<NodeRuntime>,
+}
+
+impl TailwindLspAdapter {
+ pub fn new(node: Arc<NodeRuntime>) -> Self {
+ TailwindLspAdapter { node }
+ }
+}
+
+#[async_trait]
+impl LspAdapter for TailwindLspAdapter {
+ async fn name(&self) -> LanguageServerName {
+ LanguageServerName("tailwindcss-language-server".into())
+ }
+
+ fn short_name(&self) -> &'static str {
+ "tailwind"
+ }
+
+ async fn fetch_latest_server_version(
+ &self,
+ _: &dyn LspAdapterDelegate,
+ ) -> Result<Box<dyn 'static + Any + Send>> {
+ Ok(Box::new(
+ self.node
+ .npm_package_latest_version("@tailwindcss/language-server")
+ .await?,
+ ) as Box<_>)
+ }
+
+ async fn fetch_server_binary(
+ &self,
+ version: Box<dyn 'static + Send + Any>,
+ container_dir: PathBuf,
+ _: &dyn LspAdapterDelegate,
+ ) -> Result<LanguageServerBinary> {
+ let version = version.downcast::<String>().unwrap();
+ let server_path = container_dir.join(SERVER_PATH);
+
+ if fs::metadata(&server_path).await.is_err() {
+ self.node
+ .npm_install_packages(
+ &container_dir,
+ [("@tailwindcss/language-server", version.as_str())],
+ )
+ .await?;
+ }
+
+ Ok(LanguageServerBinary {
+ path: self.node.binary_path().await?,
+ arguments: server_binary_arguments(&server_path),
+ })
+ }
+
+ async fn cached_server_binary(
+ &self,
+ container_dir: PathBuf,
+ _: &dyn LspAdapterDelegate,
+ ) -> Option<LanguageServerBinary> {
+ get_cached_server_binary(container_dir, &self.node).await
+ }
+
+ async fn installation_test_binary(
+ &self,
+ container_dir: PathBuf,
+ ) -> Option<LanguageServerBinary> {
+ get_cached_server_binary(container_dir, &self.node).await
+ }
+
+ async fn initialization_options(&self) -> Option<serde_json::Value> {
+ Some(json!({
+ "provideFormatter": true,
+ "userLanguages": {
+ "html": "html",
+ "css": "css",
+ "javascript": "javascript",
+ "typescriptreact": "typescriptreact",
+ },
+ }))
+ }
+
+ fn workspace_configuration(&self, _: &mut AppContext) -> BoxFuture<'static, Value> {
+ future::ready(json!({
+ "tailwindCSS": {
+ "emmetCompletions": true,
+ }
+ }))
+ .boxed()
+ }
+
+ async fn language_ids(&self) -> HashMap<String, String> {
+ HashMap::from_iter(
+ [
+ ("HTML".to_string(), "html".to_string()),
+ ("CSS".to_string(), "css".to_string()),
+ ("JavaScript".to_string(), "javascript".to_string()),
+ ("TSX".to_string(), "typescriptreact".to_string()),
+ ]
+ .into_iter(),
+ )
+ }
+}
+
+async fn get_cached_server_binary(
+ container_dir: PathBuf,
+ node: &NodeRuntime,
+) -> Option<LanguageServerBinary> {
+ (|| 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(SERVER_PATH);
+ if server_path.exists() {
+ Ok(LanguageServerBinary {
+ path: node.binary_path().await?,
+ arguments: server_binary_arguments(&server_path),
+ })
+ } else {
+ Err(anyhow!(
+ "missing executable in directory {:?}",
+ last_version_dir
+ ))
+ }
+ })()
+ .await
+ .log_err()
+}
@@ -13,7 +13,12 @@ brackets = [
{ start = "/*", end = " */", close = true, newline = false, not_in = ["string", "comment"] },
]
word_characters = ["#", "$"]
+scope_opt_in_language_servers = ["tailwindcss-language-server"]
[overrides.element]
line_comment = { remove = true }
block_comment = ["{/* ", " */}"]
+
+[overrides.string]
+word_characters = ["-"]
+opt_into_language_servers = ["tailwindcss-language-server"]
@@ -56,6 +56,10 @@ impl LspAdapter for TypeScriptLspAdapter {
LanguageServerName("typescript-language-server".into())
}
+ fn short_name(&self) -> &'static str {
+ "tsserver"
+ }
+
async fn fetch_latest_server_version(
&self,
_: &dyn LspAdapterDelegate,
@@ -202,24 +206,26 @@ impl EsLintLspAdapter {
#[async_trait]
impl LspAdapter for EsLintLspAdapter {
- fn workspace_configuration(&self, _: &mut AppContext) -> Option<BoxFuture<'static, Value>> {
- Some(
- future::ready(json!({
- "": {
- "validate": "on",
- "rulesCustomizations": [],
- "run": "onType",
- "nodePath": null,
- }
- }))
- .boxed(),
- )
+ fn workspace_configuration(&self, _: &mut AppContext) -> BoxFuture<'static, Value> {
+ future::ready(json!({
+ "": {
+ "validate": "on",
+ "rulesCustomizations": [],
+ "run": "onType",
+ "nodePath": null,
+ }
+ }))
+ .boxed()
}
async fn name(&self) -> LanguageServerName {
LanguageServerName("eslint".into())
}
+ fn short_name(&self) -> &'static str {
+ "eslint"
+ }
+
async fn fetch_latest_server_version(
&self,
delegate: &dyn LspAdapterDelegate,
@@ -40,6 +40,10 @@ impl LspAdapter for YamlLspAdapter {
LanguageServerName("yaml-language-server".into())
}
+ fn short_name(&self) -> &'static str {
+ "yaml"
+ }
+
async fn fetch_latest_server_version(
&self,
_: &dyn LspAdapterDelegate,
@@ -86,21 +90,20 @@ impl LspAdapter for YamlLspAdapter {
) -> Option<LanguageServerBinary> {
get_cached_server_binary(container_dir, &self.node).await
}
- fn workspace_configuration(&self, cx: &mut AppContext) -> Option<BoxFuture<'static, Value>> {
+ fn workspace_configuration(&self, cx: &mut AppContext) -> BoxFuture<'static, Value> {
let tab_size = all_language_settings(None, cx)
.language(Some("YAML"))
.tab_size;
- Some(
- future::ready(serde_json::json!({
- "yaml": {
- "keyOrdering": false
- },
- "[yaml]": {
- "editor.tabSize": tab_size,
- }
- }))
- .boxed(),
- )
+
+ future::ready(serde_json::json!({
+ "yaml": {
+ "keyOrdering": false
+ },
+ "[yaml]": {
+ "editor.tabSize": tab_size,
+ }
+ }))
+ .boxed()
}
}
@@ -206,6 +206,9 @@ 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,
},
diagnostic_header: {
background: background(theme.middle),