swt 5.0.0-rc.5

🍬 Sweet: A blazing-fast code health and architecture analyzer.
Documentation
//! Backend implementation for the Sweet Language Server.

pub mod diag;

use dashmap::DashMap;
use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::Arc;
use std::time::Duration;
use swt::Config;
use swt::analyzer::analyze_content;
use swt::analyzer::ignore::get_disabled_rules;
use swt::languages::{Language, LanguageRegistry};
use tokio::sync::RwLock;
use tokio::task::JoinHandle;
use tokio::time::sleep;
use tower_lsp::jsonrpc::Result;
use tower_lsp::lsp_types::{
    CodeAction, CodeActionKind, CodeActionOrCommand, CodeActionParams,
    CodeActionProviderCapability, CodeActionResponse, DidChangeTextDocumentParams,
    DidOpenTextDocumentParams, InitializeParams, InitializeResult, InitializedParams, MessageType,
    Position, Range, ServerCapabilities, TextDocumentSyncCapability, TextDocumentSyncKind,
    TextEdit, Url, WorkspaceEdit,
};
use tower_lsp::{Client, LanguageServer};

#[derive(Debug)]
pub struct Backend {
    pub client: Client,
    pub workspace_root: Arc<RwLock<Option<PathBuf>>>,
    pub pending_validations: Arc<DashMap<Url, JoinHandle<()>>>,
}

impl Backend {
    pub fn new(client: Client) -> Self {
        Self {
            client,
            workspace_root: Arc::new(RwLock::new(None)),
            pending_validations: Arc::new(DashMap::new()),
        }
    }

    pub async fn validate_document(&self, uri: Url, content: String) {
        if let Some(report) = self.perform_analysis(uri.clone(), content).await {
            let config = Config::load(&uri.to_file_path().unwrap_or_default()).unwrap_or_default();
            let diagnostics = diag::generate(&report, &config);
            self.client
                .publish_diagnostics(uri, diagnostics, None)
                .await;
        }
    }

    async fn perform_analysis(&self, uri: Url, content: String) -> Option<swt::FileReport> {
        let path = uri.to_file_path().ok()?;

        if !Config::is_supported_file(&path) {
            return None;
        }

        {
            let root_lock = self.workspace_root.read().await;
            if let Some(ref root) = *root_lock
                && !path.starts_with(root)
            {
                return None;
            }
        }

        let config = Config::load(&path).unwrap_or_default();
        if config.is_excluded(&path) {
            return None;
        }

        let extension = path.extension()?.to_str()?;
        let thresholds = config.get_thresholds(extension);
        let disabled_rules = get_disabled_rules(content.as_bytes());

        Some(analyze_content(
            content.as_bytes(),
            extension,
            &thresholds,
            &path,
            &config,
            &disabled_rules,
            true,
        ))
    }
}

#[tower_lsp::async_trait]
impl LanguageServer for Backend {
    async fn initialize(&self, params: InitializeParams) -> Result<InitializeResult> {
        if let Some(root_uri) = params.root_uri
            && let Ok(root_path) = root_uri.to_file_path()
        {
            *self.workspace_root.write().await = Some(root_path);
        }
        Ok(InitializeResult {
            capabilities: ServerCapabilities {
                text_document_sync: Some(TextDocumentSyncCapability::Kind(
                    TextDocumentSyncKind::FULL,
                )),
                code_action_provider: Some(CodeActionProviderCapability::Simple(true)),
                ..Default::default()
            },
            ..Default::default()
        })
    }

    async fn initialized(&self, _: InitializedParams) {
        self.client
            .log_message(MessageType::INFO, "Sweet LSP server initialized!")
            .await;
    }

    async fn did_open(&self, params: DidOpenTextDocumentParams) {
        self.validate_document(params.text_document.uri, params.text_document.text)
            .await;
    }

    async fn did_change(&self, params: DidChangeTextDocumentParams) {
        let uri = params.text_document.uri;
        let Some(change) = params.content_changes.into_iter().next() else {
            return;
        };

        if let Some((_, handle)) = self.pending_validations.remove(&uri) {
            handle.abort();
        }

        let client = self.client.clone();
        let workspace_root = self.workspace_root.clone();
        let pending_validations = self.pending_validations.clone();
        let uri_clone = uri.clone();
        let content = change.text;

        let handle = tokio::spawn(async move {
            sleep(Duration::from_millis(300)).await;

            let path = uri_clone.to_file_path().unwrap_or_default();
            if !Config::is_supported_file(&path) {
                return;
            }

            {
                let root_lock = workspace_root.read().await;
                if let Some(ref root) = *root_lock
                    && !path.starts_with(root)
                {
                    return;
                }
            }

            let config = Config::load(&path).unwrap_or_default();
            if config.is_excluded(&path) {
                return;
            }

            let extension = path
                .extension()
                .and_then(|e| e.to_str())
                .unwrap_or_default();
            let thresholds = config.get_thresholds(extension);
            let disabled_rules = get_disabled_rules(content.as_bytes());
            let report = analyze_content(
                content.as_bytes(),
                extension,
                &thresholds,
                &path,
                &config,
                &disabled_rules,
                true,
            );

            let diagnostics = diag::generate(&report, &config);
            let () = client
                .publish_diagnostics(uri_clone.clone(), diagnostics, None)
                .await;

            pending_validations.remove(&uri_clone);
        });

        self.pending_validations.insert(uri, handle);
    }

    async fn code_action(&self, params: CodeActionParams) -> Result<Option<CodeActionResponse>> {
        let mut actions = Vec::new();
        let Ok(path) = params.text_document.uri.to_file_path() else {
            return Ok(None);
        };
        let extension = path
            .extension()
            .and_then(|e| e.to_str())
            .unwrap_or_default();
        let registry = LanguageRegistry::get();
        let comment = registry
            .get_by_extension(extension)
            .and_then(Language::line_comment)
            .unwrap_or("//");

        for diagnostic in params.context.diagnostics {
            if let Some(rule) = diagnostic.data.as_ref().and_then(|v| v.as_str()) {
                if rule == "unknown" {
                    continue;
                }
                let title = format!("🍬 Disable rule '{rule}' for this file");
                let edit = TextEdit::new(
                    Range::new(Position::new(0, 0), Position::new(0, 0)),
                    format!("{comment} @swt-disable {rule}\n"),
                );
                let mut changes = HashMap::new();
                changes.insert(params.text_document.uri.clone(), vec![edit]);
                actions.push(CodeActionOrCommand::CodeAction(CodeAction {
                    title,
                    kind: Some(CodeActionKind::QUICKFIX),
                    edit: Some(WorkspaceEdit {
                        changes: Some(changes),
                        ..Default::default()
                    }),
                    diagnostics: Some(vec![diagnostic]),
                    ..Default::default()
                }));
            }
        }
        Ok(Some(actions))
    }

    async fn shutdown(&self) -> Result<()> {
        Ok(())
    }
}