//! The `language` crate provides a large chunk of Zed's language-related //! features (the other big contributors being project and lsp crates that revolve around LSP features). //! Namely, this crate: //! - Provides [`Language`], [`Grammar`] and [`LanguageRegistry`] types that //! use Tree-sitter to provide syntax highlighting to the editor; note though that `language` doesn't perform the highlighting by itself. It only maps ranges in a buffer to colors. Treesitter is also used for buffer outlines (lists of symbols in a buffer) //! - Exposes [`LanguageConfig`] that describes how constructs (like brackets or line comments) should be handled by the editor for a source file of a particular language. //! //! Notably we do *not* assign a single language to a single file; in real world a single file can consist of multiple programming languages - HTML is a good example of that - and `language` crate tends to reflect that status quo in its API. mod buffer; mod diagnostic; mod diagnostic_set; mod file_content; mod language_registry; pub mod language_settings; mod manifest; pub mod modeline; mod outline; pub mod proto; mod syntax_map; mod task_context; mod text_diff; mod toolchain; #[cfg(test)] pub mod buffer_tests; pub use crate::language_settings::{AutoIndentMode, EditPredictionsMode, IndentGuideSettings}; use anyhow::{Context as _, Result}; use async_trait::async_trait; use collections::{HashMap, HashSet}; use futures::Future; use futures::future::LocalBoxFuture; use futures::lock::OwnedMutexGuard; use gpui::{App, AsyncApp, Entity}; use http_client::HttpClient; pub use language_core::highlight_map::{HighlightId, HighlightMap}; use futures::future::FutureExt as _; pub use language_core::{ BlockCommentConfig, BracketPair, BracketPairConfig, BracketPairContent, BracketsConfig, BracketsPatternConfig, CodeLabel, CodeLabelBuilder, DebugVariablesConfig, DebuggerTextObject, DecreaseIndentConfig, Grammar, GrammarId, HighlightsConfig, IndentConfig, InjectionConfig, InjectionPatternConfig, JsxTagAutoCloseConfig, LanguageConfig, LanguageConfigOverride, LanguageId, LanguageMatcher, OrderedListConfig, OutlineConfig, Override, OverrideConfig, OverrideEntry, PromptResponseContext, RedactionConfig, RunnableCapture, RunnableConfig, SoftWrap, Symbol, TaskListConfig, TextObject, TextObjectConfig, ToLspPosition, WrapCharactersConfig, auto_indent_using_last_non_empty_line_default, deserialize_regex, deserialize_regex_vec, regex_json_schema, regex_vec_json_schema, serialize_regex, }; pub use language_registry::{ LanguageName, LanguageServerStatusUpdate, LoadedLanguage, ServerHealth, }; use lsp::{ CodeActionKind, InitializeParams, LanguageServerBinary, LanguageServerBinaryOptions, Uri, }; pub use manifest::{ManifestDelegate, ManifestName, ManifestProvider, ManifestQuery}; pub use modeline::{ModelineSettings, parse_modeline}; use parking_lot::Mutex; use regex::Regex; use semver::Version; use serde_json::Value; use settings::WorktreeId; use std::{ ffi::OsStr, fmt::Debug, hash::Hash, mem, ops::{DerefMut, Range}, path::{Path, PathBuf}, str, sync::{Arc, LazyLock}, }; use syntax_map::{QueryCursorHandle, SyntaxSnapshot}; use task::RunnableTag; pub use task_context::{ContextLocation, ContextProvider, RunnableRange}; pub use text_diff::{ DiffOptions, apply_diff_patch, apply_reversed_diff_patch, char_diff, line_diff, text_diff, text_diff_with_options, unified_diff, unified_diff_with_context, unified_diff_with_offsets, word_diff_ranges, }; use theme::SyntaxTheme; pub use toolchain::{ LanguageToolchainStore, LocalLanguageToolchainStore, Toolchain, ToolchainList, ToolchainLister, ToolchainMetadata, ToolchainScope, }; use tree_sitter::{self, QueryCursor, WasmStore, wasmtime}; use util::rel_path::RelPath; pub use buffer::Operation; pub use buffer::*; pub use diagnostic::{Diagnostic, DiagnosticSourceKind}; pub use diagnostic_set::{DiagnosticEntry, DiagnosticEntryRef, DiagnosticGroup}; pub use file_content::{ByteContent, FILE_ANALYSIS_BYTES, analyze_byte_content}; pub use language_registry::{ AvailableLanguage, BinaryStatus, LanguageNotFound, LanguageQueries, LanguageRegistry, QUERY_FILENAME_PREFIXES, }; pub use lsp::{LanguageServerId, LanguageServerName}; pub use outline::*; pub use syntax_map::{ OwnedSyntaxLayer, SyntaxLayer, SyntaxMapMatches, ToTreeSitterPoint, TreeSitterOptions, }; pub use text::{AnchorRangeExt, LineEnding}; pub use tree_sitter::{Node, Parser, Tree, TreeCursor}; pub(crate) fn to_settings_soft_wrap(value: language_core::SoftWrap) -> settings::SoftWrap { match value { language_core::SoftWrap::None => settings::SoftWrap::None, language_core::SoftWrap::PreferLine => settings::SoftWrap::PreferLine, language_core::SoftWrap::EditorWidth => settings::SoftWrap::EditorWidth, language_core::SoftWrap::Bounded => settings::SoftWrap::Bounded, } } static QUERY_CURSORS: Mutex> = Mutex::new(vec![]); static PARSERS: Mutex> = Mutex::new(vec![]); #[ztracing::instrument(skip_all)] pub fn with_parser(func: F) -> R where F: FnOnce(&mut Parser) -> R, { let mut parser = PARSERS.lock().pop().unwrap_or_else(|| { let mut parser = Parser::new(); parser .set_wasm_store(WasmStore::new(&WASM_ENGINE).unwrap()) .unwrap(); parser }); // Tree-sitter auto-resets the parser at the end of a successful parse, // but the cancellation paths (progress callback returning `Break`, // cancelled balancing) leave outstanding state on the parser. The next // call to `parse_with_options` would then *resume* that cancelled parse // instead of starting fresh. parser.reset(); parser.set_included_ranges(&[]).unwrap(); let result = func(&mut parser); PARSERS.lock().push(parser); result } pub fn with_query_cursor(func: F) -> R where F: FnOnce(&mut QueryCursor) -> R, { let mut cursor = QueryCursorHandle::new(); func(cursor.deref_mut()) } static WASM_ENGINE: LazyLock = LazyLock::new(|| { wasmtime::Engine::new(&wasmtime::Config::new()).expect("Failed to create Wasmtime engine") }); /// A shared grammar for plain text, exposed for reuse by downstream crates. pub static PLAIN_TEXT: LazyLock> = LazyLock::new(|| { Arc::new(Language::new( LanguageConfig { name: "Plain Text".into(), soft_wrap: Some(SoftWrap::EditorWidth), matcher: LanguageMatcher { path_suffixes: vec!["txt".to_owned()], first_line_pattern: None, modeline_aliases: vec!["text".to_owned(), "txt".to_owned()], }, brackets: BracketPairConfig { pairs: vec![ BracketPair { start: "(".to_string(), end: ")".to_string(), close: true, surround: true, newline: false, }, BracketPair { start: "[".to_string(), end: "]".to_string(), close: true, surround: true, newline: false, }, BracketPair { start: "{".to_string(), end: "}".to_string(), close: true, surround: true, newline: false, }, BracketPair { start: "\"".to_string(), end: "\"".to_string(), close: true, surround: true, newline: false, }, BracketPair { start: "'".to_string(), end: "'".to_string(), close: true, surround: true, newline: false, }, ], disabled_scopes_by_bracket_ix: Default::default(), }, ..Default::default() }, None, )) }); /// Commands that the client (editor) handles locally rather than forwarding /// to the language server. Servers embed these in code lens and code action /// responses when they want the editor to perform a well-known UI action. #[derive(Debug, Clone)] pub enum ClientCommand { /// Open a location list (references panel / peek view). ShowLocations, /// Schedule a task from an LSP command's arguments. ScheduleTask(task::TaskTemplate), } #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct Location { pub buffer: Entity, pub range: Range, } type ServerBinaryCache = futures::lock::Mutex>; type DownloadableLanguageServerBinary = LocalBoxFuture<'static, Result>; pub type LanguageServerBinaryLocations = LocalBoxFuture< 'static, ( Result, Option, ), >; /// Represents a Language Server, with certain cached sync properties. /// Uses [`LspAdapter`] under the hood, but calls all 'static' methods /// once at startup, and caches the results. pub struct CachedLspAdapter { pub name: LanguageServerName, pub disk_based_diagnostic_sources: Vec, pub disk_based_diagnostics_progress_token: Option, language_ids: HashMap, pub adapter: Arc, cached_binary: Arc, } impl Debug for CachedLspAdapter { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("CachedLspAdapter") .field("name", &self.name) .field( "disk_based_diagnostic_sources", &self.disk_based_diagnostic_sources, ) .field( "disk_based_diagnostics_progress_token", &self.disk_based_diagnostics_progress_token, ) .field("language_ids", &self.language_ids) .finish_non_exhaustive() } } impl CachedLspAdapter { pub fn new(adapter: Arc) -> Arc { let name = adapter.name(); let disk_based_diagnostic_sources = adapter.disk_based_diagnostic_sources(); let disk_based_diagnostics_progress_token = adapter.disk_based_diagnostics_progress_token(); let language_ids = adapter.language_ids(); Arc::new(CachedLspAdapter { name, disk_based_diagnostic_sources, disk_based_diagnostics_progress_token, language_ids, adapter, cached_binary: Default::default(), }) } pub fn name(&self) -> LanguageServerName { self.adapter.name() } pub async fn get_language_server_command( self: Arc, delegate: Arc, toolchains: Option, binary_options: LanguageServerBinaryOptions, cx: &mut AsyncApp, ) -> LanguageServerBinaryLocations { let cached_binary = self.cached_binary.clone().lock_owned().await; self.adapter.clone().get_language_server_command( delegate, toolchains, binary_options, cached_binary, cx.clone(), ) } pub fn code_action_kinds(&self) -> Option> { self.adapter.code_action_kinds() } pub fn process_diagnostics( &self, params: &mut lsp::PublishDiagnosticsParams, server_id: LanguageServerId, ) { self.adapter.process_diagnostics(params, server_id) } pub fn retain_old_diagnostic(&self, previous_diagnostic: &Diagnostic) -> bool { self.adapter.retain_old_diagnostic(previous_diagnostic) } pub fn underline_diagnostic(&self, diagnostic: &lsp::Diagnostic) -> bool { self.adapter.underline_diagnostic(diagnostic) } pub fn diagnostic_message_to_markdown(&self, message: &str) -> Option { self.adapter.diagnostic_message_to_markdown(message) } pub async fn process_completions(&self, completion_items: &mut [lsp::CompletionItem]) { self.adapter.process_completions(completion_items).await } pub async fn labels_for_completions( &self, completion_items: &[lsp::CompletionItem], language: &Arc, ) -> Result>> { self.adapter .clone() .labels_for_completions(completion_items, language) .await } pub async fn labels_for_symbols( &self, symbols: &[Symbol], language: &Arc, ) -> Result>> { self.adapter .clone() .labels_for_symbols(symbols, language) .await } pub fn language_id(&self, language_name: &LanguageName) -> String { self.language_ids .get(language_name) .cloned() .unwrap_or_else(|| language_name.lsp_id()) } pub async fn initialization_options_schema( &self, delegate: &Arc, cx: &mut AsyncApp, ) -> Option { self.adapter .clone() .initialization_options_schema( delegate, self.cached_binary.clone().lock_owned().await, cx, ) .await } pub async fn settings_schema( &self, delegate: &Arc, cx: &mut AsyncApp, ) -> Option { self.adapter .clone() .settings_schema(delegate, self.cached_binary.clone().lock_owned().await, cx) .await } pub fn process_prompt_response(&self, context: &PromptResponseContext, cx: &mut AsyncApp) { self.adapter.process_prompt_response(context, cx) } } /// [`LspAdapterDelegate`] allows [`LspAdapter]` implementations to interface with the application // e.g. to display a notification or fetch data from the web. #[async_trait] pub trait LspAdapterDelegate: Send + Sync { fn show_notification(&self, message: &str, cx: &mut App); fn http_client(&self) -> Arc; fn worktree_id(&self) -> WorktreeId; fn worktree_root_path(&self) -> &Path; fn resolve_relative_path(&self, path: PathBuf) -> PathBuf; fn update_status(&self, language: LanguageServerName, status: BinaryStatus); fn registered_lsp_adapters(&self) -> Vec>; async fn language_server_download_dir(&self, name: &LanguageServerName) -> Option>; async fn npm_package_installed_version( &self, package_name: &str, ) -> Result>; async fn which(&self, command: &OsStr) -> Option; async fn shell_env(&self) -> HashMap; async fn read_text_file(&self, path: &RelPath) -> Result; async fn try_exec(&self, binary: LanguageServerBinary) -> Result<()>; } #[async_trait(?Send)] pub trait LspAdapter: 'static + Send + Sync + DynLspInstaller { fn name(&self) -> LanguageServerName; fn process_diagnostics(&self, _: &mut lsp::PublishDiagnosticsParams, _: LanguageServerId) {} /// When processing new `lsp::PublishDiagnosticsParams` diagnostics, whether to retain previous one(s) or not. fn retain_old_diagnostic(&self, _previous_diagnostic: &Diagnostic) -> bool { false } /// Whether to underline a given diagnostic or not, when rendering in the editor. /// /// https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#diagnosticTag /// states that /// > Clients are allowed to render diagnostics with this tag faded out instead of having an error squiggle. /// for the unnecessary diagnostics, so do not underline them. fn underline_diagnostic(&self, _diagnostic: &lsp::Diagnostic) -> bool { true } /// Post-processes completions provided by the language server. async fn process_completions(&self, _: &mut [lsp::CompletionItem]) {} fn diagnostic_message_to_markdown(&self, _message: &str) -> Option { None } async fn labels_for_completions( self: Arc, completions: &[lsp::CompletionItem], language: &Arc, ) -> Result>> { let mut labels = Vec::new(); for (ix, completion) in completions.iter().enumerate() { let label = self.label_for_completion(completion, language).await; if let Some(label) = label { labels.resize(ix + 1, None); *labels.last_mut().unwrap() = Some(label); } } Ok(labels) } async fn label_for_completion( &self, _: &lsp::CompletionItem, _: &Arc, ) -> Option { None } async fn labels_for_symbols( self: Arc, symbols: &[Symbol], language: &Arc, ) -> Result>> { let mut labels = Vec::new(); for (ix, symbol) in symbols.iter().enumerate() { let label = self.label_for_symbol(symbol, language).await; if let Some(label) = label { labels.resize(ix + 1, None); *labels.last_mut().unwrap() = Some(label); } } Ok(labels) } async fn label_for_symbol( &self, _symbol: &Symbol, _language: &Arc, ) -> Option { None } /// Returns initialization options that are going to be sent to a LSP server as a part of [`lsp::InitializeParams`] async fn initialization_options( self: Arc, _: &Arc, _cx: &mut AsyncApp, ) -> Result> { Ok(None) } /// Returns the JSON schema of the initialization_options for the language server. async fn initialization_options_schema( self: Arc, _delegate: &Arc, _cached_binary: OwnedMutexGuard>, _cx: &mut AsyncApp, ) -> Option { None } /// Returns the JSON schema of the settings for the language server. /// This corresponds to the `settings` field in `LspSettings`, which is used /// to respond to `workspace/configuration` requests from the language server. async fn settings_schema( self: Arc, _delegate: &Arc, _cached_binary: OwnedMutexGuard>, _cx: &mut AsyncApp, ) -> Option { None } async fn workspace_configuration( self: Arc, _: &Arc, _: Option, _: Option, _cx: &mut AsyncApp, ) -> Result { Ok(serde_json::json!({})) } async fn additional_initialization_options( self: Arc, _target_language_server_id: LanguageServerName, _: &Arc, ) -> Result> { Ok(None) } async fn additional_workspace_configuration( self: Arc, _target_language_server_id: LanguageServerName, _: &Arc, _cx: &mut AsyncApp, ) -> Result> { Ok(None) } /// Returns a list of code actions supported by a given LspAdapter fn code_action_kinds(&self) -> Option> { None } fn disk_based_diagnostic_sources(&self) -> Vec { Default::default() } fn disk_based_diagnostics_progress_token(&self) -> Option { None } fn language_ids(&self) -> HashMap { HashMap::default() } /// Support custom initialize params. fn prepare_initialize_params( &self, original: InitializeParams, _: &App, ) -> Result { Ok(original) } fn client_command( &self, _command_name: &str, _arguments: &[serde_json::Value], ) -> Option { None } /// Method only implemented by the default JSON language server adapter. /// Used to provide dynamic reloading of the JSON schemas used to /// provide autocompletion and diagnostics in Zed setting and keybind /// files fn is_primary_zed_json_schema_adapter(&self) -> bool { false } /// True for the extension adapter and false otherwise. fn is_extension(&self) -> bool { false } /// Called when a user responds to a ShowMessageRequest from this language server. /// This allows adapters to intercept preference selections (like "Always" or "Never") /// for settings that should be persisted to Zed's settings file. fn process_prompt_response(&self, _context: &PromptResponseContext, _cx: &mut AsyncApp) {} } pub trait LspInstaller { type BinaryVersion; fn check_if_user_installed( &self, _: &dyn LspAdapterDelegate, _: Option, _: &AsyncApp, ) -> impl Future> { async { None } } fn fetch_latest_server_version( &self, delegate: &dyn LspAdapterDelegate, pre_release: bool, cx: &mut AsyncApp, ) -> impl Future>; fn check_if_version_installed( &self, _version: &Self::BinaryVersion, _container_dir: &PathBuf, _delegate: &dyn LspAdapterDelegate, ) -> impl Send + Future> { async { None } } fn fetch_server_binary( &self, latest_version: Self::BinaryVersion, container_dir: PathBuf, delegate: &dyn LspAdapterDelegate, ) -> impl Send + Future>; fn cached_server_binary( &self, container_dir: PathBuf, delegate: &dyn LspAdapterDelegate, ) -> impl Future>; } #[async_trait(?Send)] pub trait DynLspInstaller { async fn try_fetch_server_binary( &self, delegate: &Arc, container_dir: PathBuf, pre_release: bool, cx: &mut AsyncApp, ) -> Result; fn get_language_server_command( self: Arc, delegate: Arc, toolchains: Option, binary_options: LanguageServerBinaryOptions, cached_binary: OwnedMutexGuard>, cx: AsyncApp, ) -> LanguageServerBinaryLocations; } #[async_trait(?Send)] impl DynLspInstaller for LI where BinaryVersion: Send + Sync, LI: LspInstaller + LspAdapter, { async fn try_fetch_server_binary( &self, delegate: &Arc, container_dir: PathBuf, pre_release: bool, cx: &mut AsyncApp, ) -> Result { let name = self.name(); log::debug!("fetching latest version of language server {:?}", name.0); delegate.update_status(name.clone(), BinaryStatus::CheckingForUpdate); let latest_version = self .fetch_latest_server_version(delegate.as_ref(), pre_release, cx) .await?; if let Some(binary) = cx .background_executor() .await_on_background(self.check_if_version_installed( &latest_version, &container_dir, delegate.as_ref(), )) .await { log::debug!("language server {:?} is already installed", name.0); delegate.update_status(name.clone(), BinaryStatus::None); Ok(binary) } else { log::debug!("downloading language server {:?}", name.0); delegate.update_status(name.clone(), BinaryStatus::Downloading); let binary = cx .background_executor() .await_on_background(self.fetch_server_binary( latest_version, container_dir, delegate.as_ref(), )) .await; delegate.update_status(name.clone(), BinaryStatus::None); binary } } fn get_language_server_command( self: Arc, delegate: Arc, toolchain: Option, binary_options: LanguageServerBinaryOptions, mut cached_binary: OwnedMutexGuard>, mut cx: AsyncApp, ) -> LanguageServerBinaryLocations { async move { let cached_binary_deref = cached_binary.deref_mut(); // First we check whether the adapter can give us a user-installed binary. // If so, we do *not* want to cache that, because each worktree might give us a different // binary: // // worktree 1: user-installed at `.bin/gopls` // worktree 2: user-installed at `~/bin/gopls` // worktree 3: no gopls found in PATH -> fallback to Zed installation // // We only want to cache when we fall back to the global one, // because we don't want to download and overwrite our global one // for each worktree we might have open. if binary_options.allow_path_lookup && let Some(binary) = self .check_if_user_installed(delegate.as_ref(), toolchain, &mut cx) .await { log::info!( "found user-installed language server for {}. path: {:?}, arguments: {:?}", self.name().0, binary.path, binary.arguments ); return (Ok(binary), None); } if let Some((pre_release, cached_binary)) = cached_binary_deref && *pre_release == binary_options.pre_release { return (Ok(cached_binary.clone()), None); } if !binary_options.allow_binary_download { return ( Err(anyhow::anyhow!("downloading language servers disabled")), None, ); } let Some(container_dir) = delegate.language_server_download_dir(&self.name()).await else { return ( Err(anyhow::anyhow!("no language server download dir defined")), None, ); }; let last_downloaded_binary = self .cached_server_binary(container_dir.to_path_buf(), delegate.as_ref()) .await .context( "did not find existing language server binary, falling back to downloading", ); let download_binary = async move { let mut binary = self .try_fetch_server_binary( &delegate, container_dir.to_path_buf(), binary_options.pre_release, &mut cx, ) .await; if let Err(error) = binary.as_ref() { if let Some(prev_downloaded_binary) = self .cached_server_binary(container_dir.to_path_buf(), delegate.as_ref()) .await { log::info!( "failed to fetch newest version of language server {:?}. \ error: {:?}, falling back to using {:?}", self.name(), error, prev_downloaded_binary.path ); binary = Ok(prev_downloaded_binary); } else { delegate.update_status( self.name(), BinaryStatus::Failed { error: format!("{error:?}"), }, ); } } if let Ok(binary) = &binary { *cached_binary = Some((binary_options.pre_release, binary.clone())); } binary } .boxed_local(); (last_downloaded_binary, Some(download_binary)) } .boxed_local() } } /// Represents a language for the given range. Some languages (e.g. HTML) /// interleave several languages together, thus a single buffer might actually contain /// several nested scopes. #[derive(Clone, Debug)] pub struct LanguageScope { language: Arc, override_id: Option, } #[doc(hidden)] #[cfg(any(test, feature = "test-support"))] pub struct FakeLspAdapter { pub name: &'static str, pub initialization_options: Option, pub prettier_plugins: Vec<&'static str>, pub disk_based_diagnostics_progress_token: Option, pub disk_based_diagnostics_sources: Vec, pub language_server_binary: LanguageServerBinary, pub capabilities: lsp::ServerCapabilities, pub initializer: Option>, pub label_for_completion: Option< Box< dyn 'static + Send + Sync + Fn(&lsp::CompletionItem, &Arc) -> Option, >, >, } pub struct Language { pub(crate) id: LanguageId, pub(crate) config: LanguageConfig, pub(crate) grammar: Option>, pub(crate) context_provider: Option>, pub(crate) toolchain: Option>, pub(crate) manifest_name: Option, } impl Language { pub fn new(config: LanguageConfig, ts_language: Option) -> Self { Self::new_with_id(LanguageId::new(), config, ts_language) } pub fn id(&self) -> LanguageId { self.id } fn new_with_id( id: LanguageId, config: LanguageConfig, ts_language: Option, ) -> Self { Self { id, config, grammar: ts_language.map(|ts_language| Arc::new(Grammar::new(ts_language))), context_provider: None, toolchain: None, manifest_name: None, } } pub fn with_context_provider(mut self, provider: Option>) -> Self { self.context_provider = provider; self } pub fn with_toolchain_lister(mut self, provider: Option>) -> Self { self.toolchain = provider; self } pub fn with_manifest(mut self, name: Option) -> Self { self.manifest_name = name; self } pub fn with_queries(mut self, queries: LanguageQueries) -> Result { if let Some(grammar) = self.grammar.take() { let grammar = Arc::try_unwrap(grammar).map_err(|_| anyhow::anyhow!("cannot mutate grammar"))?; let grammar = grammar.with_queries(queries, &mut self.config)?; self.grammar = Some(Arc::new(grammar)); } Ok(self) } pub fn with_highlights_query(self, source: &str) -> Result { self.with_grammar_query(|grammar| grammar.with_highlights_query(source)) } pub fn with_runnable_query(self, source: &str) -> Result { self.with_grammar_query(|grammar| grammar.with_runnable_query(source)) } pub fn with_outline_query(self, source: &str) -> Result { self.with_grammar_query_and_name(|grammar, name| grammar.with_outline_query(source, name)) } pub fn with_text_object_query(self, source: &str) -> Result { self.with_grammar_query_and_name(|grammar, name| { grammar.with_text_object_query(source, name) }) } pub fn with_debug_variables_query(self, source: &str) -> Result { self.with_grammar_query_and_name(|grammar, name| { grammar.with_debug_variables_query(source, name) }) } pub fn with_brackets_query(self, source: &str) -> Result { self.with_grammar_query_and_name(|grammar, name| grammar.with_brackets_query(source, name)) } pub fn with_indents_query(self, source: &str) -> Result { self.with_grammar_query_and_name(|grammar, name| grammar.with_indents_query(source, name)) } pub fn with_injection_query(self, source: &str) -> Result { self.with_grammar_query_and_name(|grammar, name| grammar.with_injection_query(source, name)) } pub fn with_override_query(mut self, source: &str) -> Result { if let Some(grammar_arc) = self.grammar.take() { let grammar = Arc::try_unwrap(grammar_arc) .map_err(|_| anyhow::anyhow!("cannot mutate grammar"))?; let grammar = grammar.with_override_query( source, &self.config.name, &self.config.overrides, &mut self.config.brackets, &self.config.scope_opt_in_language_servers, )?; self.grammar = Some(Arc::new(grammar)); } Ok(self) } pub fn with_redaction_query(self, source: &str) -> Result { self.with_grammar_query_and_name(|grammar, name| grammar.with_redaction_query(source, name)) } fn with_grammar_query( mut self, build: impl FnOnce(Grammar) -> Result, ) -> Result { if let Some(grammar_arc) = self.grammar.take() { let grammar = Arc::try_unwrap(grammar_arc) .map_err(|_| anyhow::anyhow!("cannot mutate grammar"))?; self.grammar = Some(Arc::new(build(grammar)?)); } Ok(self) } fn with_grammar_query_and_name( mut self, build: impl FnOnce(Grammar, &LanguageName) -> Result, ) -> Result { if let Some(grammar_arc) = self.grammar.take() { let grammar = Arc::try_unwrap(grammar_arc) .map_err(|_| anyhow::anyhow!("cannot mutate grammar"))?; self.grammar = Some(Arc::new(build(grammar, &self.config.name)?)); } Ok(self) } pub fn name(&self) -> LanguageName { self.config.name.clone() } pub fn manifest(&self) -> Option<&ManifestName> { self.manifest_name.as_ref() } pub fn code_fence_block_name(&self) -> Arc { self.config .code_fence_block_name .clone() .unwrap_or_else(|| self.config.name.as_ref().to_lowercase().into()) } pub fn matches_kernel_language(&self, kernel_language: &str) -> bool { let kernel_language_lower = kernel_language.to_lowercase(); if self.code_fence_block_name().to_lowercase() == kernel_language_lower { return true; } if self.config.name.as_ref().to_lowercase() == kernel_language_lower { return true; } self.config .kernel_language_names .iter() .any(|name| name.to_lowercase() == kernel_language_lower) } pub fn context_provider(&self) -> Option> { self.context_provider.clone() } pub fn toolchain_lister(&self) -> Option> { self.toolchain.clone() } pub fn highlight_text<'a>( self: &'a Arc, text: &'a Rope, range: Range, ) -> Vec<(Range, HighlightId)> { let mut result = Vec::new(); if let Some(grammar) = &self.grammar { let tree = parse_text(grammar, text, None); let captures = SyntaxSnapshot::single_tree_captures(range.clone(), text, &tree, self, |grammar| { grammar .highlights_config .as_ref() .map(|config| &config.query) }); let highlight_maps = vec![grammar.highlight_map()]; let mut offset = 0; for chunk in BufferChunks::new(text, range, Some((captures, highlight_maps)), false, None) { let end_offset = offset + chunk.text.len(); if let Some(highlight_id) = chunk.syntax_highlight_id { result.push((offset..end_offset, highlight_id)); } offset = end_offset; } } result } pub fn path_suffixes(&self) -> &[String] { &self.config.matcher.path_suffixes } pub fn should_autoclose_before(&self, c: char) -> bool { c.is_whitespace() || self.config.autoclose_before.contains(c) } pub fn set_theme(&self, theme: &SyntaxTheme) { if let Some(grammar) = self.grammar.as_ref() && let Some(highlights_config) = &grammar.highlights_config { *grammar.highlight_map.lock() = build_highlight_map(highlights_config.query.capture_names(), theme); } } pub fn grammar(&self) -> Option<&Arc> { self.grammar.as_ref() } pub fn default_scope(self: &Arc) -> LanguageScope { LanguageScope { language: self.clone(), override_id: None, } } pub fn lsp_id(&self) -> String { self.config.name.lsp_id() } pub fn prettier_parser_name(&self) -> Option<&str> { self.config.prettier_parser_name.as_deref() } pub fn config(&self) -> &LanguageConfig { &self.config } } #[inline] pub fn build_highlight_map(capture_names: &[&str], theme: &SyntaxTheme) -> HighlightMap { HighlightMap::from_ids( capture_names .iter() .map(|capture_name| theme.highlight_id(capture_name).map(HighlightId::new)), ) } impl LanguageScope { pub fn path_suffixes(&self) -> &[String] { self.language.path_suffixes() } pub fn language_name(&self) -> LanguageName { self.language.config.name.clone() } pub fn collapsed_placeholder(&self) -> &str { self.language.config.collapsed_placeholder.as_ref() } /// Returns line prefix that is inserted in e.g. line continuations or /// in `toggle comments` action. pub fn line_comment_prefixes(&self) -> &[Arc] { Override::as_option( self.config_override().map(|o| &o.line_comments), Some(&self.language.config.line_comments), ) .map_or([].as_slice(), |e| e.as_slice()) } /// Config for block comments for this language. pub fn block_comment(&self) -> Option<&BlockCommentConfig> { Override::as_option( self.config_override().map(|o| &o.block_comment), self.language.config.block_comment.as_ref(), ) } /// Config for documentation-style block comments for this language. pub fn documentation_comment(&self) -> Option<&BlockCommentConfig> { self.language.config.documentation_comment.as_ref() } /// Returns list markers that are inserted unchanged on newline (e.g., `- `, `* `, `+ `). pub fn unordered_list(&self) -> &[Arc] { &self.language.config.unordered_list } /// Returns configuration for ordered lists with auto-incrementing numbers (e.g., `1. ` becomes `2. `). pub fn ordered_list(&self) -> &[OrderedListConfig] { &self.language.config.ordered_list } /// Returns configuration for task list continuation, if any (e.g., `- [x] ` continues as `- [ ] `). pub fn task_list(&self) -> Option<&TaskListConfig> { self.language.config.task_list.as_ref() } /// Returns additional regex patterns that act as prefix markers for creating /// boundaries during rewrapping. /// /// By default, Zed treats as paragraph and comment prefixes as boundaries. pub fn rewrap_prefixes(&self) -> &[Regex] { &self.language.config.rewrap_prefixes } /// Returns a list of language-specific word characters. /// /// By default, Zed treats alphanumeric characters (and '_') as word characters for /// the purpose of actions like 'move to next word end` or whole-word search. /// It additionally accounts for language's additional word characters. pub fn word_characters(&self) -> Option<&HashSet> { Override::as_option( self.config_override().map(|o| &o.word_characters), Some(&self.language.config.word_characters), ) } /// Returns a list of language-specific characters that are considered part of /// a completion query. pub fn completion_query_characters(&self) -> Option<&HashSet> { Override::as_option( self.config_override() .map(|o| &o.completion_query_characters), Some(&self.language.config.completion_query_characters), ) } /// Returns a list of language-specific characters that are considered part of /// identifiers during linked editing operations. pub fn linked_edit_characters(&self) -> Option<&HashSet> { Override::as_option( self.config_override().map(|o| &o.linked_edit_characters), Some(&self.language.config.linked_edit_characters), ) } /// Returns whether to prefer snippet `label` over `new_text` to replace text when /// completion is accepted. /// /// In cases like when cursor is in string or renaming existing function, /// you don't want to expand function signature instead just want function name /// to replace existing one. pub fn prefers_label_for_snippet_in_completion(&self) -> bool { self.config_override() .and_then(|o| o.prefer_label_for_snippet) .unwrap_or(false) } /// Returns a list of bracket pairs for a given language with an additional /// piece of information about whether the particular bracket pair is currently active for a given language. pub fn brackets(&self) -> impl Iterator { let mut disabled_ids = self .config_override() .map_or(&[] as _, |o| o.disabled_bracket_ixs.as_slice()); self.language .config .brackets .pairs .iter() .enumerate() .map(move |(ix, bracket)| { let mut is_enabled = true; if let Some(next_disabled_ix) = disabled_ids.first() && ix == *next_disabled_ix as usize { disabled_ids = &disabled_ids[1..]; is_enabled = false; } (bracket, is_enabled) }) } pub fn should_autoclose_before(&self, c: char) -> bool { 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.contains(name) { if let Some(over) = self.config_override() { over.opt_into_language_servers.contains(name) } else { false } } else { true } } pub fn override_name(&self) -> Option<&str> { let id = self.override_id?; let grammar = self.language.grammar.as_ref()?; let override_config = grammar.override_config.as_ref()?; override_config.values.get(&id).map(|e| e.name.as_str()) } fn config_override(&self) -> Option<&LanguageConfigOverride> { let id = self.override_id?; let grammar = self.language.grammar.as_ref()?; let override_config = grammar.override_config.as_ref()?; override_config.values.get(&id).map(|e| &e.value) } } impl Hash for Language { fn hash(&self, state: &mut H) { self.id.hash(state) } } impl PartialEq for Language { fn eq(&self, other: &Self) -> bool { self.id.eq(&other.id) } } impl Eq for Language {} impl Debug for Language { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("Language") .field("name", &self.config.name) .finish() } } pub(crate) fn parse_text(grammar: &Grammar, text: &Rope, old_tree: Option) -> Tree { with_parser(|parser| { parser .set_language(&grammar.ts_language) .expect("incompatible grammar"); let mut chunks = text.chunks_in_range(0..text.len()); parser .parse_with_options( &mut move |offset, _| { chunks.seek(offset); chunks.next().unwrap_or("").as_bytes() }, old_tree.as_ref(), None, ) .unwrap() }) } pub trait CodeLabelExt { fn fallback_for_completion( item: &lsp::CompletionItem, language: Option<&Language>, ) -> CodeLabel; } impl CodeLabelExt for CodeLabel { fn fallback_for_completion( item: &lsp::CompletionItem, language: Option<&Language>, ) -> CodeLabel { let highlight_id = item.kind.and_then(|kind| { let grammar = language?.grammar()?; use lsp::CompletionItemKind as Kind; match kind { Kind::CLASS => grammar.highlight_id_for_name("type"), Kind::CONSTANT => grammar.highlight_id_for_name("constant"), Kind::CONSTRUCTOR => grammar.highlight_id_for_name("constructor"), Kind::ENUM => grammar .highlight_id_for_name("enum") .or_else(|| grammar.highlight_id_for_name("type")), Kind::ENUM_MEMBER => grammar .highlight_id_for_name("variant") .or_else(|| grammar.highlight_id_for_name("property")), Kind::FIELD => grammar.highlight_id_for_name("property"), Kind::FUNCTION => grammar.highlight_id_for_name("function"), Kind::INTERFACE => grammar.highlight_id_for_name("type"), Kind::METHOD => grammar .highlight_id_for_name("function.method") .or_else(|| grammar.highlight_id_for_name("function")), Kind::OPERATOR => grammar.highlight_id_for_name("operator"), Kind::PROPERTY => grammar.highlight_id_for_name("property"), Kind::STRUCT => grammar.highlight_id_for_name("type"), Kind::VARIABLE => grammar.highlight_id_for_name("variable"), Kind::KEYWORD => grammar.highlight_id_for_name("keyword"), _ => None, } }); let label = &item.label; let label_length = label.len(); let runs = highlight_id .map(|highlight_id| vec![(0..label_length, highlight_id)]) .unwrap_or_default(); let text = if let Some(detail) = item.detail.as_deref().filter(|detail| detail != label) { format!("{label} {detail}") } else if let Some(description) = item .label_details .as_ref() .and_then(|label_details| label_details.description.as_deref()) .filter(|description| description != label) { format!("{label} {description}") } else { label.clone() }; let filter_range = item .filter_text .as_deref() .and_then(|filter| text.find(filter).map(|ix| ix..ix + filter.len())) .unwrap_or(0..label_length); CodeLabel { text, runs, filter_range, } } } #[cfg(any(test, feature = "test-support"))] impl Default for FakeLspAdapter { fn default() -> Self { Self { name: "the-fake-language-server", capabilities: lsp::LanguageServer::full_capabilities(), initializer: None, disk_based_diagnostics_progress_token: None, initialization_options: None, disk_based_diagnostics_sources: Vec::new(), prettier_plugins: Vec::new(), language_server_binary: LanguageServerBinary { path: "/the/fake/lsp/path".into(), arguments: vec![], env: Default::default(), }, label_for_completion: None, } } } #[cfg(any(test, feature = "test-support"))] impl LspInstaller for FakeLspAdapter { type BinaryVersion = (); async fn fetch_latest_server_version( &self, _: &dyn LspAdapterDelegate, _: bool, _: &mut AsyncApp, ) -> Result { unreachable!() } async fn check_if_user_installed( &self, _: &dyn LspAdapterDelegate, _: Option, _: &AsyncApp, ) -> Option { Some(self.language_server_binary.clone()) } async fn fetch_server_binary( &self, _: (), _: PathBuf, _: &dyn LspAdapterDelegate, ) -> Result { unreachable!(); } async fn cached_server_binary( &self, _: PathBuf, _: &dyn LspAdapterDelegate, ) -> Option { unreachable!(); } } #[cfg(any(test, feature = "test-support"))] #[async_trait(?Send)] impl LspAdapter for FakeLspAdapter { fn name(&self) -> LanguageServerName { LanguageServerName(self.name.into()) } fn disk_based_diagnostic_sources(&self) -> Vec { self.disk_based_diagnostics_sources.clone() } fn disk_based_diagnostics_progress_token(&self) -> Option { self.disk_based_diagnostics_progress_token.clone() } async fn initialization_options( self: Arc, _: &Arc, _cx: &mut AsyncApp, ) -> Result> { Ok(self.initialization_options.clone()) } async fn label_for_completion( &self, item: &lsp::CompletionItem, language: &Arc, ) -> Option { let label_for_completion = self.label_for_completion.as_ref()?; label_for_completion(item, language) } fn is_extension(&self) -> bool { false } } pub fn point_to_lsp(point: PointUtf16) -> lsp::Position { lsp::Position::new(point.row, point.column) } pub fn point_from_lsp(point: lsp::Position) -> Unclipped { Unclipped(PointUtf16::new(point.line, point.character)) } pub fn range_to_lsp(range: Range) -> Result { anyhow::ensure!( range.start <= range.end, "Inverted range provided to an LSP request: {:?}-{:?}", range.start, range.end ); Ok(lsp::Range { start: point_to_lsp(range.start), end: point_to_lsp(range.end), }) } pub fn range_from_lsp(range: lsp::Range) -> Range> { let mut start = point_from_lsp(range.start); let mut end = point_from_lsp(range.end); if start > end { // We debug instead of warn so that this is not logged by default unless explicitly requested. // Using warn would write to the log file, and since we receive an enormous amount of // range_from_lsp calls (especially during completions), that can hang the main thread. // // See issue #36223. zlog::debug!("range_from_lsp called with inverted range {start:?}-{end:?}"); mem::swap(&mut start, &mut end); } start..end } #[doc(hidden)] #[cfg(any(test, feature = "test-support"))] pub fn rust_lang() -> Arc { use std::borrow::Cow; let language = Language::new( LanguageConfig { name: "Rust".into(), matcher: LanguageMatcher { path_suffixes: vec!["rs".to_string()], ..Default::default() }, line_comments: vec!["// ".into(), "/// ".into(), "//! ".into()], brackets: BracketPairConfig { pairs: vec![ BracketPair { start: "{".into(), end: "}".into(), close: true, surround: false, newline: true, }, BracketPair { start: "[".into(), end: "]".into(), close: true, surround: false, newline: true, }, BracketPair { start: "(".into(), end: ")".into(), close: true, surround: false, newline: true, }, BracketPair { start: "<".into(), end: ">".into(), close: false, surround: false, newline: true, }, BracketPair { start: "\"".into(), end: "\"".into(), close: true, surround: false, newline: false, }, ], ..Default::default() }, ..Default::default() }, Some(tree_sitter_rust::LANGUAGE.into()), ) .with_queries(LanguageQueries { outline: Some(Cow::from(include_str!( "../../grammars/src/rust/outline.scm" ))), indents: Some(Cow::from(include_str!( "../../grammars/src/rust/indents.scm" ))), brackets: Some(Cow::from(include_str!( "../../grammars/src/rust/brackets.scm" ))), text_objects: Some(Cow::from(include_str!( "../../grammars/src/rust/textobjects.scm" ))), highlights: Some(Cow::from(include_str!( "../../grammars/src/rust/highlights.scm" ))), injections: Some(Cow::from(include_str!( "../../grammars/src/rust/injections.scm" ))), overrides: Some(Cow::from(include_str!( "../../grammars/src/rust/overrides.scm" ))), redactions: None, runnables: Some(Cow::from(include_str!( "../../grammars/src/rust/runnables.scm" ))), debugger: Some(Cow::from(include_str!( "../../grammars/src/rust/debugger.scm" ))), }) .expect("Could not parse queries"); Arc::new(language) } #[doc(hidden)] #[cfg(any(test, feature = "test-support"))] pub fn markdown_lang() -> Arc { use std::borrow::Cow; let language = Language::new( LanguageConfig { name: "Markdown".into(), matcher: LanguageMatcher { path_suffixes: vec!["md".into()], ..Default::default() }, ..LanguageConfig::default() }, Some(tree_sitter_md::LANGUAGE.into()), ) .with_queries(LanguageQueries { brackets: Some(Cow::from(include_str!( "../../grammars/src/markdown/brackets.scm" ))), injections: Some(Cow::from(include_str!( "../../grammars/src/markdown/injections.scm" ))), highlights: Some(Cow::from(include_str!( "../../grammars/src/markdown/highlights.scm" ))), indents: Some(Cow::from(include_str!( "../../grammars/src/markdown/indents.scm" ))), outline: Some(Cow::from(include_str!( "../../grammars/src/markdown/outline.scm" ))), ..LanguageQueries::default() }) .expect("Could not parse markdown queries"); Arc::new(language) } #[cfg(test)] mod tests { use super::*; use gpui::{TestAppContext, rgba}; use pretty_assertions::assert_matches; #[test] fn test_highlight_map() { let theme = SyntaxTheme::new( [ ("function", rgba(0x100000ff)), ("function.method", rgba(0x200000ff)), ("function.async", rgba(0x300000ff)), ("variable.builtin.self.rust", rgba(0x400000ff)), ("variable.builtin", rgba(0x500000ff)), ("variable", rgba(0x600000ff)), ] .iter() .map(|(name, color)| (name.to_string(), (*color).into())), ); let capture_names = &[ "function.special", "function.async.rust", "variable.builtin.self", ]; let map = build_highlight_map(capture_names, &theme); assert_eq!( theme.get_capture_name(map.get(0).unwrap()), Some("function") ); assert_eq!( theme.get_capture_name(map.get(1).unwrap()), Some("function.async") ); assert_eq!( theme.get_capture_name(map.get(2).unwrap()), Some("variable.builtin") ); } #[test] fn test_with_parser_resets_after_cancellation() { use std::ops::ControlFlow; use tree_sitter::{Language as TsLanguage, ParseOptions}; let rust_language: TsLanguage = tree_sitter_rust::LANGUAGE.into(); // Drain the shared pool so this test sees a deterministic LIFO order: // the parser we push at the end of the first `with_parser` call is the // one we pop at the start of the second call. PARSERS.lock().clear(); // Large enough that tree-sitter invokes the progress callback before // the parse completes; otherwise the cancellation never fires. let large_input = format!("fn a() {{ {} }}", "b(c, d); e(f, g); ".repeat(5000)); let small_input = "fn z() {}"; // Cancel a parse via the progress callback. Tree-sitter retains the // in-progress parse state on the parser (its `canceled_balancing` flag // and/or non-empty parse stack), and the next call to // `parse_with_options` will *resume* that parse unless the parser is // reset first. let cancelled = with_parser(|parser| { parser.set_language(&rust_language).unwrap(); let bytes = large_input.as_bytes(); let mut break_immediately = |_: &_| ControlFlow::Break(()); parser.parse_with_options( &mut |offset, _| { if offset < bytes.len() { &bytes[offset..] } else { &[] } }, None, Some(ParseOptions { progress_callback: Some(&mut break_immediately), }), ) }); assert!( cancelled.is_none(), "first parse should be cancelled by the progress callback" ); // Deliberately do NOT call `set_language` here: tree-sitter's // `ts_parser_set_language` internally calls `ts_parser_reset`, which // would mask the very bug we're checking for. Instead we rely on the // language being preserved across `parser.reset()` (it is) and verify // that `with_parser` itself produces a clean parser for the next user. let tree = with_parser(|parser| { let bytes = small_input.as_bytes(); parser .parse_with_options( &mut |offset, _| { if offset < bytes.len() { &bytes[offset..] } else { &[] } }, None, None, ) .expect("parse of small_input should succeed") }); assert_eq!(tree.root_node().byte_range(), 0..small_input.len()); assert_eq!(tree.root_node().kind(), "source_file"); assert!( !tree.root_node().has_error(), "tree should be error-free, got: {}", tree.root_node().to_sexp() ); } #[gpui::test(iterations = 10)] async fn test_language_loading(cx: &mut TestAppContext) { let languages = LanguageRegistry::test(cx.executor()); let languages = Arc::new(languages); languages.register_native_grammars([ ("json", tree_sitter_json::LANGUAGE), ("rust", tree_sitter_rust::LANGUAGE), ]); languages.register_test_language(LanguageConfig { name: "JSON".into(), grammar: Some("json".into()), matcher: LanguageMatcher { path_suffixes: vec!["json".into()], ..Default::default() }, ..Default::default() }); languages.register_test_language(LanguageConfig { name: "Rust".into(), grammar: Some("rust".into()), matcher: LanguageMatcher { path_suffixes: vec!["rs".into()], ..Default::default() }, ..Default::default() }); assert_eq!( languages.language_names(), &[ LanguageName::new_static("JSON"), LanguageName::new_static("Plain Text"), LanguageName::new_static("Rust"), ] ); let rust1 = languages.language_for_name("Rust"); let rust2 = languages.language_for_name("Rust"); // Ensure language is still listed even if it's being loaded. assert_eq!( languages.language_names(), &[ LanguageName::new_static("JSON"), LanguageName::new_static("Plain Text"), LanguageName::new_static("Rust"), ] ); let (rust1, rust2) = futures::join!(rust1, rust2); assert!(Arc::ptr_eq(&rust1.unwrap(), &rust2.unwrap())); // Ensure language is still listed even after loading it. assert_eq!( languages.language_names(), &[ LanguageName::new_static("JSON"), LanguageName::new_static("Plain Text"), LanguageName::new_static("Rust"), ] ); // Loading an unknown language returns an error. assert!(languages.language_for_name("Unknown").await.is_err()); } #[gpui::test] async fn test_completion_label_omits_duplicate_data() { let regular_completion_item_1 = lsp::CompletionItem { label: "regular1".to_string(), detail: Some("detail1".to_string()), label_details: Some(lsp::CompletionItemLabelDetails { detail: None, description: Some("description 1".to_string()), }), ..lsp::CompletionItem::default() }; let regular_completion_item_2 = lsp::CompletionItem { label: "regular2".to_string(), label_details: Some(lsp::CompletionItemLabelDetails { detail: None, description: Some("description 2".to_string()), }), ..lsp::CompletionItem::default() }; let completion_item_with_duplicate_detail_and_proper_description = lsp::CompletionItem { detail: Some(regular_completion_item_1.label.clone()), ..regular_completion_item_1.clone() }; let completion_item_with_duplicate_detail = lsp::CompletionItem { detail: Some(regular_completion_item_1.label.clone()), label_details: None, ..regular_completion_item_1.clone() }; let completion_item_with_duplicate_description = lsp::CompletionItem { label_details: Some(lsp::CompletionItemLabelDetails { detail: None, description: Some(regular_completion_item_2.label.clone()), }), ..regular_completion_item_2.clone() }; assert_eq!( CodeLabel::fallback_for_completion(®ular_completion_item_1, None).text, format!( "{} {}", regular_completion_item_1.label, regular_completion_item_1.detail.unwrap() ), "LSP completion items with both detail and label_details.description should prefer detail" ); assert_eq!( CodeLabel::fallback_for_completion(®ular_completion_item_2, None).text, format!( "{} {}", regular_completion_item_2.label, regular_completion_item_2 .label_details .as_ref() .unwrap() .description .as_ref() .unwrap() ), "LSP completion items without detail but with label_details.description should use that" ); assert_eq!( CodeLabel::fallback_for_completion( &completion_item_with_duplicate_detail_and_proper_description, None ) .text, format!( "{} {}", regular_completion_item_1.label, regular_completion_item_1 .label_details .as_ref() .unwrap() .description .as_ref() .unwrap() ), "LSP completion items with both detail and label_details.description should prefer description only if the detail duplicates the completion label" ); assert_eq!( CodeLabel::fallback_for_completion(&completion_item_with_duplicate_detail, None).text, regular_completion_item_1.label, "LSP completion items with duplicate label and detail, should omit the detail" ); assert_eq!( CodeLabel::fallback_for_completion(&completion_item_with_duplicate_description, None) .text, regular_completion_item_2.label, "LSP completion items with duplicate label and detail, should omit the detail" ); } #[test] fn test_deserializing_comments_backwards_compat() { // current version of `block_comment` and `documentation_comment` work { let config: LanguageConfig = ::toml::from_str( r#" name = "Foo" block_comment = { start = "a", end = "b", prefix = "c", tab_size = 1 } documentation_comment = { start = "d", end = "e", prefix = "f", tab_size = 2 } "#, ) .unwrap(); assert_matches!(config.block_comment, Some(BlockCommentConfig { .. })); assert_matches!( config.documentation_comment, Some(BlockCommentConfig { .. }) ); let block_config = config.block_comment.unwrap(); assert_eq!(block_config.start.as_ref(), "a"); assert_eq!(block_config.end.as_ref(), "b"); assert_eq!(block_config.prefix.as_ref(), "c"); assert_eq!(block_config.tab_size, 1); let doc_config = config.documentation_comment.unwrap(); assert_eq!(doc_config.start.as_ref(), "d"); assert_eq!(doc_config.end.as_ref(), "e"); assert_eq!(doc_config.prefix.as_ref(), "f"); assert_eq!(doc_config.tab_size, 2); } // former `documentation` setting is read into `documentation_comment` { let config: LanguageConfig = ::toml::from_str( r#" name = "Foo" documentation = { start = "a", end = "b", prefix = "c", tab_size = 1} "#, ) .unwrap(); assert_matches!( config.documentation_comment, Some(BlockCommentConfig { .. }) ); let config = config.documentation_comment.unwrap(); assert_eq!(config.start.as_ref(), "a"); assert_eq!(config.end.as_ref(), "b"); assert_eq!(config.prefix.as_ref(), "c"); assert_eq!(config.tab_size, 1); } // old block_comment format is read into BlockCommentConfig { let config: LanguageConfig = ::toml::from_str( r#" name = "Foo" block_comment = ["a", "b"] "#, ) .unwrap(); assert_matches!(config.block_comment, Some(BlockCommentConfig { .. })); let config = config.block_comment.unwrap(); assert_eq!(config.start.as_ref(), "a"); assert_eq!(config.end.as_ref(), "b"); assert_eq!(config.prefix.as_ref(), ""); assert_eq!(config.tab_size, 0); } } }