From f31d279009b0a603482cce3619d90a4c91084bbe Mon Sep 17 00:00:00 2001 From: ethicnology Date: Thu, 30 Jul 2026 21:15:40 -0400 Subject: [PATCH 1/3] Add Dart syntax support to the editor --- Cargo.lock | 12 ++++ Cargo.toml | 1 + .../ai/src/index/file_outline/native_tests.rs | 51 ++++++++++++++++ crates/languages/grammars/dart/config.yaml | 11 ++++ .../languages/grammars/dart/identifiers.scm | 15 +++++ crates/languages/grammars/dart/indents.scm | 16 +++++ crates/languages/src/lib.rs | 5 +- crates/languages/src/lib_tests.rs | 52 +++++++++++++++++ .../src/queries/indent_query_tests.rs | 58 +++++++++++++++++++ 9 files changed, 220 insertions(+), 1 deletion(-) create mode 100644 crates/languages/grammars/dart/config.yaml create mode 100644 crates/languages/grammars/dart/identifiers.scm create mode 100644 crates/languages/grammars/dart/indents.scm diff --git a/Cargo.lock b/Cargo.lock index 7e77ed7daea..18c99e4a0ef 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -650,6 +650,7 @@ dependencies = [ "arborium-c-sharp", "arborium-cpp", "arborium-css", + "arborium-dart", "arborium-dockerfile", "arborium-elixir", "arborium-go", @@ -741,6 +742,17 @@ dependencies = [ "tree-sitter-language", ] +[[package]] +name = "arborium-dart" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a58169ac16f6d22dcf6bcad164e94889e9b30c14ac13f2ca8ff365afcbcf4b5d" +dependencies = [ + "arborium-sysroot", + "cc", + "tree-sitter-language", +] + [[package]] name = "arborium-dockerfile" version = "2.13.0" diff --git a/Cargo.toml b/Cargo.toml index 0da7f572b88..1c9eef93827 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -334,6 +334,7 @@ arborium = { version = "2", default-features = false, features = [ "lang-vue", "lang-dockerfile", "lang-markdown", + "lang-dart", ] } unicode-general-category = "1.1.0" unicode-linebreak = "0.1.5" diff --git a/crates/ai/src/index/file_outline/native_tests.rs b/crates/ai/src/index/file_outline/native_tests.rs index d303e7a8556..789f25983ec 100644 --- a/crates/ai/src/index/file_outline/native_tests.rs +++ b/crates/ai/src/index/file_outline/native_tests.rs @@ -194,3 +194,54 @@ fmt.Println("Helper function") assert_eq!(symbols[4].name, "helperFunction"); assert_eq!(symbols[4].type_prefix, Some("func".to_owned())); } + +#[test] +fn test_parse_dart_outline() { + let temp_dir = TempDir::new().unwrap(); + let content = r#" +/// Widget used to verify Dart outline extraction. +class Probe { + Probe(); + + Widget build() => const SizedBox.shrink(); +} + +mixin Logging { + void log() {} +} + +extension StringX on String { + bool get blank => isEmpty; +} + +enum Status { ready } +"#; + let file_path = create_test_file(&temp_dir, "main.dart", content); + + let outline = parse_file_outline(&file_path).unwrap(); + let symbols = outline.symbols.unwrap(); + let names_and_types: Vec<_> = symbols + .iter() + .map(|symbol| (symbol.name.as_str(), symbol.type_prefix.as_deref())) + .collect(); + + assert_eq!( + names_and_types, + vec![ + ("Probe", Some("class")), + ("Probe", Some("constructor")), + ("build", Some("fn")), + ("Logging", Some("class")), + ("log", Some("fn")), + ("StringX", Some("class")), + ("blank", Some("fn")), + ("Status", Some("enum")), + ], + ); + assert_eq!( + symbols[0].comment, + Some(vec![ + "/// Widget used to verify Dart outline extraction.".to_string() + ]), + ); +} diff --git a/crates/languages/grammars/dart/config.yaml b/crates/languages/grammars/dart/config.yaml new file mode 100644 index 00000000000..635426345b3 --- /dev/null +++ b/crates/languages/grammars/dart/config.yaml @@ -0,0 +1,11 @@ +display_name: "Dart" +indent_unit: + space: 2 +comment_prefix: "//" +brackets: + - start: "{" + end: "}" + - start: "[" + end: "]" + - start: "(" + end: ")" diff --git a/crates/languages/grammars/dart/identifiers.scm b/crates/languages/grammars/dart/identifiers.scm new file mode 100644 index 00000000000..b194c57df1d --- /dev/null +++ b/crates/languages/grammars/dart/identifiers.scm @@ -0,0 +1,15 @@ +(comment) @comment +(documentation_comment) @comment + +; Type definitions +(class_definition name: (identifier) @definition.class) +(enum_declaration name: (identifier) @definition.enum) +(mixin_declaration (identifier) @definition.class) +(extension_declaration name: (identifier) @definition.class) +(extension_type_declaration name: (identifier) @definition.class) + +; Function and constructor definitions +(function_signature name: (identifier) @definition.fn) +(getter_signature name: (identifier) @definition.fn) +(setter_signature name: (identifier) @definition.fn) +(constructor_signature name: (identifier) @definition.constructor) diff --git a/crates/languages/grammars/dart/indents.scm b/crates/languages/grammars/dart/indents.scm new file mode 100644 index 00000000000..176d456c40b --- /dev/null +++ b/crates/languages/grammars/dart/indents.scm @@ -0,0 +1,16 @@ +[ + (class_body) + (extension_body) + (enum_body) + (block) + (arguments) + (formal_parameter_list) + (list_literal) + (set_or_map_literal) +] @indent + +[ + "}" + "]" + ")" +] @outdent diff --git a/crates/languages/src/lib.rs b/crates/languages/src/lib.rs index d389eea7e4d..ec3deb31f20 100644 --- a/crates/languages/src/lib.rs +++ b/crates/languages/src/lib.rs @@ -18,7 +18,7 @@ lazy_static! { static ref LANGUAGE_REGISTRY: LanguageRegistry = LanguageRegistry::new(); } -pub const SUPPORTED_LANGUAGES: [&str; 35] = [ +pub const SUPPORTED_LANGUAGES: [&str; 36] = [ "rust", "golang", "yaml", @@ -54,6 +54,7 @@ pub const SUPPORTED_LANGUAGES: [&str; 35] = [ "dockerfile", "nix", "markdown", + "dart", ]; /// Registry that holds all of the supported languages. @@ -196,6 +197,7 @@ fn language_by_filename_parts( "vue" => language_by_name("vue"), "dockerfile" => language_by_name("dockerfile"), "md" | "markdown" => language_by_name("markdown"), + "dart" => language_by_name("dart"), _ => None, } } @@ -294,6 +296,7 @@ fn get_arborium_highlight_query(lang: &str) -> Option<&str> { "vue" => Some(&arborium::lang_vue::HIGHLIGHTS_QUERY), "dockerfile" => Some(arborium::lang_dockerfile::HIGHLIGHTS_QUERY), "markdown" => Some(arborium::lang_markdown::HIGHLIGHTS_QUERY), + "dart" => Some(arborium::lang_dart::HIGHLIGHTS_QUERY), _ => None, } } diff --git a/crates/languages/src/lib_tests.rs b/crates/languages/src/lib_tests.rs index 5e3aa9bbe7e..f63b6a66d67 100644 --- a/crates/languages/src/lib_tests.rs +++ b/crates/languages/src/lib_tests.rs @@ -1,5 +1,6 @@ use std::path::Path; +use arborium::tree_sitter::Parser; use warp_util::standardized_path::StandardizedPath; use crate::{SUPPORTED_LANGUAGES, language_by_filename, language_by_local_filename, load_language}; @@ -95,3 +96,54 @@ fn markdown_extensions_resolve_to_markdown() { ); } } + +#[test] +fn dart_extension_resolves_to_dart() { + let standardized_path = + StandardizedPath::try_new("/tmp/main.dart").expect("test path should be absolute"); + let language = language_by_filename(&standardized_path) + .expect("`.dart` files should resolve to a language"); + assert_eq!(language.display_name(), "Dart"); + + let language = language_by_local_filename(Path::new("main.dart")) + .expect("local `.dart` files should resolve to a language"); + assert_eq!(language.display_name(), "Dart"); +} + +#[test] +fn dart_grammar_parses_modern_flutter_code() { + let language = language_by_local_filename(Path::new("main.dart")) + .expect("`.dart` files should resolve to a language"); + let source = r#" +import 'package:flutter/material.dart'; + +sealed class LoadState {} +class Loaded extends LoadState { + Loaded(this.value); + final T value; +} + +class Probe extends StatelessWidget { + const Probe({super.key}); + + @override + Widget build(BuildContext context) => switch (>{}) { + final values when values.isEmpty => const SizedBox.shrink(), + _ => const Text('Loaded'), + }; +} +"#; + + let mut parser = Parser::new(); + parser + .set_language(&language.grammar) + .expect("Dart grammar should be compatible with tree-sitter"); + let tree = parser + .parse(source, None) + .expect("Dart parser should produce a syntax tree"); + + assert!( + !tree.root_node().has_error(), + "modern Dart and Flutter syntax should parse without errors", + ); +} diff --git a/crates/syntax_tree/src/queries/indent_query_tests.rs b/crates/syntax_tree/src/queries/indent_query_tests.rs index ceab17b2f5b..a39cb3b8156 100644 --- a/crates/syntax_tree/src/queries/indent_query_tests.rs +++ b/crates/syntax_tree/src/queries/indent_query_tests.rs @@ -215,6 +215,64 @@ fn test_indent_query_on_go() { }); } +#[test] +fn test_indent_query_on_dart_widget_tree() { + App::test((), |mut app| async move { + let language = language_by_filename(&test_path("main.dart")) + .expect("Should contain language rule for Dart"); + let text_content = r#"class Probe { + Widget build() { + return Column( + children: [ + Text('hello'), + ], + ); + } +}"#; + + let buffer_handle = app.add_model(|_| Buffer::new(Box::new(|_, _| IndentBehavior::Ignore))); + let selection = app.add_model(|_| BufferSelectionModel::new(buffer_handle.clone())); + + buffer_handle.update(&mut app, |buffer, ctx| { + *buffer = Buffer::from_plain_text( + text_content, + None, + Box::new(|_, _| IndentBehavior::Ignore), + selection, + ctx, + ); + }); + + let buffer_snapshot = buffer_handle.read(&app, |buffer, _| buffer.buffer_snapshot()); + let tree = SyntaxTreeState::parse_text(buffer_snapshot, None, &language) + .await + .expect("test buffer is small and should parse"); + let query = language + .indents_query + .as_ref() + .expect("Dart should provide an indentation query"); + + buffer_handle.read(&app, |buffer, _| { + for (row, column, expected_delta) in [ + (1, 0, 1), + (2, 0, 2), + (3, 0, 3), + (4, 0, 4), + (5, 6, 3), + (6, 4, 2), + ] { + assert_eq!( + indentation_delta(buffer, &tree, Point { row, column }, query) + .expect("indentation should be available") + .delta, + expected_delta, + "unexpected Dart indentation on row {row}", + ); + } + }); + }); +} + #[test] fn test_indent_query_on_go_bracket_expansion() { let language = From ece44a8bb9d7c507d9ceec844974756def8bdbd5 Mon Sep 17 00:00:00 2001 From: ethicnology Date: Thu, 30 Jul 2026 21:16:00 -0400 Subject: [PATCH 2/3] Highlight Dart code in agent output --- app/src/ai/agent/mod.rs | 1 + app/src/ai/agent/mod_tests.rs | 1 + 2 files changed, 2 insertions(+) diff --git a/app/src/ai/agent/mod.rs b/app/src/ai/agent/mod.rs index 6b832089cdc..5e4af818a24 100644 --- a/app/src/ai/agent/mod.rs +++ b/app/src/ai/agent/mod.rs @@ -984,6 +984,7 @@ impl ProgrammingLanguage { "vue" => Some("vue"), "dockerfile" | "docker" | "containerfile" => Some("dockerfile"), "markdown" | "md" => Some("md"), + "dart" => Some("dart"), _ => None, }, Self::Shell(ShellType::PowerShell) => Some("ps1"), diff --git a/app/src/ai/agent/mod_tests.rs b/app/src/ai/agent/mod_tests.rs index 0b3aa34ab32..c716c7461c4 100644 --- a/app/src/ai/agent/mod_tests.rs +++ b/app/src/ai/agent/mod_tests.rs @@ -287,6 +287,7 @@ fn test_programming_language_to_extension() { ("containerfile", "dockerfile"), ("markdown", "md"), ("md", "md"), + ("dart", "dart"), ]; for (token, expected_extension) in cases { let language = ProgrammingLanguage::from((*token).to_string()); From 1b7666387aadd205fa53574f026fb96308b6865d Mon Sep 17 00:00:00 2001 From: ethicnology Date: Thu, 30 Jul 2026 21:17:05 -0400 Subject: [PATCH 3/3] Add Dart Analysis Server support --- crates/lsp/src/config.rs | 4 ++ crates/lsp/src/config_tests.rs | 13 +++- crates/lsp/src/servers/dart.rs | 76 +++++++++++++++++++++++ crates/lsp/src/servers/mod.rs | 1 + crates/lsp/src/supported_servers.rs | 13 ++++ crates/lsp/src/supported_servers_tests.rs | 14 +++++ 6 files changed, 120 insertions(+), 1 deletion(-) create mode 100644 crates/lsp/src/servers/dart.rs create mode 100644 crates/lsp/src/supported_servers_tests.rs diff --git a/crates/lsp/src/config.rs b/crates/lsp/src/config.rs index 71cb9cac0ff..442406f1422 100644 --- a/crates/lsp/src/config.rs +++ b/crates/lsp/src/config.rs @@ -32,6 +32,7 @@ pub enum LanguageId { JavaScriptReact, C, Cpp, + Dart, } impl LanguageId { @@ -52,6 +53,7 @@ impl LanguageId { // compile_commands.json is present, clangd will use the correct language // regardless of the languageId we send. "h" | "H" | "hh" | "hpp" | "hxx" => Some(Self::Cpp), + "dart" => Some(Self::Dart), _ => None, } } @@ -69,6 +71,7 @@ impl LanguageId { LanguageId::JavaScriptReact => "javascriptreact", LanguageId::C => "c", LanguageId::Cpp => "cpp", + LanguageId::Dart => "dart", } } @@ -83,6 +86,7 @@ impl LanguageId { | LanguageId::JavaScript | LanguageId::JavaScriptReact => LSPServerType::TypeScriptLanguageServer, LanguageId::C | LanguageId::Cpp => LSPServerType::Clangd, + LanguageId::Dart => LSPServerType::DartAnalysisServer, } } } diff --git a/crates/lsp/src/config_tests.rs b/crates/lsp/src/config_tests.rs index e4ec2dd2245..16fb2855d10 100644 --- a/crates/lsp/src/config_tests.rs +++ b/crates/lsp/src/config_tests.rs @@ -2,7 +2,8 @@ use std::path::PathBuf; use lsp_types::Uri; -use crate::config::{lsp_uri_to_path, path_to_lsp_uri}; +use crate::config::{LanguageId, lsp_uri_to_path, path_to_lsp_uri}; +use crate::supported_servers::LSPServerType; // Unix-specific tests use Unix paths #[cfg(not(windows))] @@ -216,3 +217,13 @@ fn test_path_to_lsp_uri_rejects_relative_path() { assert!(result.is_err()); assert!(result.unwrap_err().to_string().contains("must be absolute")); } + +#[test] +fn test_dart_path_uses_dart_analysis_server() { + let language = LanguageId::from_path(&PathBuf::from("lib/main.dart")) + .expect("`.dart` files should resolve to an LSP language"); + + assert_eq!(language, LanguageId::Dart); + assert_eq!(language.lsp_language_identifier(), "dart"); + assert_eq!(language.server_type(), LSPServerType::DartAnalysisServer); +} diff --git a/crates/lsp/src/servers/dart.rs b/crates/lsp/src/servers/dart.rs new file mode 100644 index 00000000000..94a98006366 --- /dev/null +++ b/crates/lsp/src/servers/dart.rs @@ -0,0 +1,76 @@ +use std::path::Path; + +use async_trait::async_trait; + +use crate::CommandBuilder; +use crate::language_server_candidate::{LanguageServerCandidate, LanguageServerMetadata}; + +pub struct DartAnalysisServerCandidate; + +#[async_trait] +#[cfg(feature = "local_fs")] +impl LanguageServerCandidate for DartAnalysisServerCandidate { + async fn should_suggest_for_repo(&self, path: &Path, executor: &CommandBuilder) -> bool { + path.join("pubspec.yaml").exists() && self.is_installed_on_path(executor).await + } + + async fn is_installed_in_data_dir(&self, _executor: &CommandBuilder) -> bool { + false + } + + async fn is_installed_on_path(&self, executor: &CommandBuilder) -> bool { + executor + .command("dart") + .args(["language-server", "--help"]) + .output() + .await + .map(|output| output.status.success()) + .unwrap_or(false) + } + + async fn install( + &self, + _metadata: LanguageServerMetadata, + _executor: &CommandBuilder, + ) -> anyhow::Result<()> { + anyhow::bail!( + "The Dart Analysis Server is bundled with the Dart SDK. Install Dart or Flutter and ensure `dart` is available on your PATH." + ) + } + + async fn fetch_latest_server_metadata(&self) -> anyhow::Result { + Ok(LanguageServerMetadata { + version: "bundled".to_string(), + url: None, + digest: None, + }) + } +} + +#[async_trait] +#[cfg(not(feature = "local_fs"))] +impl LanguageServerCandidate for DartAnalysisServerCandidate { + async fn should_suggest_for_repo(&self, _path: &Path, _executor: &CommandBuilder) -> bool { + false + } + + async fn is_installed_in_data_dir(&self, _executor: &CommandBuilder) -> bool { + false + } + + async fn is_installed_on_path(&self, _executor: &CommandBuilder) -> bool { + false + } + + async fn install( + &self, + _metadata: LanguageServerMetadata, + _executor: &CommandBuilder, + ) -> anyhow::Result<()> { + anyhow::bail!("Dart language support requires local filesystem access") + } + + async fn fetch_latest_server_metadata(&self) -> anyhow::Result { + anyhow::bail!("Dart language support requires local filesystem access") + } +} diff --git a/crates/lsp/src/servers/mod.rs b/crates/lsp/src/servers/mod.rs index 32d836768e9..ab721e9ac47 100644 --- a/crates/lsp/src/servers/mod.rs +++ b/crates/lsp/src/servers/mod.rs @@ -1,4 +1,5 @@ pub mod clangd; +pub mod dart; pub mod go; pub mod pyright; pub mod rust; diff --git a/crates/lsp/src/supported_servers.rs b/crates/lsp/src/supported_servers.rs index c4f902fb013..52d29bbddeb 100644 --- a/crates/lsp/src/supported_servers.rs +++ b/crates/lsp/src/supported_servers.rs @@ -12,6 +12,7 @@ use strum_macros::EnumIter; #[cfg(not(target_arch = "wasm32"))] use crate::CommandBuilder; use crate::servers::clangd::ClangdCandidate; +use crate::servers::dart::DartAnalysisServerCandidate; use crate::servers::go::GoPlsCandidate; use crate::servers::pyright::PyrightCandidate; use crate::servers::rust::RustAnalyzerCandidate; @@ -43,6 +44,7 @@ pub enum LSPServerType { Pyright, TypeScriptLanguageServer, Clangd, + DartAnalysisServer, } /// Provides server-specific configuration for each LSP server type. @@ -110,6 +112,8 @@ impl LSPServerType { binary_path: path, prepend_args: vec![], }), + // The analysis server is bundled with the Dart SDK. + LSPServerType::DartAnalysisServer => None, } } @@ -133,6 +137,7 @@ impl LSPServerType { LSPServerType::Pyright => "pyright-langserver", LSPServerType::TypeScriptLanguageServer => "typescript-language-server", LSPServerType::Clangd => "clangd", + LSPServerType::DartAnalysisServer => "dart", } } @@ -141,6 +146,7 @@ impl LSPServerType { fn args(&self) -> Vec<&'static str> { match self { LSPServerType::RustAnalyzer | LSPServerType::GoPls | LSPServerType::Clangd => vec![], + LSPServerType::DartAnalysisServer => vec!["language-server"], LSPServerType::Pyright | LSPServerType::TypeScriptLanguageServer => vec!["--stdio"], } } @@ -155,6 +161,7 @@ impl LSPServerType { LSPServerType::Pyright => vec!["--stdio"], LSPServerType::TypeScriptLanguageServer => vec!["--stdio"], LSPServerType::Clangd => vec![], + LSPServerType::DartAnalysisServer => vec!["language-server"], } } @@ -173,6 +180,7 @@ impl LSPServerType { ] } LSPServerType::Clangd => vec![LanguageId::C, LanguageId::Cpp], + LSPServerType::DartAnalysisServer => vec![LanguageId::Dart], } } @@ -206,6 +214,7 @@ impl LSPServerType { Box::new(TypeScriptLanguageServerCandidate::new(client)) } LSPServerType::Clangd => Box::new(ClangdCandidate::new(client)), + LSPServerType::DartAnalysisServer => Box::new(DartAnalysisServerCandidate), } } @@ -213,3 +222,7 @@ impl LSPServerType { LSPServerType::iter() } } + +#[cfg(test)] +#[path = "supported_servers_tests.rs"] +mod tests; diff --git a/crates/lsp/src/supported_servers_tests.rs b/crates/lsp/src/supported_servers_tests.rs new file mode 100644 index 00000000000..65e891a1c3b --- /dev/null +++ b/crates/lsp/src/supported_servers_tests.rs @@ -0,0 +1,14 @@ +use super::LSPServerType; +use crate::config::LanguageId; + +#[test] +fn dart_analysis_server_configuration() { + let server = LSPServerType::DartAnalysisServer; + + assert_eq!(server.binary_name(), "dart"); + assert_eq!(server.languages(), vec![LanguageId::Dart]); + assert_eq!(server.language_name(), "Dart"); + + #[cfg(not(target_arch = "wasm32"))] + assert_eq!(server.args(), vec!["language-server"]); +}