#![doc = include_str!("../readme.md")]
pub mod colors;
use dashmap::DashMap;
use fancy_regex::Regex;
use tower_lsp::Client;
use tower_lsp::LanguageServer;
use tower_lsp::jsonrpc::Error;
use tower_lsp::jsonrpc::Result;
#[allow(clippy::wildcard_imports, reason = "there is a ton of types")]
use tower_lsp::lsp_types::*;
const INDENTED_HASH_REGEX: &str = r"^\s+#";
const VARIABLE_REGEX: &str = r#"^([a-zA-Z_]+)\s*=\s*(["'])\#([0-9a-fA-F]{8}|[0-9a-fA-F]{6}|[0-9a-fA-F]{4}|[0-9a-fA-F]{3})\2"#;
#[derive(Debug)]
pub struct Backend {
pub client: Client,
pub documents: DashMap<Url, String>,
pub color_regex: Regex,
pub variable_completions: bool,
pub color_completions: Option<Vec<CompletionItem>>,
}
#[tower_lsp::async_trait]
impl LanguageServer for Backend {
async fn initialize(&self, _: InitializeParams) -> Result<InitializeResult> {
Ok(InitializeResult {
capabilities: ServerCapabilities {
text_document_sync: Some(TextDocumentSyncKind::FULL.into()),
color_provider: Some(ColorProviderCapability::Simple(true)),
completion_provider: Some(CompletionOptions {
trigger_characters: Some(vec!["#".to_string()]),
..CompletionOptions::default()
}),
..Default::default()
},
..Default::default()
})
}
async fn initialized(&self, _: InitializedParams) {
self.client
.log_message(MessageType::INFO, "server initialized")
.await;
}
async fn shutdown(&self) -> Result<()> {
Ok(())
}
async fn did_open(&self, params: DidOpenTextDocumentParams) {
let uri = params.text_document.uri;
let text = params.text_document.text;
self.documents.insert(uri, text);
}
async fn did_change(&self, params: DidChangeTextDocumentParams) {
let uri = params.text_document.uri;
let docs = &self.documents;
if let Some(mut text) = docs.get_mut(&uri) {
text.clone_from(¶ms.content_changes[0].text);
}
}
async fn completion(&self, params: CompletionParams) -> Result<Option<CompletionResponse>> {
let Self {
color_completions,
variable_completions,
..
} = &self;
let Position {
line: line_pos,
character: char_pos,
} = params.text_document_position.position;
let Some(context) = params.context else {
return Ok(None);
};
if char_pos <= 1 {
return Ok(None);
}
let uri = params.text_document_position.text_document.uri;
let Some(document) = &self.documents.get(&uri) else {
return Ok(None);
};
if let Some(completions) = color_completions {
if Some("#".to_string()) == context.trigger_character {
let indented_hash_regex =
Regex::new(INDENTED_HASH_REGEX).expect("perfectly valid regex");
let line = document.lines().collect::<Vec<&str>>()[line_pos as usize];
if !indented_hash_regex
.is_match(line)
.expect("perfectly valid regex")
{
return Ok(Some(CompletionResponse::Array(completions.clone())));
}
}
}
if *variable_completions && context.trigger_character.is_none() {
let bindings = Regex::new(VARIABLE_REGEX).expect("perfectly valid regex");
let mut completions: Vec<CompletionItem> = vec![];
document.lines().for_each(|line| {
if let Some(captures) = bindings.captures(line).unwrap() {
let (variable_name, color_match) = (
captures.get(1).unwrap().as_str(),
captures.get(3).unwrap().as_str(),
);
completions.push(CompletionItem {
kind: Some(CompletionItemKind::COLOR),
documentation: Some(Documentation::String(format!("#{color_match}"))),
sort_text: Some(variable_name.to_owned()),
insert_text: Some(variable_name.to_owned()),
label: variable_name.to_owned(),
..CompletionItem::default()
});
}
});
return Ok(Some(CompletionResponse::Array(completions)));
}
Ok(None)
}
async fn document_color(&self, params: DocumentColorParams) -> Result<Vec<ColorInformation>> {
let uri = params.text_document.uri;
let docs = &self.documents;
let colors: Vec<ColorInformation> = docs
.get(&uri)
.ok_or_else(|| Error::invalid_params("document not found"))?
.lines()
.enumerate()
.flat_map(|(line_num, line)| {
colors::colors_in_line_iter(&self.color_regex, line_num, line)
})
.collect();
Ok(colors)
}
}