diff --git a/Cargo.lock b/Cargo.lock index b30324650..25b4f6cd0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1264,5 +1264,7 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" name = "xtask" version = "0.5.0" dependencies = [ + "helix-core", "helix-term", + "toml", ] diff --git a/book/src/SUMMARY.md b/book/src/SUMMARY.md index 4da79925e..a8f165c01 100644 --- a/book/src/SUMMARY.md +++ b/book/src/SUMMARY.md @@ -4,6 +4,7 @@ # Summary - [Usage](./usage.md) - [Keymap](./keymap.md) - [Commands](./commands.md) + - [Language Support](./lang-support.md) - [Migrating from Vim](./from-vim.md) - [Configuration](./configuration.md) - [Themes](./themes.md) diff --git a/book/src/generated/lang-support.md b/book/src/generated/lang-support.md new file mode 100644 index 000000000..729801ad5 --- /dev/null +++ b/book/src/generated/lang-support.md @@ -0,0 +1,41 @@ +| Language | Syntax Highlighting | Treesitter Textobjects | Auto Indent | Default LSP | +| --- | --- | --- | --- | --- | +| Bash | ✓ | | | `bash-language-server` | +| C | ✓ | | | `clangd` | +| C# | ✓ | | | | +| CMake | ✓ | | | `cmake-language-server` | +| C++ | ✓ | | | `clangd` | +| CSS | ✓ | | | | +| Elixir | ✓ | | | `elixir-ls` | +| GLSL | ✓ | | ✓ | | +| Go | ✓ | ✓ | ✓ | `gopls` | +| HTML | ✓ | | | | +| Java | ✓ | | | | +| JavaScript | ✓ | | ✓ | | +| JSON | ✓ | | ✓ | | +| Julia | ✓ | | | `julia` | +| LaTeX | ✓ | | | | +| Ledger | ✓ | | | | +| LLVM | ✓ | | | | +| Lua | ✓ | | ✓ | | +| Mint | | | | `mint` | +| Nix | ✓ | | ✓ | `rnix-lsp` | +| OCaml | ✓ | | ✓ | | +| OCaml-Interface | ✓ | | | | +| Perl | ✓ | ✓ | | | +| PHP | ✓ | | ✓ | | +| Prolog | | | | `swipl` | +| Protobuf | ✓ | | ✓ | | +| Python | ✓ | ✓ | ✓ | `pylsp` | +| Racket | | | | `racket` | +| Ruby | ✓ | | | `solargraph` | +| Rust | ✓ | ✓ | ✓ | `rust-analyzer` | +| Svelte | ✓ | | ✓ | `svelteserver` | +| TOML | ✓ | | | | +| TSQ | ✓ | | | | +| TSX | ✓ | | | `typescript-language-server` | +| TypeScript | ✓ | | ✓ | `typescript-language-server` | +| Vue | ✓ | | | | +| WGSL | ✓ | | | | +| YAML | ✓ | | ✓ | | +| Zig | ✓ | | ✓ | `zls` | diff --git a/book/src/generated/typable-cmd.md b/book/src/generated/typable-cmd.md index 5de5c787d..bb21fd6b3 100644 --- a/book/src/generated/typable-cmd.md +++ b/book/src/generated/typable-cmd.md @@ -1,5 +1,5 @@ | Name | Description | -| --- | --- | +| --- | --- | | `:quit`, `:q` | Close the current view. | | `:quit!`, `:q!` | Close the current view forcefully (ignoring unsaved changes). | | `:open`, `:o` | Open a file from disk into the current view. | diff --git a/book/src/lang-support.md b/book/src/lang-support.md new file mode 100644 index 000000000..3920f3424 --- /dev/null +++ b/book/src/lang-support.md @@ -0,0 +1,10 @@ +# Language Support + +For more information like arguments passed to default LSP server, +extensions assosciated with a filetype, custom LSP settings, filetype +specific indent settings, etc see the default +[`languages.toml`][languages.toml] file. + +{{#include ./generated/lang-support.md}} + +[languages.toml]: https://github.com/helix-editor/helix/blob/master/languages.toml diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index 7b923db8c..bdd771aaf 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -23,7 +23,7 @@ # Auto generated documentation files, run ```shell -cargo xtask bookgen +cargo xtask docgen ``` inside the project. We use [xtask][xtask] as an ad-hoc task runner and diff --git a/helix-core/src/indent.rs b/helix-core/src/indent.rs index b6f5081ac..3ce3620a2 100644 --- a/helix-core/src/indent.rs +++ b/helix-core/src/indent.rs @@ -452,6 +452,7 @@ pub fn change(document: &Document, changes: I) -> Self file_types: vec!["rs".to_string()], shebangs: vec![], language_id: "Rust".to_string(), + display_name: "Rust".to_string(), highlight_config: OnceCell::new(), config: None, // diff --git a/helix-core/src/syntax.rs b/helix-core/src/syntax.rs index ba78adaaa..3c65ae33e 100644 --- a/helix-core/src/syntax.rs +++ b/helix-core/src/syntax.rs @@ -50,7 +50,8 @@ pub struct Configuration { #[serde(rename_all = "kebab-case", deny_unknown_fields)] pub struct LanguageConfiguration { #[serde(rename = "name")] - pub language_id: String, + pub language_id: String, // c-sharp, rust + pub display_name: String, // C#, Rust pub scope: String, // source.rust pub file_types: Vec, // filename ends_with? #[serde(default)] diff --git a/languages.toml b/languages.toml index 4208e4b68..ca339c98a 100644 --- a/languages.toml +++ b/languages.toml @@ -1,5 +1,6 @@ [[language]] name = "rust" +display-name = "Rust" scope = "source.rust" injection-regex = "rust" file-types = ["rs"] @@ -14,6 +15,7 @@ procMacro = { enable = false } [[language]] name = "toml" +display-name = "TOML" scope = "source.toml" injection-regex = "toml" file-types = ["toml"] @@ -24,6 +26,7 @@ indent = { tab-width = 2, unit = " " } [[language]] name = "protobuf" +display-name = "Protobuf" scope = "source.proto" injection-regex = "protobuf" file-types = ["proto"] @@ -34,6 +37,7 @@ indent = { tab-width = 2, unit = " " } [[language]] name = "elixir" +display-name = "Elixir" scope = "source.elixir" injection-regex = "elixir" file-types = ["ex", "exs"] @@ -46,6 +50,7 @@ indent = { tab-width = 2, unit = " " } [[language]] name = "mint" +display-name = "Mint" scope = "source.mint" injection-regex = "mint" file-types = ["mint"] @@ -58,6 +63,7 @@ indent = { tab-width = 2, unit = " " } [[language]] name = "json" +display-name = "JSON" scope = "source.json" injection-regex = "json" file-types = ["json"] @@ -67,6 +73,7 @@ indent = { tab-width = 2, unit = " " } [[language]] name = "c" +display-name = "C" scope = "source.c" injection-regex = "c" file-types = ["c"] # TODO: ["h"] @@ -78,6 +85,7 @@ indent = { tab-width = 2, unit = " " } [[language]] name = "cpp" +display-name = "C++" scope = "source.cpp" injection-regex = "cpp" file-types = ["cc", "hh", "cpp", "hpp", "h", "ipp", "tpp", "cxx", "hxx", "ixx", "txx", "ino"] @@ -89,6 +97,7 @@ indent = { tab-width = 2, unit = " " } [[language]] name = "c-sharp" +display-name = "C#" scope = "source.csharp" injection-regex = "c-?sharp" file-types = ["cs"] @@ -99,6 +108,7 @@ indent = { tab-width = 4, unit = "\t" } [[language]] name = "go" +display-name = "Go" scope = "source.go" injection-regex = "go" file-types = ["go"] @@ -112,6 +122,7 @@ indent = { tab-width = 4, unit = "\t" } [[language]] name = "javascript" +display-name = "JavaScript" scope = "source.js" injection-regex = "^(js|javascript)$" file-types = ["js", "mjs"] @@ -124,6 +135,7 @@ indent = { tab-width = 2, unit = " " } [[language]] name = "typescript" +display-name = "TypeScript" scope = "source.ts" injection-regex = "^(ts|typescript)$" file-types = ["ts"] @@ -136,6 +148,7 @@ indent = { tab-width = 2, unit = " " } [[language]] name = "tsx" +display-name = "TSX" scope = "source.tsx" injection-regex = "^(tsx)$" # |typescript file-types = ["tsx"] @@ -147,6 +160,7 @@ indent = { tab-width = 2, unit = " " } [[language]] name = "css" +display-name = "CSS" scope = "source.css" injection-regex = "css" file-types = ["css"] @@ -156,6 +170,7 @@ indent = { tab-width = 2, unit = " " } [[language]] name = "html" +display-name = "HTML" scope = "text.html.basic" injection-regex = "html" file-types = ["html"] @@ -165,6 +180,7 @@ indent = { tab-width = 2, unit = " " } [[language]] name = "python" +display-name = "Python" scope = "source.python" injection-regex = "python" file-types = ["py"] @@ -178,6 +194,7 @@ indent = { tab-width = 4, unit = " " } [[language]] name = "nix" +display-name = "Nix" scope = "source.nix" injection-regex = "nix" file-types = ["nix"] @@ -190,6 +207,7 @@ indent = { tab-width = 2, unit = " " } [[language]] name = "ruby" +display-name = "Ruby" scope = "source.ruby" injection-regex = "ruby" file-types = ["rb"] @@ -202,6 +220,7 @@ indent = { tab-width = 2, unit = " " } [[language]] name = "bash" +display-name = "Bash" scope = "source.bash" injection-regex = "bash" file-types = ["sh", "bash"] @@ -214,6 +233,7 @@ indent = { tab-width = 2, unit = " " } [[language]] name = "php" +display-name = "PHP" scope = "source.php" injection-regex = "php" file-types = ["php"] @@ -224,6 +244,7 @@ indent = { tab-width = 4, unit = " " } [[language]] name = "latex" +display-name = "LaTeX" scope = "source.tex" injection-regex = "tex" file-types = ["tex"] @@ -234,6 +255,7 @@ indent = { tab-width = 4, unit = "\t" } [[language]] name = "julia" +display-name = "Julia" scope = "source.julia" injection-regex = "julia" file-types = ["jl"] @@ -259,6 +281,7 @@ indent = { tab-width = 2, unit = " " } [[language]] name = "java" +display-name = "Java" scope = "source.java" injection-regex = "java" file-types = ["java"] @@ -267,6 +290,7 @@ indent = { tab-width = 4, unit = " " } [[language]] name = "ledger" +display-name = "Ledger" scope = "source.ledger" injection-regex = "ledger" file-types = ["ldg", "ledger", "journal"] @@ -276,6 +300,7 @@ indent = { tab-width = 4, unit = " " } [[language]] name = "ocaml" +display-name = "OCaml" scope = "source.ocaml" injection-regex = "ocaml" file-types = ["ml"] @@ -286,6 +311,7 @@ indent = { tab-width = 2, unit = " " } [[language]] name = "ocaml-interface" +display-name = "OCaml-Interface" scope = "source.ocaml.interface" file-types = ["mli"] shebangs = [] @@ -295,6 +321,7 @@ indent = { tab-width = 2, unit = " "} [[language]] name = "lua" +display-name = "Lua" scope = "source.lua" file-types = ["lua"] shebangs = ["lua"] @@ -304,6 +331,7 @@ indent = { tab-width = 2, unit = " " } [[language]] name = "svelte" +display-name = "Svelte" scope = "source.svelte" injection-regex = "svelte" file-types = ["svelte"] @@ -314,6 +342,7 @@ language-server = { command = "svelteserver", args = ["--stdio"] } [[language]] name = "vue" +display-name = "Vue" scope = "source.vue" injection-regex = "vue" file-types = ["vue"] @@ -322,6 +351,7 @@ indent = { tab-width = 2, unit = " " } [[language]] name = "yaml" +display-name = "YAML" scope = "source.yaml" file-types = ["yml", "yaml"] roots = [] @@ -330,6 +360,7 @@ indent = { tab-width = 2, unit = " " } # [[language]] # name = "haskell" +# display-name = "Haskell" # scope = "source.haskell" # injection-regex = "haskell" # file-types = ["hs"] @@ -340,6 +371,7 @@ indent = { tab-width = 2, unit = " " } [[language]] name = "zig" +display-name = "Zig" scope = "source.zig" injection-regex = "zig" file-types = ["zig"] @@ -352,6 +384,7 @@ indent = { tab-width = 4, unit = " " } [[language]] name = "prolog" +display-name = "Prolog" scope = "source.prolog" roots = [] file-types = ["pl", "prolog"] @@ -365,6 +398,7 @@ language-server = { command = "swipl", args = [ [[language]] name = "tsq" +display-name = "TSQ" scope = "source.tsq" file-types = ["scm"] roots = [] @@ -373,6 +407,7 @@ indent = { tab-width = 2, unit = " " } [[language]] name = "cmake" +display-name = "CMake" scope = "source.cmake" file-types = ["cmake", "CMakeLists.txt"] roots = [] @@ -382,6 +417,7 @@ language-server = { command = "cmake-language-server" } [[language]] name = "glsl" +display-name = "GLSL" scope = "source.glsl" file-types = ["glsl", "vert", "tesc", "tese", "geom", "frag", "comp" ] roots = [] @@ -390,6 +426,7 @@ indent = { tab-width = 4, unit = " " } [[language]] name = "perl" +display-name = "Perl" scope = "source.perl" file-types = ["pl", "pm"] shebangs = ["perl"] @@ -399,6 +436,7 @@ indent = { tab-width = 2, unit = " " } [[language]] name = "racket" +display-name = "Racket" scope = "source.rkt" roots = [] file-types = ["rkt"] @@ -408,6 +446,7 @@ language-server = { command = "racket", args = ["-l", "racket-langserver"] } [[language]] name = "wgsl" +display-name = "WGSL" scope = "source.wgsl" file-types = ["wgsl"] roots = [] @@ -416,6 +455,7 @@ indent = { tab-width = 4, unit = " " } [[language]] name = "llvm" +display-name = "LLVM" scope = "source.llvm" roots = [] file-types = ["ll"] diff --git a/xtask/Cargo.toml b/xtask/Cargo.toml index cb890de9d..fe5d55d40 100644 --- a/xtask/Cargo.toml +++ b/xtask/Cargo.toml @@ -7,3 +7,5 @@ edition = "2021" [dependencies] helix-term = { version = "0.5", path = "../helix-term" } +helix-core = { version = "0.5", path = "../helix-core" } +toml = "0.5" diff --git a/xtask/src/main.rs b/xtask/src/main.rs index 4bf0ae9f8..37e705928 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -1,17 +1,138 @@ -use std::env; +use std::{env, error::Error}; + +type DynError = Box; + +pub mod helpers { + use std::{ + fmt::Display, + path::{Path, PathBuf}, + }; + + use crate::path; + use helix_core::syntax::Configuration as LangConfig; + + #[derive(Copy, Clone)] + pub enum TsFeature { + Highlight, + TextObjects, + AutoIndent, + } + + impl TsFeature { + pub fn all() -> &'static [Self] { + &[Self::Highlight, Self::TextObjects, Self::AutoIndent] + } + + pub fn runtime_filename(&self) -> &'static str { + match *self { + Self::Highlight => "highlights.scm", + Self::TextObjects => "textobjects.scm", + Self::AutoIndent => "indents.toml", + } + } + } + + impl Display for TsFeature { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + match *self { + Self::Highlight => "Syntax Highlighting", + Self::TextObjects => "Treesitter Textobjects", + Self::AutoIndent => "Auto Indent", + } + ) + } + } + + /// Get the list of languages that support a particular tree-sitter + /// based feature. + pub fn ts_lang_support(feat: TsFeature) -> Vec { + let queries_dir = path::ts_queries(); + + find_files(&queries_dir, feat.runtime_filename()) + .iter() + .map(|f| { + // .../helix/runtime/queries/python/highlights.scm + let tail = f.strip_prefix(&queries_dir).unwrap(); // python/highlights.scm + let lang = tail.components().next().unwrap(); // python + lang.as_os_str().to_string_lossy().to_string() + }) + .collect() + } + + /// Get the list of languages that have any form of tree-sitter + /// queries defined in the runtime directory. + pub fn langs_with_ts_queries() -> Vec { + std::fs::read_dir(path::ts_queries()) + .unwrap() + .filter_map(|entry| { + let entry = entry.ok()?; + entry + .file_type() + .ok()? + .is_dir() + .then(|| entry.file_name().to_string_lossy().to_string()) + }) + .collect() + } + + // naive implementation, but suffices for our needs + pub fn find_files(dir: &Path, filename: &str) -> Vec { + std::fs::read_dir(dir) + .unwrap() + .filter_map(|entry| { + let path = entry.ok()?.path(); + if path.is_dir() { + Some(find_files(&path, filename)) + } else { + (path.file_name()?.to_string_lossy() == filename).then(|| vec![path]) + } + }) + .flatten() + .collect() + } + + pub fn lang_config() -> LangConfig { + let bytes = std::fs::read(path::lang_config()).unwrap(); + toml::from_slice(&bytes).unwrap() + } +} pub mod md_gen { - use super::path; + use crate::DynError; + + use crate::helpers; + use crate::path; use std::fs; use helix_term::commands::cmd::TYPABLE_COMMAND_LIST; pub const TYPABLE_COMMANDS_MD_OUTPUT: &str = "typable-cmd.md"; + pub const LANG_SUPPORT_MD_OUTPUT: &str = "lang-support.md"; - pub fn typable_commands() -> String { + fn md_table_heading(cols: &[String]) -> String { + let mut header = String::new(); + header += &md_table_row(cols); + header += &md_table_row(&vec!["---".to_string(); cols.len()]); + header + } + + fn md_table_row(cols: &[String]) -> String { + "| ".to_owned() + &cols.join(" | ") + " |\n" + } + + fn md_mono(s: &str) -> String { + format!("`{}`", s) + } + + pub fn typable_commands() -> Result { let mut md = String::new(); - md.push_str("| Name | Description |\n"); - md.push_str("| --- | --- |\n"); + md.push_str(&md_table_heading(&[ + "Name".to_owned(), + "Description".to_owned(), + ])); let cmdify = |s: &str| format!("`:{}`", s); @@ -22,11 +143,72 @@ pub fn typable_commands() -> String { .collect::>() .join(", "); - let entry = format!("| {} | {} |\n", names, cmd.doc); - md.push_str(&entry); + md.push_str(&md_table_row(&[names.to_owned(), cmd.doc.to_owned()])); } - md + Ok(md) + } + + pub fn lang_features() -> Result { + let mut md = String::new(); + let ts_features = helpers::TsFeature::all(); + + let mut cols = vec!["Language".to_owned()]; + cols.append( + &mut ts_features + .iter() + .map(|t| t.to_string()) + .collect::>(), + ); + cols.push("Default LSP".to_owned()); + + md.push_str(&md_table_heading(&cols)); + let config = helpers::lang_config(); + + let mut langs = config + .language + .iter() + .map(|l| l.language_id.clone()) + .collect::>(); + langs.sort_unstable(); + + let mut ts_features_to_langs = Vec::new(); + for &feat in ts_features { + ts_features_to_langs.push((feat, helpers::ts_lang_support(feat))); + } + + let mut row = Vec::new(); + for lang in langs { + let lc = config + .language + .iter() + .find(|l| l.language_id == lang) + .unwrap(); // lang comes from config + row.push(lc.display_name.clone()); + + for (_feat, support_list) in &ts_features_to_langs { + row.push( + if support_list.contains(&lang) { + "✓" + } else { + "" + } + .to_owned(), + ); + } + row.push( + lc.language_server + .as_ref() + .map(|s| s.command.clone()) + .map(|c| md_mono(&c)) + .unwrap_or_default(), + ); + + md.push_str(&md_table_row(&row)); + row.clear(); + } + + Ok(md) } pub fn write(filename: &str, data: &str) { @@ -49,37 +231,46 @@ pub fn project_root() -> PathBuf { pub fn book_gen() -> PathBuf { project_root().join("book/src/generated/") } + + pub fn ts_queries() -> PathBuf { + project_root().join("runtime/queries") + } + + pub fn lang_config() -> PathBuf { + project_root().join("languages.toml") + } } pub mod tasks { - use super::md_gen; + use crate::md_gen; + use crate::DynError; - pub fn bookgen() { - md_gen::write( - md_gen::TYPABLE_COMMANDS_MD_OUTPUT, - &md_gen::typable_commands(), - ); + pub fn docgen() -> Result<(), DynError> { + use md_gen::*; + write(TYPABLE_COMMANDS_MD_OUTPUT, &typable_commands()?); + write(LANG_SUPPORT_MD_OUTPUT, &lang_features()?); + Ok(()) } pub fn print_help() { println!( " -Usage: Run with `cargo xtask `, eg. `cargo xtask bookgen`. +Usage: Run with `cargo xtask `, eg. `cargo xtask docgen`. Tasks: - bookgen: Generate files to be included in the mdbook output. + docgen: Generate files to be included in the mdbook output. " ); } } -fn main() -> Result<(), String> { +fn main() -> Result<(), DynError> { let task = env::args().nth(1); match task { None => tasks::print_help(), Some(t) => match t.as_str() { - "bookgen" => tasks::bookgen(), - invalid => return Err(format!("Invalid task name: {}", invalid)), + "docgen" => tasks::docgen()?, + invalid => return Err(format!("Invalid task name: {}", invalid).into()), }, }; Ok(())