Skip to content

Commit

Permalink
add workspace config and manual LSP root management
Browse files Browse the repository at this point in the history
fixup documentation

Co-authored-by: LeoniePhiline <[email protected]>

fixup typo

Co-authored-by: LeoniePhiline <[email protected]>
  • Loading branch information
pascalkuthe and LeoniePhiline committed Mar 10, 2023
1 parent 3d230e7 commit 134ae32
Show file tree
Hide file tree
Showing 16 changed files with 293 additions and 181 deletions.
4 changes: 4 additions & 0 deletions book/src/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ You can use a custom configuration file by specifying it with the `-c` or
Additionally, you can reload the configuration file by sending the USR1
signal to the Helix process on Unix operating systems, such as by using the command `pkill -USR1 hx`.

Finally, you can have a `config.toml` local to a project by putting it under a `.helix` directory in your repository.
Its settings will be merged with the configuration directory `config.toml` and the built-in configuration.

## Editor

### `[editor]` Section
Expand Down Expand Up @@ -57,6 +60,7 @@ signal to the Helix process on Unix operating systems, such as by using the comm
| `bufferline` | Renders a line at the top of the editor displaying open buffers. Can be `always`, `never` or `multiple` (only shown if more than one buffer is in use) | `never` |
| `color-modes` | Whether to color the mode indicator with different colors depending on the mode itself | `false` |
| `text-width` | Maximum line length. Used for the `:reflow` command and soft-wrapping if `soft-wrap.wrap_at_text_width` is set | `80` |
| `workspace-lsp-roots` | Directories relative to the workspace root that are treated as LSP roots. Should only be set in `.helix/config.toml` | `[]` |

### `[editor.statusline]` Section

Expand Down
1 change: 1 addition & 0 deletions book/src/generated/typable-cmd.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@
| `:tree-sitter-subtree`, `:ts-subtree` | Display tree sitter subtree under cursor, primarily for debugging queries. |
| `:config-reload` | Refresh user config. |
| `:config-open` | Open the user config.toml file. |
| `:config-open-workspace` | Open the workspace config.toml file. |
| `:log-open` | Open the helix log file. |
| `:insert-output` | Run shell command, inserting output before each selection. |
| `:append-output` | Run shell command, appending output after each selection. |
Expand Down
1 change: 1 addition & 0 deletions book/src/languages.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ These configuration keys are available:
| `grammar` | The tree-sitter grammar to use (defaults to the value of `name`) |
| `formatter` | The formatter for the language, it will take precedence over the lsp when defined. The formatter must be able to take the original file as input from stdin and write the formatted file to stdout |
| `text-width` | Maximum line length. Used for the `:reflow` command and soft-wrapping if `soft-wrap.wrap_at_text_width` is set, defaults to `editor.text-width` |
| `workspace-lsp-roots` | Directories relative to the workspace root that are treated as LSP roots. Should only be set in `.helix/config.toml`. Overwrites the setting of the same name in `config.toml` if set. | `` |

### File-type detection and the `file-types` key

Expand Down
47 changes: 2 additions & 45 deletions helix-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,55 +36,12 @@ pub mod unicode {
pub use unicode_width as width;
}

pub use helix_loader::find_workspace;

pub fn find_first_non_whitespace_char(line: RopeSlice) -> Option<usize> {
line.chars().position(|ch| !ch.is_whitespace())
}

