zed-industries / zed

Code at the speed of thought – Zed is a high-performance, multiplayer code editor from the creators of Atom and Tree-sitter.
https://zed.dev
Other
35.33k stars 1.79k forks source link

Add Haskell support (#5281) #6786

Closed pseudomata closed 4 months ago

pseudomata commented 4 months ago

This PR adds the Haskell tree-sitter grammar copied from nvim-treesitter. It also adds the Haskell Language Server.

This is a joint effort by myself (adding the grammar) and @leifu1128 (who is adding the language server integration).

This PR resolves https://github.com/zed-industries/zed/issues/5281

Release Notes:

cla-bot[bot] commented 4 months ago

We require contributors to sign our Contributor License Agreement, and we don't have @leifu1128 on file. You can sign our CLA at https://zed.dev/cla. Once you've signed, post a comment here that says '@cla-bot check'.

lei-rs commented 4 months ago

@cla-bot check

cla-bot[bot] commented 4 months ago

The cla-bot has been summoned, and re-checked this pull request!

pseudomata commented 4 months ago

Sweet, thanks @leifu1128! Marking this "Ready for review". There was an open question about which directory the LSP runs in (https://github.com/zed-industries/zed/issues/5281#issuecomment-1912226039) though I do not think that's a blocker and can likely be a follow-up (I don't have a strong preference).

pseudomata commented 4 months ago

Thank you for the review @mikayla-maki. I've switched back to using the highlights.scm from tree-sitter-haskell as the base and modified it to use the captures supported by Zed (thank you for that reference!).

With these changes I've taken screenshots of a random code selection that showcases most of the syntax in common use, comparison is with the Helix editor.

Screenshot showing syntax highlighting in Zed
Comparison screenshot showing syntax highlighting in Helix

(P.S. I just marked it back as ready for review and re-requested a review, I apologize if you meant to keep it in draft and I don't mean to be impatient here -- appreciate the review!)

maxdeviant commented 4 months ago

@pseudomata Have you had a chance to try out the LSP support in Zed?

@mikayla-maki and I are trying to test it out locally, but we're running into issues where it's not able to resolve the haskell-language-server for the right GHC version.

pseudomata commented 4 months ago

Thank you @mikayla-maki and @maxdeviant. I believe this might have been the issue @leifu1128 was running into as well. I would like to take a crack at fixing the issues and see if I can come up with a reasonable solution first! I'm going to dig around a little bit and see if I can get things working by looking at how VS Code handles this (which might have a similar environment).

If not, I'll convert this PR to cover syntax highlighting only.

mikayla-maki commented 4 months ago

Sounds good, and good luck @pseudomata!

pseudomata commented 4 months ago

Aside: I just realized why the LSP integration works fine for me -- it's because I have HLS installed via ghcup on my system, so haskell-language-server-wrapper is actually able to find the binary under ~/.ghcup/bin/. In order to reproduce the issue, removing HLS via ghcup rm hls 2.5.0.0 and re-running Zed will break things again.

mikayla-maki commented 4 months ago

@pseudomata we could just request users install it themselves in our documentation. I believe for nu and uiua, whose LSPs are integrated into their binary, you just have to have it in your PATH. If Haskell land is ok with that, it's a good middle ground I think.

pseudomata commented 4 months ago

I was actually just going to ask about this, I was looking at the Elixir implementation and copied that to expose a Haskell setting to do something similar, e.g. install HLS with ghcup and then adding this to editor configuration:

  "haskell": {
    "lsp": {
      "local": {
        "path": "~/.ghcup/hls/2.5.0.0/bin/haskell-language-server-wrapper",
        "arguments": ["lsp"]
      }
    }
  }

To play it safe, I added the default to be none so a fresh installation of Zed would open a Haskell project with syntax highlighting but no LSP integration. So far things are working for me:

CleanShot 2024-01-26 at 18 57 04@2x

Let me clean up my branch really quick and I can push that change up.

pseudomata commented 4 months ago

Let me know what you think! I see nu does this a bit differently (as opposed to exposing a setting), but considering we'd have to provide the path anyway which has the version in the path (~/.ghcup/hls/2.5.0.0/bin/haskell-language-server-wrapper) (EDIT: nvm, it's in the path already if using ghcup so we don't need to provide any configuration) my thoughts are this avoids an error with the default setting and is more explicit.

"haskell": {
  "lsp": "none"
}
But I'm happy to change it to match `nu` if that pattern is preferred. I have that change ready to go if it's preferred, I just tested it and it works as well. Click to see patch (didn't want to push and change the current state of the PR) ```diff diff --git a/assets/settings/default.json b/assets/settings/default.json index dda8b8af6..24923d6d3 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -300,7 +300,9 @@ "copilot": { // The set of glob patterns for which copilot should be disabled // in any matching file. - "disabled_globs": [".env"] + "disabled_globs": [ + ".env" + ] }, // Settings specific to journaling "journal": { @@ -409,7 +411,12 @@ // Default directories to search for virtual environments, relative // to the current working directory. We recommend overriding this // in your project's settings, rather than globally. - "directories": [".env", "env", ".venv", "venv"], + "directories": [ + ".env", + "env", + ".venv", + "venv" + ], // Can also be 'csh', 'fish', and `nushell` "activate_script": "default" } @@ -451,10 +458,6 @@ "deno": { "enable": false }, - // Settings specific to the Haskell integration - "haskell": { - "lsp": "none" - }, // Different settings for specific languages. "languages": { "Plain Text": { diff --git a/crates/zed/src/languages.rs b/crates/zed/src/languages.rs index 56f9d17f1..7bd47a14d 100644 --- a/crates/zed/src/languages.rs +++ b/crates/zed/src/languages.rs @@ -7,7 +7,7 @@ use settings::Settings; use std::{borrow::Cow, str, sync::Arc}; use util::{asset_str, paths::PLUGINS_DIR}; -use self::{deno::DenoSettings, elixir::ElixirSettings, haskell::HaskellSettings}; +use self::{deno::DenoSettings, elixir::ElixirSettings}; mod c; mod css; @@ -55,7 +55,6 @@ pub fn init( ) { ElixirSettings::register(cx); DenoSettings::register(cx); - HaskellSettings::register(cx); let language = |name, grammar, adapters| { languages.register(name, load_config(name), grammar, adapters, load_queries) @@ -204,19 +203,25 @@ pub fn init( } } - match &HaskellSettings::get(None, cx).lsp { - haskell::HaskellLspSetting::None => { - language("haskell", tree_sitter_haskell::language(), vec![]) - } - haskell::HaskellLspSetting::Local { path, arguments } => language( - "haskell", - tree_sitter_haskell::language(), - vec![Arc::new(haskell::LocalLspAdapter { - path: path.clone(), - arguments: arguments.clone(), - })], - ), - } + language( + "haskell", + tree_sitter_haskell::language(), + vec![Arc::new(haskell::HaskellLanguageServer {})], + ); + + // match &HaskellSettings::get(None, cx).lsp { + // haskell::HaskellLspSetting::None => { + // language("haskell", tree_sitter_haskell::language(), vec![]) + // } + // haskell::HaskellLspSetting::Local { path, arguments } => language( + // "haskell", + // tree_sitter_haskell::language(), + // vec![Arc::new(haskell::LocalLspAdapter { + // path: path.clone(), + // arguments: arguments.clone(), + // })], + // ), + // } language( "html", diff --git a/crates/zed/src/languages/haskell.rs b/crates/zed/src/languages/haskell.rs index 14a8ba798..666726770 100644 --- a/crates/zed/src/languages/haskell.rs +++ b/crates/zed/src/languages/haskell.rs @@ -1,83 +1,37 @@ -use anyhow::Result; +use anyhow::{anyhow, Result}; use async_trait::async_trait; use language::{LanguageServerName, LspAdapter, LspAdapterDelegate}; use lsp::LanguageServerBinary; -use schemars::JsonSchema; -use serde_derive::{Deserialize, Serialize}; -use settings::Settings; -use std::ops::Deref; use std::{any::Any, path::PathBuf}; -#[derive(Clone, Serialize, Deserialize, JsonSchema)] -pub struct HaskellSettings { - pub lsp: HaskellLspSetting, -} - -#[derive(Clone, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub enum HaskellLspSetting { - None, - Local { - path: String, - arguments: Vec, - }, -} - -#[derive(Clone, Serialize, Default, Deserialize, JsonSchema)] -pub struct HaskellSettingsContent { - lsp: Option, -} - -impl Settings for HaskellSettings { - const KEY: Option<&'static str> = Some("haskell"); - - type FileContent = HaskellSettingsContent; - - fn load( - default_value: &Self::FileContent, - user_values: &[&Self::FileContent], - _: &mut gpui::AppContext, - ) -> Result - where - Self: Sized, - { - Self::load_via_json_merge(default_value, user_values) - } -} - -pub struct LocalLspAdapter { - pub path: String, - pub arguments: Vec, -} +pub struct HaskellLanguageServer; #[async_trait] -impl LspAdapter for LocalLspAdapter { +impl LspAdapter for HaskellLanguageServer { fn name(&self) -> LanguageServerName { - LanguageServerName("local-hls".into()) + LanguageServerName("hls".into()) } fn short_name(&self) -> &'static str { - "local-hls" + "hls" } async fn fetch_latest_server_version( &self, _: &dyn LspAdapterDelegate, - ) -> Result> { - Ok(Box::new(()) as Box<_>) + ) -> Result> { + Ok(Box::new(())) } async fn fetch_server_binary( &self, - _: Box, - _: PathBuf, + _version: Box, + _container_dir: PathBuf, _: &dyn LspAdapterDelegate, ) -> Result { - let path = shellexpand::full(&self.path)?; - Ok(LanguageServerBinary { - path: PathBuf::from(path.deref()), - arguments: self.arguments.iter().map(|arg| arg.into()).collect(), - }) + Err(anyhow!( + "hls (haskell language server) must be installed via ghcup" + )) } async fn cached_server_binary( @@ -85,18 +39,17 @@ impl LspAdapter for LocalLspAdapter { _: PathBuf, _: &dyn LspAdapterDelegate, ) -> Option { - let path = shellexpand::full(&self.path).ok()?; Some(LanguageServerBinary { - path: PathBuf::from(path.deref()), - arguments: self.arguments.iter().map(|arg| arg.into()).collect(), + path: "haskell-language-server-wrapper".into(), + arguments: vec!["lsp".into()], }) } + fn can_be_reinstalled(&self) -> bool { + false + } + async fn installation_test_binary(&self, _: PathBuf) -> Option { - let path = shellexpand::full(&self.path).ok()?; - Some(LanguageServerBinary { - path: PathBuf::from(path.deref()), - arguments: self.arguments.iter().map(|arg| arg.into()).collect(), - }) + None } } ```

With this version if a user opens a Haskell project and doesn't have HLS installed, there's an error but otherwise things work fine:

failed to start language server "hls": No such file or directory (os error 2)
mikayla-maki commented 4 months ago

I think that the nu style with the useful error about hls would be best. No need for additional settings, smart default, works with the way most non-vscode haskell systems work. We can add more configuration options later if needed.

pseudomata commented 4 months ago

Sounds good to me, thanks @mikayla-maki. I just pushed those changes up.

mikayla-maki commented 4 months ago

Oh and I'm so sorry, I forgot to mention this earlier, do you think you could add an outline.scm file? It's how a language integrates with Zed's buffer symbol search (cmd-shift-o).

This PR is really great, thanks for working with us on this :D :D

pseudomata commented 4 months ago

Oh cool, I hadn't discovered that feature yet! Happy to add that, I think it might take a bit to figure out all the symbols to add for Haskell (I just picked up learning the language so I'm a bit of a noob). I'm signing off for the weekend but might find some time to work on it over the next few days and can ping this PR again when I have something working -- I can also commit to doing that in a follow-up. Whichever you prefer 🙂

Thanks for the all the feedback, and for open sourcing Zed!

mikayla-maki commented 4 months ago

Let's fast follow with it, y'all have put in the work for this one :). Thank you so much for the contribution!

lei-rs commented 4 months ago

Just woke up to see this, thanks for the hard work y'all. I can finally write more whitepapers.

BasLaa commented 3 months ago

Is this already out in the main version of Zed?

maxdeviant commented 3 months ago

Is this already out in the main version of Zed?

Yes, it is available in Zed Stable starting from Zed 0.121.5.

BasLaa commented 3 months ago

Amazing! Is it automatically integrated or do I need to install it somehow? I'm not getting type signatures highlighted yet in my code (just installed latest version today).

maxdeviant commented 3 months ago

It is currently bundled with part of Zed, but it will most likely move to an extension once we have language server support in extensions.

If it doesn't seem to be working then it might be a problem running the language server?

BasLaa commented 3 months ago

It could be that, I'm not really sure of how I should be 'running' the language server if I'm honest. On VSCode it works well though, does that indicate anything?

pseudomata commented 3 months ago

Hey @BasLaa, you need to have HLS installed (see installation instructions via ghcup) and then things should work. Though syntax highlighting should work out of the box without needing to install anything — is this not the case for you? Can you confirm which version of Zed you're running?

If you run into other issues feel free to contact me and I'll see if I can help 🙂

BasLaa commented 3 months ago

Hi @pseudomata, I have HLS installed via ghcup, although I do have multiple versions of HLS installed (2.4.0.0, 2.5.0.0, 2.6.0.0), that might interfere? Syntax highlighting for Haskell works for me in Zed, version is 0.123.6. I'm just trying out a Cabal project, and there I'm not getting any type signatures or hints, whereas I do get them in VSCode with the same project.

pseudomata commented 3 months ago

I don't think multiple versions should be causing an issue, but I think it might be worth trying removing the others and re-installing just one version. AFAIK the 'wrapper' binary will select the correct version.

Have you tried building the project first as well? It might also be helpful to see some of the debug logs and language server logs. Currently away from my computer so can't share exact steps to get logs but they're in the command panel.

Before stepping away I opened a Haskell project and tested things were working for me on the same version of Zed you're running, so at least there isn't a regression.

BasLaa commented 3 months ago

I tried removing all versions and reinstalling a single one, no change unfortunately. I also tried building the project. I believe I found the debug log for the LSP, but the log seems to be empty for me, so perhaps I'm looking at the wrong log?

pseudomata commented 3 months ago

There's also the Zed debug log (not related to LSP) which might be helpful to take a look at.