Tailwind autocomplete (#2920)

Release Notes:
- Added basic Tailwind CSS autocomplete support
([#746](https://github.com/zed-industries/community/issues/746)).
This commit is contained in:
Kirill Bulatov 2023-08-31 16:55:46 +03:00 committed by GitHub
commit ddc6214216
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
43 changed files with 1075 additions and 280 deletions

View file

@ -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",

View file

@ -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,

View file

@ -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()
}

View file

@ -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 = ["-"]

View file

@ -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>,

View file

@ -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,

View file

@ -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,

View file

@ -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 = ["-"]

View file

@ -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"]

View file

@ -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> {

View file

@ -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,

View file

@ -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,

View file

@ -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,

View file

@ -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,

View file

@ -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,

View file

@ -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,

View file

@ -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,

View file

@ -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()
}

View file

@ -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"]

View file

@ -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,

View file

@ -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()
}
}