Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
1 change: 1 addition & 0 deletions app/src/ai/agent/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand Down
1 change: 1 addition & 0 deletions app/src/ai/agent/mod_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand Down
51 changes: 51 additions & 0 deletions crates/ai/src/index/file_outline/native_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
]),
);
}
11 changes: 11 additions & 0 deletions crates/languages/grammars/dart/config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
display_name: "Dart"
indent_unit:
space: 2
comment_prefix: "//"
brackets:
- start: "{"
end: "}"
- start: "["
end: "]"
- start: "("
end: ")"
15 changes: 15 additions & 0 deletions crates/languages/grammars/dart/identifiers.scm
Original file line number Diff line number Diff line change
@@ -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)
16 changes: 16 additions & 0 deletions crates/languages/grammars/dart/indents.scm
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
[
(class_body)
(extension_body)
(enum_body)
(block)
(arguments)
(formal_parameter_list)
(list_literal)
(set_or_map_literal)
] @indent

[
"}"
"]"
")"
] @outdent
5 changes: 4 additions & 1 deletion crates/languages/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -54,6 +54,7 @@ pub const SUPPORTED_LANGUAGES: [&str; 35] = [
"dockerfile",
"nix",
"markdown",
"dart",
];

/// Registry that holds all of the supported languages.
Expand Down Expand Up @@ -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,
}
}
Expand Down Expand Up @@ -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,
}
}
Expand Down
52 changes: 52 additions & 0 deletions crates/languages/src/lib_tests.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand Down Expand Up @@ -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<T> extends LoadState {
Loaded(this.value);
final T value;
}

class Probe extends StatelessWidget {
const Probe({super.key});

@override
Widget build(BuildContext context) => switch (<String, List<int>>{}) {
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",
);
}
4 changes: 4 additions & 0 deletions crates/lsp/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ pub enum LanguageId {
JavaScriptReact,
C,
Cpp,
Dart,
}

impl LanguageId {
Expand All @@ -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,
}
}
Expand All @@ -69,6 +71,7 @@ impl LanguageId {
LanguageId::JavaScriptReact => "javascriptreact",
LanguageId::C => "c",
LanguageId::Cpp => "cpp",
LanguageId::Dart => "dart",
}
}

Expand All @@ -83,6 +86,7 @@ impl LanguageId {
| LanguageId::JavaScript
| LanguageId::JavaScriptReact => LSPServerType::TypeScriptLanguageServer,
LanguageId::C | LanguageId::Cpp => LSPServerType::Clangd,
LanguageId::Dart => LSPServerType::DartAnalysisServer,
}
}
}
Expand Down
13 changes: 12 additions & 1 deletion crates/lsp/src/config_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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))]
Expand Down Expand Up @@ -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);
}
76 changes: 76 additions & 0 deletions crates/lsp/src/servers/dart.rs
Original file line number Diff line number Diff line change
@@ -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<LanguageServerMetadata> {
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<LanguageServerMetadata> {
anyhow::bail!("Dart language support requires local filesystem access")
}
}
1 change: 1 addition & 0 deletions crates/lsp/src/servers/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
pub mod clangd;
pub mod dart;
pub mod go;
pub mod pyright;
pub mod rust;
Expand Down
Loading