├── rust-toolchain.toml ├── Cargo.toml ├── .gitignore ├── LICENSE ├── extension.toml ├── README.md └── src └── lib.rs /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "1.80" 3 | profile = "minimal" 4 | components = [ "rustfmt", "clippy" ] 5 | targets = [ "wasm32-wasi", "wasm32-wasip1" ] 6 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "typos" 3 | version = "0.0.4" 4 | edition = "2021" 5 | publish = false 6 | 7 | [lib] 8 | crate-type = ["cdylib"] 9 | 10 | [dependencies] 11 | zed_extension_api = "0.1.0" 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | debug/ 4 | target/ 5 | *.wasm 6 | 7 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 8 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 9 | Cargo.lock 10 | 11 | # These are backup files generated by rustfmt 12 | **/*.rs.bk 13 | 14 | # MSVC Windows builds of rustc generate these, which store debugging information 15 | *.pdb 16 | 17 | # RustRover 18 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 19 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 20 | # and can be added to the global gitignore or merged into this file. For a more nuclear 21 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 22 | #.idea/ 23 | 24 | # Editor configuration 25 | .zed/ 26 | .vscode/ 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Baptiste Roseau 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /extension.toml: -------------------------------------------------------------------------------- 1 | id = "typos" 2 | name = "Typos spell checker" 3 | description = "Low false-positive source code spell checker." 4 | version = "0.0.5" 5 | schema_version = 1 6 | authors = ["Baptiste Roseau "] 7 | repository = "https://github.com/BaptisteRoseau/zed-typos" 8 | 9 | [language_servers.typos] 10 | name = "typos" 11 | languages = [ 12 | "AsciiDoc", 13 | "Astro", 14 | "Bash", 15 | "Biome", 16 | "C", 17 | "C#", 18 | "C++", 19 | "Clojure", 20 | "CSharp", 21 | "CSS", 22 | "CSV", 23 | "D", 24 | "Dart", 25 | "Deno", 26 | "Docker", 27 | "Elixir", 28 | "Elm", 29 | "Emmet", 30 | "Erlang", 31 | "Fish", 32 | "FSharp", 33 | "GDScript", 34 | "Git Commit", 35 | "Gleam", 36 | "GLSL", 37 | "Go", 38 | "GraphQL", 39 | "Groovy", 40 | "Haskell", 41 | "HEEX", 42 | "HTML", 43 | "Hy", 44 | "Idris", 45 | "Java", 46 | "JavaScript", 47 | "JSON", 48 | "JSONC", 49 | "Julia", 50 | "Kotlin", 51 | "LaTeX", 52 | "Lua", 53 | "Luau", 54 | "Makefile", 55 | "Markdown", 56 | "Nim", 57 | "Nix", 58 | "OCaml", 59 | "PHP", 60 | "Plain Text", 61 | "Prisma", 62 | "Proto", 63 | "PureScript", 64 | "Python", 65 | "R", 66 | "Racket", 67 | "Rego", 68 | "reST", 69 | "ReStructuredText", 70 | "Roc", 71 | "Ruby", 72 | "Rust", 73 | "Scala", 74 | "Scheme", 75 | "SCSS", 76 | "Shell Script", 77 | "SQL", 78 | "Svelte", 79 | "Swift", 80 | "TailwindCSS", 81 | "Terraform", 82 | "TOML", 83 | "TSX", 84 | "TypeScript", 85 | "Typst", 86 | "Uiua", 87 | "Vue", 88 | "WIT", 89 | "XML", 90 | "YAML", 91 | "Yarn", 92 | "Zig", 93 | ] 94 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Typos Language Server 2 | 3 | [Typos Language Server](https://github.com/tekumara/typos-lsp) support for Zed editor. 4 | 5 | Typos is a spell checking tool using a list of commonly known typos, allowing it to have fewer false-positives than dictionary-based tools like [CSpell](https://github.com/streetsidesoftware/cspell). 6 | This is a great alternative for CI/CD integration, but I would suggest using both. 7 | 8 | ## Installation 9 | 10 | You can install this extension directly through Zed's extension marketplace. 11 | 12 | ## Configuration 13 | 14 | The Typos extension can be configured through a `.typos.toml` configuration file, which reference can be found [here](https://github.com/crate-ci/typos/blob/master/docs/reference.md). 15 | 16 | Additionally, you can configure it in your Zed's settings with the following: 17 | 18 | ```javascript 19 | { 20 | "lsp": { 21 | "typos": { 22 | "initialization_options": { 23 | // Path to your typos config file, .typos.toml by default. 24 | "config": ".typos.toml", 25 | // Path to your typos-lsp executable, takes $PATH into account. 26 | "path": "typos-lsp", 27 | // Diagnostic severity within Zed. "Information" by default, can be: 28 | // "Error", "Hint", "Information", "Warning" 29 | "diagnosticSeverity": "Information", 30 | // Minimum logging level for the LSP, displayed in Zed's logs. "info" by default, can be: 31 | // "debug", "error", "info", "off", "trace", "warn" 32 | "logLevel": "info", 33 | // Traces the communication between ZED and the language server. Recommended for debugging only. "off" by default, can be: 34 | // "messages", "off", "verbose" 35 | "trace.server": "off" 36 | } 37 | } 38 | } 39 | } 40 | ``` 41 | 42 | **WARNING**: When modifying your Typos configuration either in `typos.toml` or `Cargo.toml` you will need to reload the workspace to take them into account. 43 | You do not need to reload when editing Zed's `settings.json`. 44 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::{fs, path::Path}; 2 | 3 | use zed_extension_api::{ 4 | self as zed, settings::LspSettings, Architecture, Command, LanguageServerId, Os, Result, 5 | Worktree, 6 | }; 7 | 8 | struct TyposBinary { 9 | path: String, 10 | args: Option>, 11 | } 12 | 13 | struct TyposExtension { 14 | cached_binary_path: Option, 15 | } 16 | 17 | impl TyposExtension { 18 | #[allow(dead_code)] 19 | pub const LANGUAGE_SERVER_ID: &'static str = "typos"; 20 | 21 | fn language_server_binary( 22 | &mut self, 23 | language_server_id: &LanguageServerId, 24 | worktree: &zed::Worktree, 25 | ) -> Result { 26 | if let Some(path) = worktree.which("typos-lsp") { 27 | return Ok(TyposBinary { 28 | path, 29 | args: Some(vec![]), 30 | }); 31 | } 32 | 33 | if let Some(path) = &self.cached_binary_path { 34 | if fs::metadata(path).map_or(false, |stat| stat.is_file()) { 35 | return Ok(TyposBinary { 36 | path: path.clone(), 37 | args: Some(vec![]), 38 | }); 39 | } 40 | } 41 | 42 | zed::set_language_server_installation_status( 43 | language_server_id, 44 | &zed::LanguageServerInstallationStatus::CheckingForUpdate, 45 | ); 46 | let release = zed::latest_github_release( 47 | "tekumara/typos-lsp", 48 | zed::GithubReleaseOptions { 49 | require_assets: true, 50 | pre_release: false, 51 | }, 52 | )?; 53 | 54 | let (platform, architecture) = zed::current_platform(); 55 | let version = release.version; 56 | 57 | let asset_name = Self::binary_release_name(&version, &platform, &architecture); 58 | let asset = release 59 | .assets 60 | .iter() 61 | .find(|asset| asset.name == asset_name) 62 | .ok_or_else(|| format!("no asset found matching {:?}", asset_name))?; 63 | 64 | let version_dir = format!("typos-lsp-{}", version); 65 | let binary_path = Path::new(&version_dir) 66 | .join(Self::binary_path_within_archive(&platform, &architecture)) 67 | .to_str() 68 | .expect("Could not convert binary path to str") 69 | .to_string(); 70 | 71 | if !fs::metadata(&binary_path).map_or(false, |stat| stat.is_file()) { 72 | zed::set_language_server_installation_status( 73 | language_server_id, 74 | &zed::LanguageServerInstallationStatus::Downloading, 75 | ); 76 | let file_kind = match platform { 77 | zed::Os::Windows => zed::DownloadedFileType::Zip, 78 | _ => zed::DownloadedFileType::GzipTar, 79 | }; 80 | zed::download_file(&asset.download_url, &version_dir, file_kind) 81 | .map_err(|e| format!("failed to download file: {e}"))?; 82 | 83 | Self::clean_other_installations(&version_dir)?; 84 | } 85 | 86 | self.cached_binary_path = Some(binary_path.clone()); 87 | Ok(TyposBinary { 88 | path: binary_path, 89 | args: Some(vec![]), 90 | }) 91 | } 92 | 93 | /// The name of the archive found under the "Release" tabs of the GitHub repository, 94 | /// depending on the version, platform and architecture. 95 | fn binary_release_name(version: &String, platform: &Os, architecture: &Architecture) -> String { 96 | format!( 97 | "typos-lsp-{version}-{arch}-{os}.{ext}", 98 | version = version, 99 | arch = match architecture { 100 | Architecture::Aarch64 => "aarch64", 101 | Architecture::X86 | Architecture::X8664 => "x86_64", 102 | }, 103 | os = match platform { 104 | zed::Os::Mac => "apple-darwin", 105 | zed::Os::Linux => "unknown-linux-gnu", 106 | zed::Os::Windows => "pc-windows-msvc", 107 | }, 108 | ext = match platform { 109 | zed::Os::Windows => "zip", 110 | _ => "tar.gz", 111 | } 112 | ) 113 | } 114 | 115 | /// The path of the binary inside the archive. 116 | fn binary_path_within_archive(platform: &Os, architecture: &Architecture) -> String { 117 | let path = match platform { 118 | zed::Os::Windows => Path::new("target") 119 | .join(format!( 120 | "{arch}-pc-windows-msvc", 121 | arch = match architecture { 122 | Architecture::Aarch64 => "aarch64", 123 | Architecture::X86 | Architecture::X8664 => "x86_64", 124 | }, 125 | )) 126 | .join("release") 127 | .join("typos-lsp.exe") 128 | .as_path() 129 | .to_owned(), 130 | _ => Path::new("typos-lsp").to_owned(), 131 | }; 132 | path.to_str() 133 | .expect("Could not convert binary path to str") 134 | .to_string() 135 | } 136 | 137 | /// Remove every typos-lsp version directories within its Zed extension directory, 138 | /// except for the version specified as [`version_to_keep`]. 139 | fn clean_other_installations(version_to_keep: &String) -> Result<(), String> { 140 | let entries = 141 | fs::read_dir(".").map_err(|e| format!("failed to list working directory {e}"))?; 142 | for entry in entries { 143 | let entry = entry.map_err(|e| format!("failed to load directory entry {e}"))?; 144 | if entry.file_name().to_str() != Some(version_to_keep) { 145 | fs::remove_dir_all(entry.path()).ok(); 146 | } 147 | } 148 | Ok(()) 149 | } 150 | } 151 | 152 | impl zed::Extension for TyposExtension { 153 | fn new() -> Self { 154 | Self { 155 | cached_binary_path: None, 156 | } 157 | } 158 | 159 | fn language_server_command( 160 | &mut self, 161 | language_server_id: &LanguageServerId, 162 | worktree: &Worktree, 163 | ) -> Result { 164 | let typos_binary = self.language_server_binary(language_server_id, worktree)?; 165 | 166 | Ok(zed::Command { 167 | command: typos_binary.path, 168 | args: typos_binary.args.unwrap(), 169 | env: Default::default(), 170 | }) 171 | } 172 | 173 | fn language_server_initialization_options( 174 | &mut self, 175 | server_id: &LanguageServerId, 176 | worktree: &zed_extension_api::Worktree, 177 | ) -> Result> { 178 | let settings = LspSettings::for_worktree(server_id.as_ref(), worktree) 179 | .ok() 180 | .and_then(|lsp_settings| lsp_settings.initialization_options.clone()) 181 | .unwrap_or_default(); 182 | Ok(Some(settings)) 183 | } 184 | 185 | fn language_server_workspace_configuration( 186 | &mut self, 187 | server_id: &LanguageServerId, 188 | worktree: &zed_extension_api::Worktree, 189 | ) -> Result> { 190 | let settings = LspSettings::for_worktree(server_id.as_ref(), worktree) 191 | .ok() 192 | .and_then(|lsp_settings| lsp_settings.settings.clone()) 193 | .unwrap_or_default(); 194 | Ok(Some(settings)) 195 | } 196 | } 197 | 198 | zed::register_extension!(TyposExtension); 199 | 200 | #[cfg(test)] 201 | mod tests { 202 | use zed_extension_api::{Architecture, Os}; 203 | 204 | use crate::TyposExtension; 205 | 206 | #[test] 207 | fn release_name() { 208 | assert_eq!( 209 | TyposExtension::binary_release_name( 210 | &"v0.1.23".to_string(), 211 | &Os::Mac, 212 | &Architecture::Aarch64 213 | ), 214 | "typos-lsp-v0.1.23-aarch64-apple-darwin.tar.gz".to_string() 215 | ); 216 | assert_eq!( 217 | TyposExtension::binary_release_name( 218 | &"v0.1.23".to_string(), 219 | &Os::Windows, 220 | &Architecture::Aarch64 221 | ), 222 | "typos-lsp-v0.1.23-aarch64-pc-windows-msvc.zip".to_string() 223 | ); 224 | assert_eq!( 225 | TyposExtension::binary_release_name( 226 | &"v0.1.23".to_string(), 227 | &Os::Linux, 228 | &Architecture::Aarch64 229 | ), 230 | "typos-lsp-v0.1.23-aarch64-unknown-linux-gnu.tar.gz".to_string() 231 | ); 232 | assert_eq!( 233 | TyposExtension::binary_release_name( 234 | &"v0.1.23".to_string(), 235 | &Os::Mac, 236 | &Architecture::X86 237 | ), 238 | "typos-lsp-v0.1.23-x86_64-apple-darwin.tar.gz".to_string() 239 | ); 240 | assert_eq!( 241 | TyposExtension::binary_release_name( 242 | &"v0.1.23".to_string(), 243 | &Os::Windows, 244 | &Architecture::X86 245 | ), 246 | "typos-lsp-v0.1.23-x86_64-pc-windows-msvc.zip".to_string() 247 | ); 248 | assert_eq!( 249 | TyposExtension::binary_release_name( 250 | &"v0.1.23".to_string(), 251 | &Os::Linux, 252 | &Architecture::X86 253 | ), 254 | "typos-lsp-v0.1.23-x86_64-unknown-linux-gnu.tar.gz".to_string() 255 | ); 256 | assert_eq!( 257 | TyposExtension::binary_release_name( 258 | &"v0.1.23".to_string(), 259 | &Os::Mac, 260 | &Architecture::X8664 261 | ), 262 | "typos-lsp-v0.1.23-x86_64-apple-darwin.tar.gz".to_string() 263 | ); 264 | assert_eq!( 265 | TyposExtension::binary_release_name( 266 | &"v0.1.23".to_string(), 267 | &Os::Windows, 268 | &Architecture::X8664 269 | ), 270 | "typos-lsp-v0.1.23-x86_64-pc-windows-msvc.zip".to_string() 271 | ); 272 | assert_eq!( 273 | TyposExtension::binary_release_name( 274 | &"v0.1.23".to_string(), 275 | &Os::Linux, 276 | &Architecture::X8664 277 | ), 278 | "typos-lsp-v0.1.23-x86_64-unknown-linux-gnu.tar.gz".to_string() 279 | ); 280 | } 281 | 282 | #[test] 283 | #[cfg(target_os = "linux")] 284 | fn binary_name_within_extension() { 285 | assert_eq!( 286 | TyposExtension::binary_path_within_archive(&Os::Mac, &Architecture::X8664), 287 | "typos-lsp".to_string() 288 | ); 289 | assert_eq!( 290 | TyposExtension::binary_path_within_archive(&Os::Windows, &Architecture::X8664), 291 | "target/x86_64-pc-windows-msvc/release/typos-lsp.exe".to_string() 292 | ); 293 | } 294 | } 295 | --------------------------------------------------------------------------------