/// Find project root.
///
/// Order of detection:
/// * Top-most folder containing a root marker in current git repository
/// * Git repository root if no marker detected
/// * Top-most folder containing a root marker if not git repository detected
/// * Current working directory as fallback
pub fn find_root(root: Option<&str>, root_markers: &[String]) -> std::path::PathBuf {
let current_dir = std::env::current_dir().expect("unable to determine current directory");

let root = match root {
Some(root) => {
let root = std::path::Path::new(root);
if root.is_absolute() {
root.to_path_buf()
} else {
current_dir.join(root)
}
}
None => current_dir.clone(),
};

let mut top_marker = None;
for ancestor in root.ancestors() {
if root_markers
.iter()
.any(|marker| ancestor.join(marker).exists())
{
top_marker = Some(ancestor);
}

if ancestor.join(".git").exists() {
// Top marker is repo root if not root marker was detected yet
if top_marker.is_none() {
top_marker = Some(ancestor);
}
// Don't go higher than repo if we're in one
break;
}
}

// Return the found top marker or the current_dir as fallback
top_marker.map_or(current_dir, |a| a.to_path_buf())
}

pub use ropey::{self, str_utils, Rope, RopeBuilder, RopeSlice};

// pub use tendril::StrTendril as Tendril;
Expand Down
6 changes: 5 additions & 1 deletion helix-core/src/syntax.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ use std::{
fmt,
hash::{Hash, Hasher},
mem::{replace, transmute},
path::Path,
path::{Path, PathBuf},
str::FromStr,
sync::Arc,
};
Expand Down Expand Up @@ -127,6 +127,10 @@ pub struct LanguageConfiguration {
pub auto_pairs: Option<AutoPairs>,

pub rulers: Option<Vec<u16>>, // if set, override editor's rulers

/// Hardcoded LSP root directories relative to the workspace root, like `examples` or `tools/fuzz`.
/// Falling back to the current working directory if none are configured.
pub workspace_lsp_roots: Option<Vec<PathBuf>>,
}

