From af5eed14fed7ed8f93827ebdf32a8ea390b703d3 Mon Sep 17 00:00:00 2001 From: Abdalla Eldoumani <119013570+Abdalla-Eldoumani@users.noreply.github.com> Date: Wed, 29 Apr 2026 15:47:23 -0600 Subject: [PATCH] Enhance programming language support in syntax highlighting by adding aliases and new languages (#9471) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Description `ProgrammingLanguage::to_extension` in `app/src/ai/agent/mod.rs` is the choke point for syntax highlighting in AI blocks. When the agent emits a markdown code fence, the language token becomes a `ProgrammingLanguage`, and `to_extension()` is called to construct a `snippet.` path that `set_language_with_path` uses to look up the tree-sitter grammar (see `app/src/ai/blocklist/block.rs:2607-2617`, `block.rs:2675`, and `block/cli.rs:732-740`). If `to_extension` returns `None`, `set_language_with_path` is never called and the block renders as plain text. The function omits a number of languages that the `languages` crate fully supports via `language_by_filename`, so common AI code-fence labels render with no syntax highlighting today: - `jsx`, `tsx` — every React snippet - `vue` — every Vue component snippet - `xml` — every config / Android / .NET snippet - `dockerfile` (and `docker`, `containerfile`) — every container snippet - `starlark`, `objective-c` (and `objc`) This PR adds those arms and folds in the most common markdown-fence aliases (`rs`, `py`, `js`, `ts`, `yml`, `kt`, `rb`, `golang`, `terraform`, `tf`) so coverage matches `language_by_filename`. The TODO comment at the top of the function (`INT-605: Refactor so we don't have to edit this function and the languages crate`) anticipates exactly this kind of sync. The `Self::Shell(Bash | Zsh | Fish)` arms are intentionally left as-is — only PowerShell currently exposes an extension, and extending the other shells looks like a separate behavior change. ## Testing - Added `test_programming_language_to_extension` in `app/src/ai/agent/mod_test.rs` covering 46 cases: every existing arm + every new arm + every alias + a round-trip note that each returned extension is one `language_by_filename` actually accepts. The new arms (`jsx`, `tsx`, `xml`, `vue`, `dockerfile`, `starlark`, `objective-c`, `objc`) and aliases (`rs`, `golang`, `py`, `js`, `ts`, `yml`, `c++`, `rb`, `kt`, `terraform`, `tf`, `docker`, `containerfile`) all fail on master and pass after the change. - `cargo fmt -p warp_terminal -- --check` passes locally. ## Agent Mode - [ ] Warp Agent Mode - This PR was created via Warp's AI Agent Mode ## Changelog Entries for Stable CHANGELOG-BUG-FIX: AI code blocks tagged `vue`, `xml`, `dockerfile`, `jsx`, `tsx`, `objective-c`, or `starlark` now render with syntax highlighting. Common aliases like `rs`, `py`, `js`, `ts`, `yml`, `kt`, `rb`, `golang`, `terraform`, and `docker` are also recognized. --- app/src/ai/agent/mod.rs | 29 +++++++++---- app/src/ai/agent/mod_test.rs | 80 ++++++++++++++++++++++++++++++++++++ 2 files changed, 100 insertions(+), 9 deletions(-) diff --git a/app/src/ai/agent/mod.rs b/app/src/ai/agent/mod.rs index 769d84d29..2ae96641e 100644 --- a/app/src/ai/agent/mod.rs +++ b/app/src/ai/agent/mod.rs @@ -717,13 +717,19 @@ impl ProgrammingLanguage { #[cfg_attr(target_family = "wasm", allow(unused))] pub fn to_extension(&self) -> Option<&str> { match self { + // The arms below cover both canonical language names emitted by the agent (e.g. + // "rust", "kotlin") and common markdown code-fence aliases (e.g. "rs", "kt") to keep + // syntax highlighting working when the model uses either. The set of recognized + // languages here is kept in sync with `SUPPORTED_LANGUAGES` in the `languages` crate. Self::Other(language) => match language.to_lowercase().as_str() { - "rust" => Some("rs"), - "go" => Some("go"), - "python" => Some("py"), - "javascript" => Some("js"), - "typescript" => Some("ts"), - "yaml" => Some("yaml"), + "rust" | "rs" => Some("rs"), + "go" | "golang" => Some("go"), + "python" | "py" => Some("py"), + "javascript" | "js" => Some("js"), + "typescript" | "ts" => Some("ts"), + "jsx" => Some("jsx"), + "tsx" => Some("tsx"), + "yaml" | "yml" => Some("yaml"), "cpp" | "c++" => Some("cpp"), "java" => Some("java"), "groovy" => Some("java"), @@ -733,17 +739,22 @@ impl ProgrammingLanguage { "css" => Some("css"), "c" => Some("c"), "json" => Some("json"), - "hcl" => Some("hcl"), + "hcl" | "terraform" | "tf" => Some("hcl"), "lua" => Some("lua"), - "ruby" => Some("rb"), + "ruby" | "rb" => Some("rb"), "php" => Some("php"), "toml" => Some("toml"), "swift" => Some("swift"), - "kotlin" => Some("kt"), + "kotlin" | "kt" => Some("kt"), "powershell" => Some("ps1"), "elixir" => Some("exs"), "scala" => Some("scala"), "sql" => Some("sql"), + "objective-c" | "objc" => Some("m"), + "starlark" => Some("bzl"), + "xml" => Some("xml"), + "vue" => Some("vue"), + "dockerfile" | "docker" | "containerfile" => Some("dockerfile"), _ => None, }, Self::Shell(ShellType::PowerShell) => Some("ps1"), diff --git a/app/src/ai/agent/mod_test.rs b/app/src/ai/agent/mod_test.rs index 10fcade97..889aca6dc 100644 --- a/app/src/ai/agent/mod_test.rs +++ b/app/src/ai/agent/mod_test.rs @@ -152,6 +152,86 @@ fn test_programming_language_from_string() { ); } +#[test] +fn test_programming_language_to_extension() { + // Each entry is (markdown language token, expected extension). The expected extension + // must resolve back to a recognized language via `languages::language_by_filename` so that + // syntax highlighting is applied to the AI block. + let cases: &[(&str, &str)] = &[ + // Canonical names. + ("rust", "rs"), + ("go", "go"), + ("python", "py"), + ("javascript", "js"), + ("typescript", "ts"), + ("yaml", "yaml"), + ("cpp", "cpp"), + ("java", "java"), + ("c#", "cs"), + ("csharp", "cs"), + ("html", "html"), + ("css", "css"), + ("c", "c"), + ("json", "json"), + ("hcl", "hcl"), + ("lua", "lua"), + ("ruby", "rb"), + ("php", "php"), + ("toml", "toml"), + ("swift", "swift"), + ("kotlin", "kt"), + ("powershell", "ps1"), + ("elixir", "exs"), + ("scala", "scala"), + ("sql", "sql"), + // Languages newly covered by this fix — previously fell through to None and rendered + // without syntax highlighting in AI blocks even though the `languages` crate supports them. + ("jsx", "jsx"), + ("tsx", "tsx"), + ("xml", "xml"), + ("vue", "vue"), + ("dockerfile", "dockerfile"), + ("starlark", "bzl"), + ("objective-c", "m"), + ("objc", "m"), + // Common markdown code-fence aliases. + ("rs", "rs"), + ("golang", "go"), + ("py", "py"), + ("js", "js"), + ("ts", "ts"), + ("yml", "yaml"), + ("c++", "cpp"), + ("rb", "rb"), + ("kt", "kt"), + ("terraform", "hcl"), + ("tf", "hcl"), + ("docker", "dockerfile"), + ("containerfile", "dockerfile"), + ]; + for (token, expected_extension) in cases { + let language = ProgrammingLanguage::from((*token).to_string()); + assert_eq!( + language.to_extension(), + Some(*expected_extension), + "expected to_extension({token:?}) to be Some({expected_extension:?})", + ); + } + + // PowerShell remains the only Shell variant whose extension is exposed; this preserves + // existing behavior for the other Shell variants which are intentionally not extended here. + assert_eq!( + ProgrammingLanguage::Shell(ShellType::PowerShell).to_extension(), + Some("ps1"), + ); + + // Unrecognized tokens still return None. + assert_eq!( + ProgrammingLanguage::Other("definitely-not-a-language".to_string()).to_extension(), + None, + ); +} + #[test] fn format_for_copy_preserves_visual_markdown_sections() { let output = AIAgentOutput {