#[derive(Debug, PartialEq, Eq, Hash)]
Expand Down
8 changes: 3 additions & 5 deletions helix-loader/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,8 @@ pub fn default_lang_config() -> toml::Value {

/// User configured languages.toml file, merged with the default config.
pub fn user_lang_config() -> Result<toml::Value, toml::de::Error> {
let config = crate::local_config_dirs()
let config = [crate::config_dir(), crate::find_workspace().join(".helix")]
.into_iter()
.chain([crate::config_dir()].into_iter())
.map(|path| path.join("languages.toml"))
.filter_map(|file| {
std::fs::read_to_string(file)
Expand All @@ -20,8 +19,7 @@ pub fn user_lang_config() -> Result<toml::Value, toml::de::Error> {
})
.collect::<Result<Vec<_>, _>>()?
.into_iter()
.chain([default_lang_config()].into_iter())
.fold(toml::Value::Table(toml::value::Table::default()), |a, b| {
.fold(default_lang_config(), |a, b| {
// combines for example
// b:
// [[language]]
Expand All @@ -38,7 +36,7 @@ pub fn user_lang_config() -> Result<toml::Value, toml::de::Error> {
// language-server = { command = "/usr/bin/taplo" }
//
// thus it overrides the third depth-level of b with values of a if they exist, but otherwise merges their values
crate::merge_toml_values(b, a, 3)
crate::merge_toml_values(a, b, 3)
});

Ok(config)
Expand Down
44 changes: 18 additions & 26 deletions helix-loader/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ fn prioritize_runtime_dirs() -> Vec<PathBuf> {
let mut rt_dirs = Vec::new();
if let Ok(dir) = std::env::var("CARGO_MANIFEST_DIR") {
// this is the directory of the crate being run by cargo, we need the workspace path so we take the parent
let path = std::path::PathBuf::from(dir).parent().unwrap().join(RT_DIR);
let path = PathBuf::from(dir).parent().unwrap().join(RT_DIR);
log::debug!("runtime dir: {}", path.to_string_lossy());
rt_dirs.push(path);
}
Expand Down Expand Up @@ -113,15 +113,6 @@ pub fn config_dir() -> PathBuf {
path
}

pub fn local_config_dirs() -> Vec<PathBuf> {
let directories = find_local_config_dirs()
.into_iter()
.map(|path| path.join(".helix"))
.collect();
log::debug!("Located configuration folders: {:?}", directories);
directories
}

pub fn cache_dir() -> PathBuf {
// TODO: allow env var override
let strategy = choose_base_strategy().expect("Unable to find the config directory!");
Expand All @@ -137,6 +128,10 @@ pub fn config_file() -> PathBuf {
.unwrap_or_else(|| config_dir().join("config.toml"))
}

pub fn workspace_config_file() -> PathBuf {
find_workspace().join(".helix").join("config.toml")
}

pub fn lang_config_file() -> PathBuf {
config_dir().join("languages.toml")
}
Expand All @@ -145,22 +140,6 @@ pub fn log_file() -> PathBuf {
cache_dir().join("helix.log")
}

pub fn find_local_config_dirs() -> Vec<PathBuf> {
let current_dir = std::env::current_dir().expect("unable to determine current directory");
let mut directories = Vec::new();

for ancestor in current_dir.ancestors() {
if ancestor.join(".git").exists() {
directories.push(ancestor.to_path_buf());
// Don't go higher than repo if we're in one
break;
} else if ancestor.join(".helix").is_dir() {
directories.push(ancestor.to_path_buf());
}
}
directories
}

/// Merge two TOML documents, merging values from `right` onto `left`
///
/// When an array exists in both `left` and `right`, `right`'s array is
Expand Down Expand Up @@ -302,3 +281,16 @@ mod merge_toml_tests {
)
}
}

/// Finds the current workspace folder.
/// Used as a ceiling dir for root resolve, for the filepicker and other related
pub fn find_workspace() -> PathBuf {
let current_dir = std::env::current_dir().expect("unable to determine current directory");
for ancestor in current_dir.ancestors() {
if ancestor.join(".git").exists() || ancestor.join(".helix").exists() {
return ancestor.to_owned();
}
}

current_dir
}
12 changes: 8 additions & 4 deletions helix-lsp/src/client.rs
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
use crate::{
jsonrpc,
find_root, jsonrpc,
transport::{Payload, Transport},
Call, Error, OffsetEncoding, Result,
};

use helix_core::{find_root, ChangeSet, Rope};
use helix_core::{ChangeSet, Rope};
use helix_loader::{self, VERSION_AND_GIT_HASH};
use lsp::PositionEncodingKind;
use lsp_types as lsp;
use serde::Deserialize;
use serde_json::Value;
use std::collections::HashMap;
use std::future::Future;
use std::process::Stdio;
use std::sync::{
atomic::{AtomicU64, Ordering},
Arc,
};
use std::{collections::HashMap, path::PathBuf};
use tokio::{
io::{BufReader, BufWriter},
process::{Child, Command},
Expand Down Expand Up @@ -49,6 +49,7 @@ impl Client {
config: Option<Value>,
server_environment: HashMap<String, String>,
root_markers: &[String],
manual_roots: &[PathBuf],
id: usize,
req_timeout: u64,
doc_path: Option<&std::path::PathBuf>,
Expand Down Expand Up @@ -77,8 +78,11 @@ impl Client {
Transport::start(reader, writer, stderr, id);

let root_path = find_root(
doc_path.and_then(|x| x.parent().and_then(|x| x.to_str())),
doc_path
.and_then(|x| x.parent().and_then(|x| x.to_str()))
.unwrap_or("."),
root_markers,
manual_roots,
);

let root_uri = lsp::Url::from_file_path(root_path.clone()).ok();
Expand Down
56 changes: 53 additions & 3 deletions helix-lsp/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,15 @@ pub use lsp::{Position, Url};
pub use lsp_types as lsp;

use futures_util::stream::select_all::SelectAll;
use helix_core::syntax::{LanguageConfiguration, LanguageServerConfiguration};
use helix_core::{
find_workspace,
syntax::{LanguageConfiguration, LanguageServerConfiguration},
};
use tokio::sync::mpsc::UnboundedReceiver;

use std::{
collections::{hash_map::Entry, HashMap},
path::PathBuf,
sync::{
atomic::{AtomicUsize, Ordering},
Arc,
Expand Down Expand Up @@ -640,6 +644,7 @@ impl Registry {
&mut self,
language_config: &LanguageConfiguration,
doc_path: Option<&std::path::PathBuf>,
root_dirs: &[PathBuf],
) -> Result<Option<Arc<Client>>> {
let config = match &language_config.language_server {
Some(config) => config,
Expand All @@ -655,7 +660,7 @@ impl Registry {
let id = self.counter.fetch_add(1, Ordering::Relaxed);

let NewClientResult(client, incoming) =
start_client(id, language_config, config, doc_path)?;
start_client(id, language_config, config, doc_path, root_dirs)?;
self.incoming.push(UnboundedReceiverStream::new(incoming));

let (_, old_client) = entry.insert((id, client.clone()));
Expand Down Expand Up @@ -683,6 +688,7 @@ impl Registry {
&mut self,
language_config: &LanguageConfiguration,
doc_path: Option<&std::path::PathBuf>,
root_dirs: &[PathBuf],
) -> Result<Option<Arc<Client>>> {
let config = match &language_config.language_server {
Some(config) => config,
Expand All @@ -696,7 +702,7 @@ impl Registry {
let id = self.counter.fetch_add(1, Ordering::Relaxed);

let NewClientResult(client, incoming) =
start_client(id, language_config, config, doc_path)?;
start_client(id, language_config, config, doc_path, root_dirs)?;
self.incoming.push(UnboundedReceiverStream::new(incoming));

entry.insert((id, client.clone()));
Expand Down Expand Up @@ -797,13 +803,15 @@ fn start_client(
config: &LanguageConfiguration,
ls_config: &LanguageServerConfiguration,
doc_path: Option<&std::path::PathBuf>,
root_dirs: &[PathBuf],
) -> Result<NewClientResult> {
let (client, incoming, initialize_notify) = Client::start(
&ls_config.command,
&ls_config.args,
config.config.clone(),
ls_config.environment.clone(),
&config.roots,
config.workspace_lsp_roots.as_deref().unwrap_or(root_dirs),
id,
ls_config.timeout,
doc_path,
Expand Down Expand Up @@ -841,6 +849,48 @@ fn start_client(
Ok(NewClientResult(client, incoming))
}

/// Find an LSP root of a file using the following mechansim:
/// * start at `file` (either an absolute path or relative to CWD)
/// * find the top most directory containing a root_marker
/// * inside the current workspace
/// * stop the search at the first root_dir that contains `file` or the workspace (obtained from `helix_core::find_workspace`)
/// * root_dirs only apply inside the workspace. For files outside of the workspace they are ignored
/// * outside the current workspace: keep searching to the top of the file hiearchy
pub fn find_root(file: &str, root_markers: &[String], root_dirs: &[PathBuf]) -> PathBuf {
let file = std::path::Path::new(file);
let workspace = find_workspace();
let file = if file.is_absolute() {
file.to_path_buf()
} else {
let current_dir = std::env::current_dir().expect("unable to determine current directory");
current_dir.join(file)
};

let inside_workspace = file.strip_prefix(&workspace).is_ok();

let mut top_marker = None;
for ancestor in file.ancestors() {
if root_markers
.iter()
.any(|marker| ancestor.join(marker).exists())
{
top_marker = Some(ancestor);
}

if inside_workspace
&& (ancestor == workspace
|| root_dirs
.iter()
.any(|root_dir| root_dir == ancestor.strip_prefix(&workspace).unwrap()))
{
return top_marker.unwrap_or(ancestor).to_owned();
}
}

// If no root was found use the workspace as a fallback
workspace
}

#[cfg(test)]
mod tests {
use super::{lsp, util::*, OffsetEncoding};
Expand Down
Loading

0 comments on commit 134ae32

Please sign in to comment.