Merge branch 'master' of github.com:helix-editor/helix

imgbot
trivernis 2 years ago
commit a6e16df9a3
Signed by: Trivernis
GPG Key ID: DFFFCC2C7A02DB45

16
Cargo.lock generated

@ -101,9 +101,9 @@ dependencies = [
[[package]]
name = "clipboard-win"
version = "4.4.1"
version = "4.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f3e1238132dc01f081e1cbb9dace14e5ef4c3a51ee244bd982275fb514605db"
checksum = "c4ab1b92798304eedc095b53942963240037c0516452cb11aeba709d420b2219"
dependencies = [
"error-code",
"str-buf",
@ -335,9 +335,9 @@ dependencies = [
[[package]]
name = "grep-regex"
version = "0.1.9"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "121553c9768c363839b92fc2d7cdbbad44a3b70e8d6e7b1b72b05c977527bd06"
checksum = "1345f8d33c89f2d5b081f2f2a41175adef9fd0bed2fea6a26c96c2deb027e58e"
dependencies = [
"aho-corasick",
"bstr",
@ -350,9 +350,9 @@ dependencies = [
[[package]]
name = "grep-searcher"
version = "0.1.8"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fbdbde90ba52adc240d2deef7b6ad1f99f53142d074b771fe9b7bede6c4c23d"
checksum = "48852bd08f9b4eb3040ecb6d2f4ade224afe880a9a0909c5563cc59fa67932cc"
dependencies = [
"bstr",
"bytecount",
@ -644,9 +644,9 @@ checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a"
[[package]]
name = "memmap2"
version = "0.3.1"
version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00b6c2ebff6180198788f5db08d7ce3bc1d0b617176678831a7510825973e357"
checksum = "3a79b39c93a7a5a27eeaf9a23b5ff43f1b9e0ad6b1cdd441140ae53c35613fc7"
dependencies = [
"libc",
]

@ -38,7 +38,7 @@ hidden = false
| `shell` | Shell to use when running external commands. | Unix: `["sh", "-c"]`<br/>Windows: `["cmd", "/C"]` |
| `line-number` | Line number display: `absolute` simply shows each line's number, while `relative` shows the distance from the current line. When unfocused or in insert mode, `relative` will still show absolute line numbers. | `absolute` |
| `cursorline` | Highlight all lines with a cursor. | `false` |
| `gutters` | Gutters to display: Available are `diagnostics` and `line-numbers`, note that `diagnostics` also includes other features like breakpoints | `["diagnostics", "line-numbers"]` |
| `gutters` | Gutters to display: Available are `diagnostics` and `line-numbers` and `padding`, note that `diagnostics` also includes other features like breakpoints | `["diagnostics", "line-numbers", "padding"]` |
| `auto-completion` | Enable automatic pop up of auto-completion. | `true` |
| `auto-format` | Enable automatic formatting on save. | `true` |
| `idle-timeout` | Time in milliseconds since last keypress before idle timers trigger. Used for autocompletion, set to 0 for instant. | `400` |
@ -48,19 +48,52 @@ hidden = false
| `rulers` | List of column positions at which to display the rulers. Can be overridden by language specific `rulers` in `languages.toml` file. | `[]` |
| `color-modes` | Whether to color the mode indicator with different colors depending on the mode itself | `false` |
### `[editor.statusline]` Section
Allows configuring the statusline at the bottom of the editor.
The configuration distinguishes between three areas of the status line:
`[ ... ... LEFT ... ... | ... ... ... ... CENTER ... ... ... ... | ... ... RIGHT ... ... ]`
Statusline elements can be defined as follows:
```toml
[editor.statusline]
left = ["mode", "spinner"]
center = ["file-name"]
right = ["diagnostics", "selections", "position", "file-encoding", "file-line-ending", "file-type"]
```
The following elements can be configured:
| Key | Description |
| ------ | ----------- |
| `mode` | The current editor mode (`NOR`/`INS`/`SEL`) |
| `spinner` | A progress spinner indicating LSP activity |
| `file-name` | The path/name of the opened file |
| `file-encoding` | The encoding of the opened file if it differs from UTF-8 |
| `file-line-ending` | The file line endings (CRLF or LF) |
| `file-type` | The type of the opened file |
| `diagnostics` | The number of warnings and/or errors |
| `selections` | The number of active selections |
| `position` | The cursor position |
### `[editor.lsp]` Section
| Key | Description | Default |
| --- | ----------- | ------- |
| `display-messages` | Display LSP progress messages below statusline[^1] | `false` |
| Key | Description | Default |
| --- | ----------- | ------- |
| `display-messages` | Display LSP progress messages below statusline[^1] | `false` |
| `auto-signature-help` | Enable automatic popup of signature help (parameter hints) | `true` |
| `display-signature-help-docs` | Display docs under signature help popup | `true` |
[^1]: A progress spinner is always shown in the statusline beside the file path.
[^1]: By default, a progress spinner is shown in the statusline beside the file path.
### `[editor.cursor-shape]` Section
Defines the shape of cursor in each mode. Note that due to limitations
of the terminal environment, only the primary cursor can change shape.
Valid values for these options are `block`, `bar`, `underline`, or `none`.
Valid values for these options are `block`, `bar`, `underline`, or `hidden`.
| Key | Description | Default |
| --- | ----------- | ------- |

@ -30,7 +30,7 @@
| git-diff | ✓ | | | |
| git-ignore | ✓ | | | |
| git-rebase | ✓ | | | |
| gleam | ✓ | ✓ | | |
| gleam | ✓ | ✓ | | `gleam` |
| glsl | ✓ | ✓ | ✓ | |
| go | ✓ | ✓ | ✓ | `gopls` |
| gomod | ✓ | | | `gopls` |
@ -85,6 +85,7 @@
| rust | ✓ | ✓ | ✓ | `rust-analyzer` |
| scala | ✓ | | ✓ | `metals` |
| scheme | ✓ | | | |
| scss | ✓ | | | `vscode-css-language-server` |
| solidity | ✓ | | | `solc` |
| sql | ✓ | | | |
| sshclientconfig | ✓ | | | |

@ -238,6 +238,7 @@ This layer is a kludge of mappings, mostly pickers.
| ----- | ----------- | ------- |
| `f` | Open file picker | `file_picker` |
| `b` | Open buffer picker | `buffer_picker` |
| `j` | Open jumplist picker | `jumplist_picker` |
| `k` | Show documentation for item under cursor in a [popup](#popup) (**LSP**) | `hover` |
| `s` | Open document symbol picker (**LSP**) | `symbol_picker` |
| `S` | Open workspace symbol picker (**LSP**) | `workspace_symbol_picker` |
@ -354,6 +355,7 @@ Keys to use within picker. Remapping currently not supported.
| `Enter` | Open selected |
| `Ctrl-s` | Open horizontally |
| `Ctrl-v` | Open vertically |
| `Ctrl-t` | Toggle preview |
| `Escape`, `Ctrl-c` | Close picker |
# Prompt

@ -0,0 +1,59 @@
## Checklist
Helix releases are versioned in the Calendar Versioning scheme:
`YY.0M(.MICRO)`, for example `22.05` for May of 2022. In these instructions
we'll use `<tag>` as a placeholder for the tag being published.
* Merge the changelog PR
* Tag and push
* `git tag -s -m "<tag>" -a <tag> && git push`
* Make sure to switch to master and pull first
* Edit the `VERSION` file and change the date to the next planned release
* Releases are planned to happen every two months, so `22.05` would change to `22.07`
* Wait for the Release CI to finish
* It will automatically turn the git tag into a GitHub release when it uploads artifacts
* Edit the new release
* Use `<tag>` as the title
* Link to the changelog and release notes
* Merge the release notes PR
* Download the macos and linux binaries and update the `sha256`s in the [homebrew formula]
* Use `sha256sum` on the downloaded `.tar.xz` files to determine the hash
* Link to the release notes in this-week-in-rust
* [Example PR](https://github.com/rust-lang/this-week-in-rust/pull/3300)
* Post to reddit
* [Example post](https://www.reddit.com/r/rust/comments/uzp5ze/helix_editor_2205_released/)
[homebrew formula]: https://github.com/helix-editor/homebrew-helix/blob/master/Formula/helix.rb
## Changelog Curation
The changelog is currently created manually by reading through commits in the
log since the last release. GitHub's compare view is a nice way to approach
this. For example when creating the 22.07 release notes, this compare link
may be used
```
https://github.com/helix-editor/helix/compare/22.05...master
```
Either side of the triple-dot may be replaced with an exact revision, so if
you wish to incrementally compile the changelog, you can tackle a weeks worth
or so, record the revision where you stopped, and use that as a starting point
next week:
```
https://github.com/helix-editor/helix/compare/7706a4a0d8b67b943c31d0c5f7b00d357b5d838d...master
```
A work-in-progress commit for a changelog might look like
[this example](https://github.com/helix-editor/helix/commit/831adfd4c709ca16b248799bfef19698d5175e55).
Not every PR or commit needs a blurb in the changelog. Each release section
tends to have a blurb that links to a GitHub comparison between release
versions for convenience:
> As usual, the following is a summary of each of the changes since the last
> release. For the full log, check out the git log.
Typically, small changes like dependencies or documentation updates, refactors,
or meta changes like GitHub Actions work are left out.

@ -328,29 +328,15 @@ fn read_query(language: &str, filename: &str) -> String {
let query = load_runtime_file(language, filename).unwrap_or_default();
// TODO: the collect() is not ideal
let inherits = INHERITS_REGEX
.captures_iter(&query)
.flat_map(|captures| {
// replaces all "; inherits <language>(,<language>)*" with the queries of the given language(s)
INHERITS_REGEX
.replace_all(&query, |captures: &regex::Captures| {
captures[1]
.split(',')
.map(str::to_owned)
.collect::<Vec<_>>()
.map(|language| format!("\n{}\n", read_query(language, filename)))
.collect::<String>()
})
.collect::<Vec<_>>();
if inherits.is_empty() {
return query;
}
let mut queries = inherits
.iter()
.map(|language| read_query(language, filename))
.collect::<Vec<_>>();
queries.push(query);
queries.concat()
.to_string()
}
impl LanguageConfiguration {

@ -322,6 +322,16 @@ impl Client {
content_format: Some(vec![lsp::MarkupKind::Markdown]),
..Default::default()
}),
signature_help: Some(lsp::SignatureHelpClientCapabilities {
signature_information: Some(lsp::SignatureInformationSettings {
documentation_format: Some(vec![lsp::MarkupKind::Markdown]),
parameter_information: Some(lsp::ParameterInformationSettings {
label_offset_support: Some(true),
}),
active_parameter_support: Some(true),
}),
..Default::default()
}),
rename: Some(lsp::RenameClientCapabilities {
dynamic_registration: Some(false),
prepare_support: Some(false),
@ -646,7 +656,12 @@ impl Client {
text_document: lsp::TextDocumentIdentifier,
position: lsp::Position,
work_done_token: Option<lsp::ProgressToken>,
) -> impl Future<Output = Result<Value>> {
) -> Option<impl Future<Output = Result<Value>>> {
let capabilities = self.capabilities.get().unwrap();
// Return early if signature help is not supported
capabilities.signature_help_provider.as_ref()?;
let params = lsp::SignatureHelpParams {
text_document_position_params: lsp::TextDocumentPositionParams {
text_document,
@ -657,7 +672,7 @@ impl Client {
// lsp::SignatureHelpContext
};
self.call::<lsp::request::SignatureHelpRequest>(params)
Some(self.call::<lsp::request::SignatureHelpRequest>(params))
}
pub fn text_document_hover(

@ -63,8 +63,8 @@ serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] }
# ripgrep for global search
grep-regex = "0.1.9"
grep-searcher = "0.1.8"
grep-regex = "0.1.10"
grep-searcher = "0.1.10"
# Remove once retain_mut lands in stable rust
retain_mut = "0.1.7"

@ -151,10 +151,7 @@ impl Application {
compositor.push(Box::new(overlayed(picker)));
} else {
let nr_of_files = args.files.len();
editor.open(first, Action::VerticalSplit)?;
// Because the line above already opens the first file, we can
// simply skip opening it a second time by using .skip(1) here.
for (file, pos) in args.files.into_iter().skip(1) {
for (i, (file, pos)) in args.files.into_iter().enumerate() {
if file.is_dir() {
return Err(anyhow::anyhow!(
"expected a path to file, found a directory. (to open a directory pass it as first argument)"
@ -166,6 +163,7 @@ impl Application {
// option. If neither of those two arguments are passed
// in, just load the files normally.
let action = match args.split {
_ if i == 0 => Action::VerticalSplit,
Some(Layout::Vertical) => Action::VerticalSplit,
Some(Layout::Horizontal) => Action::HorizontalSplit,
None => Action::Load,

@ -264,6 +264,7 @@ impl MappableCommand {
file_picker_in_current_directory, "Open file picker at current working directory",
code_action, "Perform code action",
buffer_picker, "Open buffer picker",
jumplist_picker, "Open jumplist picker",
symbol_picker, "Open symbol picker",
select_references_to_symbol_under_cursor, "Select symbol references",
workspace_symbol_picker, "Open workspace symbol picker",
@ -718,6 +719,8 @@ fn kill_to_line_start(cx: &mut Context) {
Range::new(head, anchor)
});
delete_selection_insert_mode(doc, view, &selection);
lsp::signature_help_impl(cx, SignatureHelpInvoked::Automatic);
}
fn kill_to_line_end(cx: &mut Context) {
@ -737,6 +740,8 @@ fn kill_to_line_end(cx: &mut Context) {
new_range
});
delete_selection_insert_mode(doc, view, &selection);
lsp::signature_help_impl(cx, SignatureHelpInvoked::Automatic);
}
fn goto_first_nonwhitespace(cx: &mut Context) {
@ -2306,6 +2311,87 @@ fn buffer_picker(cx: &mut Context) {
cx.push_layer(Box::new(overlayed(picker)));
}
fn jumplist_picker(cx: &mut Context) {
struct JumpMeta {
id: DocumentId,
path: Option<PathBuf>,
selection: Selection,
text: String,
is_current: bool,
}
impl ui::menu::Item for JumpMeta {
type Data = ();
fn label(&self, _data: &Self::Data) -> Spans {
let path = self
.path
.as_deref()
.map(helix_core::path::get_relative_path);
let path = match path.as_deref().and_then(Path::to_str) {
Some(path) => path,
None => SCRATCH_BUFFER_NAME,
};
let mut flags = Vec::new();
if self.is_current {
flags.push("*");
}
let flag = if flags.is_empty() {
"".into()
} else {
format!(" ({})", flags.join(""))
};
format!("{} {}{} {}", self.id, path, flag, self.text).into()
}
}
let new_meta = |view: &View, doc_id: DocumentId, selection: Selection| {
let doc = &cx.editor.documents.get(&doc_id);
let text = doc.map_or("".into(), |d| {
selection
.fragments(d.text().slice(..))
.map(Cow::into_owned)
.collect::<Vec<_>>()
.join(" ")
});
JumpMeta {
id: doc_id,
path: doc.and_then(|d| d.path().cloned()),
selection,
text,
is_current: view.doc == doc_id,
}
};
let picker = FilePicker::new(
cx.editor
.tree
.views()
.flat_map(|(view, _)| {
view.jumps
.get()
.iter()
.map(|(doc_id, selection)| new_meta(view, *doc_id, selection.clone()))
})
.collect(),
(),
|cx, meta, action| {
cx.editor.switch(meta.id, action);
let (view, doc) = current!(cx.editor);
doc.set_selection(view.id, meta.selection.clone());
},
|editor, meta| {
let doc = &editor.documents.get(&meta.id)?;
let line = meta.selection.primary().cursor_line(doc.text().slice(..));
Some((meta.path.clone()?, Some((line, line))))
},
);
cx.push_layer(Box::new(overlayed(picker)));
}
impl ui::menu::Item for MappableCommand {
type Data = ReverseKeymap;
@ -2439,7 +2525,8 @@ async fn make_format_callback(
Ok(call)
}
enum Open {
#[derive(PartialEq)]
pub enum Open {
Below,
Above,
}
@ -2837,6 +2924,9 @@ pub mod insert {
use helix_lsp::lsp;
// if ch matches signature_help char, trigger
let doc = doc_mut!(cx.editor);
// The language_server!() macro is not used here since it will
// print an "LSP not active for current buffer" message on
// every keypress.
let language_server = match doc.language_server() {
Some(language_server) => language_server,
None => return,
@ -2856,26 +2946,15 @@ pub mod insert {
{
// TODO: what if trigger is multiple chars long
let is_trigger = triggers.iter().any(|trigger| trigger.contains(ch));
// lsp doesn't tell us when to close the signature help, so we request
// the help information again after common close triggers which should
// return None, which in turn closes the popup.
let close_triggers = &[')', ';', '.'];
if is_trigger {
super::signature_help(cx);
if is_trigger || close_triggers.contains(&ch) {
super::signature_help_impl(cx, SignatureHelpInvoked::Automatic);
}
}
// SignatureHelp {
// signatures: [
// SignatureInformation {
// label: "fn open(&mut self, path: PathBuf, action: Action) -> Result<DocumentId, Error>",
// documentation: None,
// parameters: Some(
// [ParameterInformation { label: Simple("path: PathBuf"), documentation: None },
// ParameterInformation { label: Simple("action: Action"), documentation: None }]
// ),
// active_parameter: Some(0)
// }
// ],
// active_signature: None, active_parameter: Some(0)
// }
}
// The default insert hook: simply insert the character
@ -2910,7 +2989,6 @@ pub mod insert {
// this could also generically look at Transaction, but it's a bit annoying to look at
// Operation instead of Change.
for hook in &[language_server_completion, signature_help] {
// for hook in &[signature_help] {
hook(cx, c);
}
}
@ -3018,14 +3096,18 @@ pub mod insert {
pub fn delete_char_backward(cx: &mut Context) {
let count = cx.count();
let (view, doc) = current!(cx.editor);
let (view, doc) = current_ref!(cx.editor);
let text = doc.text().slice(..);
let indent_unit = doc.indent_unit();
let tab_size = doc.tab_width();
let auto_pairs = doc.auto_pairs(cx.editor);
let transaction =
Transaction::change_by_selection(doc.text(), doc.selection(view.id), |range| {
let pos = range.cursor(text);
if pos == 0 {
return (pos, pos, None);
}
let line_start_pos = text.line_to_char(range.cursor_line(text));
// consider to delete by indent level if all characters before `pos` are indent units.
let fragment = Cow::from(text.slice(line_start_pos..pos));
@ -3073,15 +3155,39 @@ pub mod insert {
(start, pos, None) // delete!
}
} else {
// delete char
(
graphemes::nth_prev_grapheme_boundary(text, pos, count),
pos,
None,
)
match (
text.get_char(pos.saturating_sub(1)),
text.get_char(pos),
auto_pairs,
) {
(Some(_x), Some(_y), Some(ap))
if range.is_single_grapheme(text)
&& ap.get(_x).is_some()
&& ap.get(_x).unwrap().close == _y =>
// delete both autopaired characters
{
(
graphemes::nth_prev_grapheme_boundary(text, pos, count),
graphemes::nth_next_grapheme_boundary(text, pos, count),
None,
)
}
_ =>
// delete 1 char
{
(
graphemes::nth_prev_grapheme_boundary(text, pos, count),
pos,
None,
)
}
}
}
});
let (view, doc) = current!(cx.editor);
doc.apply(&transaction, view.id);
lsp::signature_help_impl(cx, SignatureHelpInvoked::Automatic);
}
pub fn delete_char_forward(cx: &mut Context) {
@ -3098,6 +3204,8 @@ pub mod insert {
)
});
doc.apply(&transaction, view.id);
lsp::signature_help_impl(cx, SignatureHelpInvoked::Automatic);
}
pub fn delete_word_backward(cx: &mut Context) {
@ -3111,6 +3219,8 @@ pub mod insert {
exclude_cursor(text, next, range)
});
delete_selection_insert_mode(doc, view, &selection);
lsp::signature_help_impl(cx, SignatureHelpInvoked::Automatic);
}
pub fn delete_word_forward(cx: &mut Context) {
@ -3123,6 +3233,8 @@ pub mod insert {
.clone()
.transform(|range| movement::move_next_word_start(text, range, count));
delete_selection_insert_mode(doc, view, &selection);
lsp::signature_help_impl(cx, SignatureHelpInvoked::Automatic);
}
}
@ -4037,13 +4149,11 @@ fn split(cx: &mut Context, action: Action) {
let (view, doc) = current!(cx.editor);
let id = doc.id();
let selection = doc.selection(view.id).clone();
let offset = view.offset;
cx.editor.switch(id, action);
// match the selection in the previous view
let (view, doc) = current!(cx.editor);
view.offset = offset;
doc.set_selection(view.id, selection);
}

@ -6,18 +6,19 @@ use helix_lsp::{
};
use tui::text::{Span, Spans};
use super::{align_view, push_jump, Align, Context, Editor};
use super::{align_view, push_jump, Align, Context, Editor, Open};
use helix_core::{path, Selection};
use helix_view::{editor::Action, theme::Style};
use crate::{
compositor::{self, Compositor},
ui::{self, overlay::overlayed, FileLocation, FilePicker, Popup, PromptEvent},
ui::{
self, lsp::SignatureHelp, overlay::overlayed, FileLocation, FilePicker, Popup, PromptEvent,
},
};
use std::collections::BTreeMap;
use std::{borrow::Cow, path::PathBuf};
use std::{borrow::Cow, collections::BTreeMap, path::PathBuf, sync::Arc};
/// Gets the language server that is attached to a document, and
/// if it's not active displays a status message. Using this macro
@ -805,31 +806,116 @@ pub fn goto_reference(cx: &mut Context) {
);
}
#[derive(PartialEq)]
pub enum SignatureHelpInvoked {
Manual,
Automatic,
}
pub fn signature_help(cx: &mut Context) {
signature_help_impl(cx, SignatureHelpInvoked::Manual)
}
pub fn signature_help_impl(cx: &mut Context, invoked: SignatureHelpInvoked) {
let (view, doc) = current!(cx.editor);
let language_server = language_server!(cx.editor, doc);
let was_manually_invoked = invoked == SignatureHelpInvoked::Manual;
let language_server = match doc.language_server() {
Some(language_server) => language_server,
None => {
// Do not show the message if signature help was invoked
// automatically on backspace, trigger characters, etc.
if was_manually_invoked {
cx.editor
.set_status("Language server not active for current buffer");
}
return;
}
};
let offset_encoding = language_server.offset_encoding();
let pos = doc.position(view.id, offset_encoding);
let future = language_server.text_document_signature_help(doc.identifier(), pos, None);
let future = match language_server.text_document_signature_help(doc.identifier(), pos, None) {
Some(f) => f,
None => return,
};
cx.callback(
future,
move |_editor, _compositor, response: Option<lsp::SignatureHelp>| {
if let Some(signature_help) = response {
log::info!("{:?}", signature_help);
// signatures
// active_signature
// active_parameter
// render as:
// signature
// ----------
// doc
// with active param highlighted
move |editor, compositor, response: Option<lsp::SignatureHelp>| {
let config = &editor.config();
if !(config.lsp.auto_signature_help
|| SignatureHelp::visible_popup(compositor).is_some()
|| was_manually_invoked)
{
return;
}
let response = match response {
// According to the spec the response should be None if there
// are no signatures, but some servers don't follow this.
Some(s) if !s.signatures.is_empty() => s,
_ => {
compositor.remove(SignatureHelp::ID);
return;
}
};
let doc = doc!(editor);
let language = doc
.language()
.and_then(|scope| scope.strip_prefix("source."))
.unwrap_or("");
let signature = match response
.signatures
.get(response.active_signature.unwrap_or(0) as usize)
{
Some(s) => s,
None => return,
};
let mut contents = SignatureHelp::new(
signature.label.clone(),
language.to_string(),
Arc::clone(&editor.syn_loader),
);
let signature_doc = if config.lsp.display_signature_help_docs {
signature.documentation.as_ref().map(|doc| match doc {
lsp::Documentation::String(s) => s.clone(),
lsp::Documentation::MarkupContent(markup) => markup.value.clone(),
})
} else {
None
};
contents.set_signature_doc(signature_doc);
let active_param_range = || -> Option<(usize, usize)> {
let param_idx = signature
.active_parameter
.or(response.active_parameter)
.unwrap_or(0) as usize;
let param = signature.parameters.as_ref()?.get(param_idx)?;
match &param.label {
lsp::ParameterLabel::Simple(string) => {
let start = signature.label.find(string.as_str())?;
Some((start, start + string.len()))
}
lsp::ParameterLabel::LabelOffsets([start, end]) => {
Some((*start as usize, *end as usize))
}
}
};
contents.set_active_param_range(active_param_range());
let old_popup = compositor.find_id::<Popup<SignatureHelp>>(SignatureHelp::ID);
let popup = Popup::new(SignatureHelp::ID, contents)
.position(old_popup.and_then(|p| p.get_position()))
.position_bias(Open::Above)
.ignore_escape_key(true);
compositor.replace_or_push(SignatureHelp::ID, popup);
},
);
}
@ -885,9 +971,21 @@ pub fn hover(cx: &mut Context) {
}
pub fn rename_symbol(cx: &mut Context) {
ui::prompt(
let (view, doc) = current_ref!(cx.editor);
let text = doc.text().slice(..);
let primary_selection = doc.selection(view.id).primary();
let prefill = if primary_selection.len() > 1 {
primary_selection
} else {
use helix_core::textobject::{textobject_word, TextObject};
textobject_word(text, primary_selection, TextObject::Inside, 1, false)
}
.fragment(text)
.into();
ui::prompt_with_input(
cx,
"rename-to:".into(),
prefill,
None,
ui::completers::none,
move |cx: &mut compositor::Context, input: &str, event: PromptEvent| {

@ -413,12 +413,11 @@ fn set_line_ending(
// Attempt to parse argument as a line ending.
let line_ending = match arg {
// We check for CR first because it shares a common prefix with CRLF.
#[cfg(feature = "unicode-lines")]
arg if arg.starts_with("cr") => CR,
arg if arg.starts_with("crlf") => Crlf,
arg if arg.starts_with("lf") => LF,
#[cfg(feature = "unicode-lines")]
arg if arg.starts_with("cr") => CR,
#[cfg(feature = "unicode-lines")]
arg if arg.starts_with("ff") => FF,
#[cfg(feature = "unicode-lines")]
arg if arg.starts_with("nel") => Nel,
@ -1501,11 +1500,9 @@ fn run_shell_command(
format!("```sh\n{}\n```", output),
editor.syn_loader.clone(),
);
let mut popup = Popup::new("shell", contents);
popup.set_position(Some(helix_core::Position::new(
editor.cursor().0.unwrap_or_default().row,
2,
)));
let popup = Popup::new("shell", contents).position(Some(
helix_core::Position::new(editor.cursor().0.unwrap_or_default().row, 2),
));
compositor.replace_or_push("shell", popup);
});
Ok(call)

@ -150,6 +150,14 @@ impl Compositor {
self.layers.pop()
}
pub fn remove(&mut self, id: &'static str) -> Option<Box<dyn Component>> {
let idx = self
.layers
.iter()
.position(|layer| layer.id() == Some(id))?;
Some(self.layers.remove(idx))
}
pub fn handle_event(&mut self, event: Event, cx: &mut Context) -> bool {
// If it is a key event and a macro is being recorded, push the key event to the recording.
if let (Event::Key(key), Some((_, keys))) = (event, &mut cx.editor.macro_recording) {

@ -205,6 +205,7 @@ pub fn default() -> HashMap<Mode, Keymap> {
"f" => file_picker,
"F" => file_picker_in_current_directory,
"b" => buffer_picker,
"j" => jumplist_picker,
"s" => symbol_picker,
"S" => workspace_symbol_picker,
"g" => diagnostics_picker,

@ -85,6 +85,8 @@ pub struct Completion {
}
impl Completion {
pub const ID: &'static str = "completion";
pub fn new(
editor: &Editor,
items: Vec<CompletionItem>,
@ -214,7 +216,7 @@ impl Completion {
}
};
});
let popup = Popup::new("completion", menu);
let popup = Popup::new(Self::ID, menu);
let mut completion = Self {
popup,
start_offset,

@ -1,13 +1,12 @@
use crate::{
commands,
compositor::{Component, Context, EventResult},
key,
job, key,
keymap::{KeymapResult, Keymaps},
ui::{overlay::Overlay, Completion, Explorer, ProgressSpinners},
};
use helix_core::{
coords_at_pos, encoding,
graphemes::{
ensure_grapheme_boundary_next_byte, next_grapheme_boundary, prev_grapheme_boundary,
},
@ -17,7 +16,7 @@ use helix_core::{
LineEnding, Position, Range, Selection, Transaction,
};
use helix_view::{
document::{Mode, SCRATCH_BUFFER_NAME},
document::Mode,
editor::{CompleteAction, CursorShapeConfig},
graphics::{Color, CursorKind, Modifier, Rect, Style},
input::KeyEvent,
@ -29,6 +28,9 @@ use std::borrow::Cow;
use crossterm::event::{Event, MouseButton, MouseEvent, MouseEventKind};
use tui::buffer::Buffer as Surface;
use super::lsp::SignatureHelp;
use super::statusline;
pub struct EditorView {
pub keymaps: Keymaps,
on_next_key: Option<Box<dyn FnOnce(&mut commands::Context, KeyEvent)>>,
@ -163,7 +165,11 @@ impl EditorView {
.area
.clip_top(view.area.height.saturating_sub(1))
.clip_bottom(1); // -1 from bottom to remove commandline
self.render_statusline(editor, doc, view, statusline_area, surface, is_focused);
let mut context =
statusline::RenderContext::new(editor, doc, view, is_focused, &self.spinners);
statusline::render(&mut context, statusline_area, surface);
}
pub fn render_rulers(
@ -417,7 +423,11 @@ impl EditorView {
return;
}
for i in 0..(indent_level / tab_width as u16) {
let starting_indent = (offset.col / tab_width) as u16;
// TODO: limit to a max indent level too. It doesn't cause visual artifacts but it would avoid some
// extra loops if the code is deeply nested.
for i in starting_indent..(indent_level / tab_width as u16) {
surface.set_string(
viewport.x + (i * tab_width as u16) - offset.col as u16,
viewport.y + line,
@ -612,7 +622,7 @@ impl EditorView {
// avoid lots of small allocations by reusing a text buffer for each line
let mut text = String::with_capacity(8);
for (constructor, width) in &view.gutters {
for (constructor, width) in view.gutters() {
let gutter = constructor(editor, doc, view, theme, is_focused, *width);
text.reserve(*width); // ensure there's enough space for the gutter
for (i, line) in (view.offset.row..(last_line + 1)).enumerate() {
@ -732,158 +742,6 @@ impl EditorView {
}
}
pub fn render_statusline(
&self,
editor: &Editor,
doc: &Document,
view: &View,
viewport: Rect,
surface: &mut Surface,
is_focused: bool,
) {
use tui::text::{Span, Spans};
//-------------------------------
// Left side of the status line.
//-------------------------------
let theme = &editor.theme;
let (mode, mode_style) = match doc.mode() {
Mode::Insert => (" INS ", theme.get("ui.statusline.insert")),
Mode::Select => (" SEL ", theme.get("ui.statusline.select")),
Mode::Normal => (" NOR ", theme.get("ui.statusline.normal")),
};
let progress = doc
.language_server()
.and_then(|srv| {
self.spinners
.get(srv.id())
.and_then(|spinner| spinner.frame())
})
.unwrap_or("");
let base_style = if is_focused {
theme.get("ui.statusline")
} else {
theme.get("ui.statusline.inactive")
};
// statusline
surface.set_style(viewport.with_height(1), base_style);
if is_focused {
let color_modes = editor.config().color_modes;
surface.set_string(
viewport.x,
viewport.y,
mode,
if color_modes { mode_style } else { base_style },
);
}
surface.set_string(viewport.x + 5, viewport.y, progress, base_style);
//-------------------------------
// Right side of the status line.
//-------------------------------
let mut right_side_text = Spans::default();
// Compute the individual info strings and add them to `right_side_text`.
// Diagnostics
let diags = doc.diagnostics().iter().fold((0, 0), |mut counts, diag| {
use helix_core::diagnostic::Severity;
match diag.severity {
Some(Severity::Warning) => counts.0 += 1,
Some(Severity::Error) | None => counts.1 += 1,
_ => {}
}
counts
});
let (warnings, errors) = diags;
let warning_style = theme.get("warning");
let error_style = theme.get("error");
for i in 0..2 {
let (count, style) = match i {
0 => (warnings, warning_style),
1 => (errors, error_style),
_ => unreachable!(),
};
if count == 0 {
continue;
}
let style = base_style.patch(style);
right_side_text.0.push(Span::styled("●", style));
right_side_text
.0
.push(Span::styled(format!(" {} ", count), base_style));
}
// Selections
let sels_count = doc.selection(view.id).len();
right_side_text.0.push(Span::styled(
format!(
" {} sel{} ",
sels_count,
if sels_count == 1 { "" } else { "s" }
),
base_style,
));
// Position
let pos = coords_at_pos(
doc.text().slice(..),
doc.selection(view.id)
.primary()
.cursor(doc.text().slice(..)),
);
right_side_text.0.push(Span::styled(
format!(" {}:{} ", pos.row + 1, pos.col + 1), // Convert to 1-indexing.
base_style,
));
let enc = doc.encoding();
if enc != encoding::UTF_8 {
right_side_text
.0
.push(Span::styled(format!(" {} ", enc.name()), base_style));
}
// Render to the statusline.
surface.set_spans(
viewport.x
+ viewport
.width
.saturating_sub(right_side_text.width() as u16),
viewport.y,
&right_side_text,
right_side_text.width() as u16,
);
//-------------------------------
// Middle / File path / Title
//-------------------------------
let title = {
let rel_path = doc.relative_path();
let path = rel_path
.as_ref()
.map(|p| p.to_string_lossy())
.unwrap_or_else(|| SCRATCH_BUFFER_NAME.into());
format!("{}{}", path, if doc.is_modified() { "[+]" } else { "" })
};
surface.set_string_truncated(
viewport.x + 8, // 8: 1 space + 3 char mode string + 1 space + 1 spinner + 1 space
viewport.y,
&title,
viewport
.width
.saturating_sub(6)
.saturating_sub(right_side_text.width() as u16 + 1) as usize, // "+ 1": a space between the title and the selection info
|_| base_style,
true,
true,
);
}
/// Handle events by looking them up in `self.keymaps`. Returns None
/// if event was handled (a command was executed or a subkeymap was
/// activated). Only KeymapResult::{NotFound, Cancelled} is returned
@ -1359,10 +1217,21 @@ impl Component for EditorView {
_ => unimplemented!(),
};
self.last_insert.1.clear();
commands::signature_help_impl(
&mut cx,
commands::SignatureHelpInvoked::Automatic,
);
}
(Mode::Insert, Mode::Normal) => {
// if exiting insert mode, remove completion
self.completion = None;
// TODO: Use an on_mode_change hook to remove signature help
context.jobs.callback(async {
let call: job::Callback = Box::new(|_editor, compositor| {
compositor.remove(SignatureHelp::ID);
});
Ok(call)
});
}
_ => (),
}

@ -0,0 +1,133 @@
use std::sync::Arc;
use helix_core::syntax;
use helix_view::graphics::{Margin, Rect, Style};
use tui::buffer::Buffer;
use tui::widgets::{BorderType, Paragraph, Widget, Wrap};
use crate::compositor::{Component, Compositor, Context};
use crate::ui::Markdown;
use super::Popup;
pub struct SignatureHelp {
signature: String,
signature_doc: Option<String>,
/// Part of signature text
active_param_range: Option<(usize, usize)>,
language: String,
config_loader: Arc<syntax::Loader>,
}
impl SignatureHelp {
pub const ID: &'static str = "signature-help";
pub fn new(signature: String, language: String, config_loader: Arc<syntax::Loader>) -> Self {
Self {
signature,
signature_doc: None,
active_param_range: None,
language,
config_loader,
}
}
pub fn set_signature_doc(&mut self, signature_doc: Option<String>) {
self.signature_doc = signature_doc;
}
pub fn set_active_param_range(&mut self, offset: Option<(usize, usize)>) {
self.active_param_range = offset;
}
pub fn visible_popup(compositor: &mut Compositor) -> Option<&mut Popup<Self>> {
compositor.find_id::<Popup<Self>>(Self::ID)
}
}
impl Component for SignatureHelp {
fn render(&mut self, area: Rect, surface: &mut Buffer, cx: &mut Context) {
let margin = Margin::horizontal(1);
let active_param_span = self.active_param_range.map(|(start, end)| {
vec![(
cx.editor.theme.find_scope_index("ui.selection").unwrap(),
start..end,
)]
});
let sig_text = crate::ui::markdown::highlighted_code_block(
self.signature.clone(),
&self.language,
Some(&cx.editor.theme),
Arc::clone(&self.config_loader),
active_param_span,
);
let (_, sig_text_height) = crate::ui::text::required_size(&sig_text, area.width);
let sig_text_area = area.clip_top(1).with_height(sig_text_height);
let sig_text_para = Paragraph::new(sig_text).wrap(Wrap { trim: false });
sig_text_para.render(sig_text_area.inner(&margin), surface);
if self.signature_doc.is_none() {
return;
}
let sep_style = Style::default();
let borders = BorderType::line_symbols(BorderType::Plain);
for x in sig_text_area.left()..sig_text_area.right() {
if let Some(cell) = surface.get_mut(x, sig_text_area.bottom()) {
cell.set_symbol(borders.horizontal).set_style(sep_style);
}
}
let sig_doc = match &self.signature_doc {
None => return,
Some(doc) => Markdown::new(doc.clone(), Arc::clone(&self.config_loader)),
};
let sig_doc = sig_doc.parse(Some(&cx.editor.theme));
let sig_doc_area = area.clip_top(sig_text_area.height + 2);
let sig_doc_para = Paragraph::new(sig_doc)
.wrap(Wrap { trim: false })
.scroll((cx.scroll.unwrap_or_default() as u16, 0));
sig_doc_para.render(sig_doc_area.inner(&margin), surface);
}
fn required_size(&mut self, viewport: (u16, u16)) -> Option<(u16, u16)> {
const PADDING: u16 = 2;
const SEPARATOR_HEIGHT: u16 = 1;
if PADDING >= viewport.1 || PADDING >= viewport.0 {
return None;
}
let max_text_width = (viewport.0 - PADDING).min(120);
let signature_text = crate::ui::markdown::highlighted_code_block(
self.signature.clone(),
&self.language,
None,
Arc::clone(&self.config_loader),
None,
);
let (sig_width, sig_height) =
crate::ui::text::required_size(&signature_text, max_text_width);
let (width, height) = match self.signature_doc {
Some(ref doc) => {
let doc_md = Markdown::new(doc.clone(), Arc::clone(&self.config_loader));
let doc_text = doc_md.parse(None);
let (doc_width, doc_height) =
crate::ui::text::required_size(&doc_text, max_text_width);
(
sig_width.max(doc_width),
sig_height + SEPARATOR_HEIGHT + doc_height,
)
}
None => (sig_width, sig_height),
};
Some((width + PADDING, height + PADDING))
}
}

@ -144,7 +144,7 @@ impl Markdown {
}
}
fn parse(&self, theme: Option<&Theme>) -> tui::text::Text<'_> {
pub fn parse(&self, theme: Option<&Theme>) -> tui::text::Text<'_> {
fn push_line<'a>(spans: &mut Vec<Span<'a>>, lines: &mut Vec<Spans<'a>>) {
let spans = std::mem::take(spans);
if !spans.is_empty() {

@ -2,13 +2,15 @@ mod completion;
pub(crate) mod editor;
mod explore;
mod info;
pub mod lsp;
mod markdown;
pub mod menu;
pub mod overlay;
mod picker;
mod popup;
pub mod popup;
mod prompt;
mod spinner;
mod statusline;
mod text;
mod tree;
@ -37,7 +39,27 @@ pub fn prompt(
completion_fn: impl FnMut(&Editor, &str) -> Vec<prompt::Completion> + 'static,
callback_fn: impl FnMut(&mut crate::compositor::Context, &str, PromptEvent) + 'static,
) {
let mut prompt = Prompt::new(prompt, history_register, completion_fn, callback_fn);
show_prompt(
cx,
Prompt::new(prompt, history_register, completion_fn, callback_fn),
);
}
pub fn prompt_with_input(
cx: &mut crate::commands::Context,
prompt: std::borrow::Cow<'static, str>,
input: String,
history_register: Option<char>,
completion_fn: impl FnMut(&Editor, &str) -> Vec<prompt::Completion> + 'static,
callback_fn: impl FnMut(&mut crate::compositor::Context, &str, PromptEvent) + 'static,
) {
show_prompt(
cx,
Prompt::new(prompt, history_register, completion_fn, callback_fn).with_line(input),
);
}
fn show_prompt(cx: &mut crate::commands::Context, mut prompt: Prompt) {
// Calculate initial completion
prompt.recalculate_completion(cx.editor);
cx.push_layer(Box::new(prompt));

@ -173,7 +173,7 @@ impl<T: Item + 'static> Component for FilePicker<T> {
// | | | |
// +---------+ +---------+
let render_preview = area.width > MIN_AREA_WIDTH_FOR_PREVIEW;
let render_preview = self.picker.show_preview && area.width > MIN_AREA_WIDTH_FOR_PREVIEW;
// -- Render the frame:
// clear area
let background = cx.editor.theme.get("ui.background");
@ -300,6 +300,8 @@ pub struct Picker<T: Item> {
previous_pattern: String,
/// Whether to truncate the start (default true)
pub truncate_start: bool,
/// Whether to show the preview panel (default true)
show_preview: bool,
callback_fn: Box<dyn Fn(&mut Context, &T, Action)>,
}
@ -327,6 +329,7 @@ impl<T: Item> Picker<T> {
prompt,
previous_pattern: String::new(),
truncate_start: true,
show_preview: true,
callback_fn: Box::new(callback_fn),
completion_height: 0,
};
@ -470,6 +473,10 @@ impl<T: Item> Picker<T> {
self.filters.sort_unstable(); // used for binary search later
self.prompt.clear(cx);
}
pub fn toggle_preview(&mut self) {
self.show_preview = !self.show_preview;
}
}
// process:
@ -538,6 +545,9 @@ impl<T: Item + 'static> Component for Picker<T> {
ctrl!(' ') => {
self.save_filter(cx);
}
ctrl!('t') => {
self.toggle_preview();
}
_ => {
if let EventResult::Consumed(_) = self.prompt.handle_event(event, cx) {
// TODO: recalculate only if pattern changed

@ -1,4 +1,5 @@
use crate::{
commands::Open,
compositor::{Callback, Component, Context, EventResult},
ctrl, key,
};
@ -17,8 +18,10 @@ pub struct Popup<T: Component> {
margin: Margin,
size: (u16, u16),
child_size: (u16, u16),
position_bias: Open,
scroll: usize,
auto_close: bool,
ignore_escape_key: bool,
id: &'static str,
}
@ -29,15 +32,27 @@ impl<T: Component> Popup<T> {
position: None,
margin: Margin::none(),
size: (0, 0),
position_bias: Open::Below,
child_size: (0, 0),
scroll: 0,
auto_close: false,
ignore_escape_key: false,
id,
}
}
pub fn set_position(&mut self, pos: Option<Position>) {
pub fn position(mut self, pos: Option<Position>) -> Self {
self.position = pos;
self
}
pub fn get_position(&self) -> Option<Position> {
self.position
}
pub fn position_bias(mut self, bias: Open) -> Self {
self.position_bias = bias;
self
}
pub fn margin(mut self, margin: Margin) -> Self {
@ -50,6 +65,18 @@ impl<T: Component> Popup<T> {
self
}
/// Ignores an escape keypress event, letting the outer layer
/// (usually the editor) handle it. This is useful for popups
/// in insert mode like completion and signature help where
/// the popup is closed on the mode change from insert to normal
/// which is done with the escape key. Otherwise the popup consumes
/// the escape key event and closes it, and an additional escape
/// would be required to exit insert mode.
pub fn ignore_escape_key(mut self, ignore: bool) -> Self {
self.ignore_escape_key = ignore;
self
}
pub fn get_rel_position(&mut self, viewport: Rect, cx: &Context) -> (u16, u16) {
let position = self
.position
@ -68,13 +95,23 @@ impl<T: Component> Popup<T> {
rel_x = rel_x.saturating_sub((rel_x + width).saturating_sub(viewport.width));
}
// TODO: be able to specify orientation preference. We want above for most popups, below
// for menus/autocomplete.
if viewport.height > rel_y + height {
rel_y += 1 // position below point
} else {
rel_y = rel_y.saturating_sub(height) // position above point
}
let can_put_below = viewport.height > rel_y + height;
let can_put_above = rel_y.checked_sub(height).is_some();
let final_pos = match self.position_bias {
Open::Below => match can_put_below {
true => Open::Below,
false => Open::Above,
},
Open::Above => match can_put_above {
true => Open::Above,
false => Open::Below,
},
};
rel_y = match final_pos {
Open::Above => rel_y.saturating_sub(height),
Open::Below => rel_y + 1,
};
(rel_x, rel_y)
}
@ -112,9 +149,13 @@ impl<T: Component> Component for Popup<T> {
_ => return EventResult::Ignored(None),
};
if key!(Esc) == key.into() && self.ignore_escape_key {
return EventResult::Ignored(None);
}
let close_fn: Callback = Box::new(|compositor, _| {
// remove the layer
compositor.pop();
compositor.remove(self.id.as_ref());
});
match key.into() {

@ -84,6 +84,13 @@ impl Prompt {
}
}
pub fn with_line(mut self, line: String) -> Self {
let cursor = line.len();
self.line = line;
self.cursor = cursor;
self
}
pub fn line(&self) -> &String {
&self.line
}

@ -0,0 +1,336 @@
use helix_core::{coords_at_pos, encoding};
use helix_view::{
document::{Mode, SCRATCH_BUFFER_NAME},
graphics::Rect,
theme::Style,
Document, Editor, View,
};
use crate::ui::ProgressSpinners;
use helix_view::editor::StatusLineElement as StatusLineElementID;
use tui::buffer::Buffer as Surface;
use tui::text::{Span, Spans};
pub struct RenderContext<'a> {
pub editor: &'a Editor,
pub doc: &'a Document,
pub view: &'a View,
pub focused: bool,
pub spinners: &'a ProgressSpinners,
pub parts: RenderBuffer<'a>,
}
impl<'a> RenderContext<'a> {
pub fn new(
editor: &'a Editor,
doc: &'a Document,
view: &'a View,
focused: bool,
spinners: &'a ProgressSpinners,
) -> Self {
RenderContext {
editor,
doc,
view,
focused,
spinners,
parts: RenderBuffer::default(),
}
}
}
#[derive(Default)]
pub struct RenderBuffer<'a> {
pub left: Spans<'a>,
pub center: Spans<'a>,
pub right: Spans<'a>,
}
pub fn render(context: &mut RenderContext, viewport: Rect, surface: &mut Surface) {
let base_style = if context.focused {
context.editor.theme.get("ui.statusline")
} else {
context.editor.theme.get("ui.statusline.inactive")
};
surface.set_style(viewport.with_height(1), base_style);
let write_left = |context: &mut RenderContext, text, style| {
append(&mut context.parts.left, text, &base_style, style)
};
let write_center = |context: &mut RenderContext, text, style| {
append(&mut context.parts.center, text, &base_style, style)
};
let write_right = |context: &mut RenderContext, text, style| {
append(&mut context.parts.right, text, &base_style, style)
};
// Left side of the status line.
let element_ids = &context.editor.config().statusline.left;
element_ids
.iter()
.map(|element_id| get_render_function(*element_id))
.for_each(|render| render(context, write_left));
surface.set_spans(
viewport.x,
viewport.y,
&context.parts.left,
context.parts.left.width() as u16,
);
// Right side of the status line.
let element_ids = &context.editor.config().statusline.right;
element_ids
.iter()
.map(|element_id| get_render_function(*element_id))
.for_each(|render| render(context, write_right));
surface.set_spans(
viewport.x
+ viewport
.width
.saturating_sub(context.parts.right.width() as u16),
viewport.y,
&context.parts.right,
context.parts.right.width() as u16,
);
// Center of the status line.
let element_ids = &context.editor.config().statusline.center;
element_ids
.iter()
.map(|element_id| get_render_function(*element_id))
.for_each(|render| render(context, write_center));
// Width of the empty space between the left and center area and between the center and right area.
let spacing = 1u16;
let edge_width = context.parts.left.width().max(context.parts.right.width()) as u16;
let center_max_width = viewport.width.saturating_sub(2 * edge_width + 2 * spacing);
let center_width = center_max_width.min(context.parts.center.width() as u16);
surface.set_spans(
viewport.x + viewport.width / 2 - center_width / 2,
viewport.y,
&context.parts.center,
center_width,
);
}
fn append(buffer: &mut Spans, text: String, base_style: &Style, style: Option<Style>) {
buffer.0.push(Span::styled(
text,
style.map_or(*base_style, |s| (*base_style).patch(s)),
));
}
fn get_render_function<F>(element_id: StatusLineElementID) -> impl Fn(&mut RenderContext, F)
where
F: Fn(&mut RenderContext, String, Option<Style>) + Copy,
{
match element_id {
helix_view::editor::StatusLineElement::Mode => render_mode,
helix_view::editor::StatusLineElement::Spinner => render_lsp_spinner,
helix_view::editor::StatusLineElement::FileName => render_file_name,
helix_view::editor::StatusLineElement::FileEncoding => render_file_encoding,
helix_view::editor::StatusLineElement::FileLineEnding => render_file_line_ending,
helix_view::editor::StatusLineElement::FileType => render_file_type,
helix_view::editor::StatusLineElement::Diagnostics => render_diagnostics,
helix_view::editor::StatusLineElement::Selections => render_selections,
helix_view::editor::StatusLineElement::Position => render_position,
}
}
fn render_mode<F>(context: &mut RenderContext, write: F)
where
F: Fn(&mut RenderContext, String, Option<Style>) + Copy,
{
let visible = context.focused;
write(
context,
format!(
" {} ",
if visible {
match context.doc.mode() {
Mode::Insert => "INS",
Mode::Select => "SEL",
Mode::Normal => "NOR",
}
} else {
// If not focused, explicitly leave an empty space instead of returning None.
" "
}
),
if visible && context.editor.config().color_modes {
match context.doc.mode() {
Mode::Insert => Some(context.editor.theme.get("ui.statusline.insert")),
Mode::Select => Some(context.editor.theme.get("ui.statusline.select")),
Mode::Normal => Some(context.editor.theme.get("ui.statusline.normal")),
}
} else {
None
},
);
}
fn render_lsp_spinner<F>(context: &mut RenderContext, write: F)
where
F: Fn(&mut RenderContext, String, Option<Style>) + Copy,
{
write(
context,
context
.doc
.language_server()
.and_then(|srv| {
context
.spinners
.get(srv.id())
.and_then(|spinner| spinner.frame())
})
// Even if there's no spinner; reserve its space to avoid elements frequently shifting.
.unwrap_or(" ")
.to_string(),
None,
);
}
fn render_diagnostics<F>(context: &mut RenderContext, write: F)
where
F: Fn(&mut RenderContext, String, Option<Style>) + Copy,
{
let (warnings, errors) = context
.doc
.diagnostics()
.iter()
.fold((0, 0), |mut counts, diag| {
use helix_core::diagnostic::Severity;
match diag.severity {
Some(Severity::Warning) => counts.0 += 1,
Some(Severity::Error) | None => counts.1 += 1,
_ => {}
}
counts
});
if warnings > 0 {
write(
context,
"●".to_string(),
Some(context.editor.theme.get("warning")),
);
write(context, format!(" {} ", warnings), None);
}
if errors > 0 {
write(
context,
"●".to_string(),
Some(context.editor.theme.get("error")),
);
write(context, format!(" {} ", errors), None);
}
}
fn render_selections<F>(context: &mut RenderContext, write: F)
where
F: Fn(&mut RenderContext, String, Option<Style>) + Copy,
{
let count = context.doc.selection(context.view.id).len();
write(
context,
format!(" {} sel{} ", count, if count == 1 { "" } else { "s" }),
None,
);
}
fn render_position<F>(context: &mut RenderContext, write: F)
where
F: Fn(&mut RenderContext, String, Option<Style>) + Copy,
{
let position = coords_at_pos(
context.doc.text().slice(..),
context
.doc
.selection(context.view.id)
.primary()
.cursor(context.doc.text().slice(..)),
);
write(
context,
format!(" {}:{} ", position.row + 1, position.col + 1),
None,
);
}
fn render_file_encoding<F>(context: &mut RenderContext, write: F)
where
F: Fn(&mut RenderContext, String, Option<Style>) + Copy,
{
let enc = context.doc.encoding();
if enc != encoding::UTF_8 {
write(context, format!(" {} ", enc.name()), None);
}
}
fn render_file_line_ending<F>(context: &mut RenderContext, write: F)
where
F: Fn(&mut RenderContext, String, Option<Style>) + Copy,
{
use helix_core::LineEnding::*;
let line_ending = match context.doc.line_ending {
Crlf => "CRLF",
LF => "LF",
#[cfg(feature = "unicode-lines")]
VT => "VT", // U+000B -- VerticalTab
#[cfg(feature = "unicode-lines")]
FF => "FF", // U+000C -- FormFeed
#[cfg(feature = "unicode-lines")]
CR => "CR", // U+000D -- CarriageReturn
#[cfg(feature = "unicode-lines")]
Nel => "NEL", // U+0085 -- NextLine
#[cfg(feature = "unicode-lines")]
LS => "LS", // U+2028 -- Line Separator
#[cfg(feature = "unicode-lines")]
PS => "PS", // U+2029 -- ParagraphSeparator
};
write(context, format!(" {} ", line_ending), None);
}
fn render_file_type<F>(context: &mut RenderContext, write: F)
where
F: Fn(&mut RenderContext, String, Option<Style>) + Copy,
{
let file_type = context.doc.language_id().unwrap_or("text");
write(context, format!(" {} ", file_type), None);
}
fn render_file_name<F>(context: &mut RenderContext, write: F)
where
F: Fn(&mut RenderContext, String, Option<Style>) + Copy,
{
let title = {
let rel_path = context.doc.relative_path();
let path = rel_path
.as_ref()
.map(|p| p.to_string_lossy())
.unwrap_or_else(|| SCRATCH_BUFFER_NAME.into());
format!(
" {}{} ",
path,
if context.doc.is_modified() { "[+]" } else { "" }
)
};
write(context, title, None);
}

@ -27,7 +27,7 @@ use crate::{DocumentId, Editor, ViewId};
/// 8kB of buffer space for encoding and decoding `Rope`s.
const BUF_SIZE: usize = 8192;
const DEFAULT_INDENT: IndentStyle = IndentStyle::Spaces(4);
const DEFAULT_INDENT: IndentStyle = IndentStyle::Tabs;
pub const SCRATCH_BUFFER_NAME: &str = "[scratch]";

@ -210,6 +210,8 @@ pub struct Config {
/// Whether to display infoboxes. Defaults to true.
pub auto_info: bool,
pub file_picker: FilePickerConfig,
/// Configuration of the statusline elements
pub statusline: StatusLineConfig,
/// Shape for cursor in each mode
pub cursor_shape: CursorShapeConfig,
/// Set to `true` to override automatic detection of terminal truecolor support in the event of a false negative. Defaults to `false`.
@ -230,10 +232,25 @@ pub struct Config {
pub explorer: ExplorerConfig,
}
#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(default, rename_all = "kebab-case", deny_unknown_fields)]
pub struct LspConfig {
/// Display LSP progress messages below statusline
pub display_messages: bool,
/// Enable automatic pop up of signature help (parameter hints)
pub auto_signature_help: bool,
/// Display docs under signature help popup
pub display_signature_help_docs: bool,
}
impl Default for LspConfig {
fn default() -> Self {
Self {
display_messages: false,
auto_signature_help: true,
display_signature_help_docs: true,
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
@ -245,6 +262,57 @@ pub struct SearchConfig {
pub wrap_around: bool,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case", default, deny_unknown_fields)]
pub struct StatusLineConfig {
pub left: Vec<StatusLineElement>,
pub center: Vec<StatusLineElement>,
pub right: Vec<StatusLineElement>,
}
impl Default for StatusLineConfig {
fn default() -> Self {
use StatusLineElement as E;
Self {
left: vec![E::Mode, E::Spinner, E::FileName],
center: vec![],
right: vec![E::Diagnostics, E::Selections, E::Position, E::FileEncoding],
}
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum StatusLineElement {
/// The editor mode (Normal, Insert, Visual/Selection)
Mode,
/// The LSP activity spinner
Spinner,
/// The file nane/path, including a dirty flag if it's unsaved
FileName,
/// The file encoding
FileEncoding,
/// The file line endings (CRLF or LF)
FileLineEnding,
/// The file type (language ID or "text")
FileType,
/// A summary of the number of errors and warnings
Diagnostics,
/// The number of selections (cursors)
Selections,
/// The cursor position
Position,
}
// Cursor shape is read and used on every rendered frame and so needs
// to be fast. Therefore we avoid a hashmap and use an enum indexed array.
#[derive(Debug, Clone, PartialEq)]
@ -329,6 +397,8 @@ pub enum GutterType {
Diagnostics,
/// Show line numbers
LineNumbers,
/// Show one blank space
Padding,
}
impl std::str::FromStr for GutterType {
@ -465,7 +535,11 @@ impl Default for Config {
},
line_number: LineNumber::Absolute,
cursorline: false,
gutters: vec![GutterType::Diagnostics, GutterType::LineNumbers],
gutters: vec![
GutterType::Diagnostics,
GutterType::LineNumbers,
GutterType::Padding,
],
middle_click_paste: true,
auto_pairs: AutoPairConfig::default(),
auto_completion: true,
@ -474,6 +548,7 @@ impl Default for Config {
completion_trigger_len: 2,
auto_info: true,
file_picker: FilePickerConfig::default(),
statusline: StatusLineConfig::default(),
cursor_shape: CursorShapeConfig::default(),
true_color: false,
search: SearchConfig::default(),
@ -865,7 +940,13 @@ impl Editor {
return;
}
Action::HorizontalSplit | Action::VerticalSplit => {
let view = View::new(id, self.config().gutters.clone());
// copy the current view, unless there is no view yet
let view = self
.tree
.try_get(self.tree.focus)
.filter(|v| id == v.doc) // Different Document
.cloned()
.unwrap_or_else(|| View::new(id, self.config().gutters.clone()));
let view_id = self.tree.split(
view,
match action {

@ -102,6 +102,17 @@ pub fn line_numbers<'doc>(
})
}
pub fn padding<'doc>(
_editor: &'doc Editor,
_doc: &'doc Document,
_view: &View,
_theme: &Theme,
_is_focused: bool,
_width: usize,
) -> GutterFn<'doc> {
Box::new(|_line: usize, _selected: bool, _out: &mut String| None)
}
#[inline(always)]
const fn abs_diff(a: usize, b: usize) -> usize {
if a > b {

@ -271,12 +271,16 @@ impl Tree {
}
pub fn get(&self, index: ViewId) -> &View {
self.try_get(index).unwrap()
}
pub fn try_get(&self, index: ViewId) -> Option<&View> {
match &self.nodes[index] {
Node {
content: Content::View(view),
..
} => view,
_ => unreachable!(),
} => Some(view),
_ => None,
}
}

@ -58,8 +58,13 @@ impl JumpList {
pub fn remove(&mut self, doc_id: &DocumentId) {
self.jumps.retain(|(other_id, _)| other_id != doc_id);
}
pub fn get(&self) -> &[Jump] {
&self.jumps
}
}
#[derive(Clone)]
pub struct View {
pub id: ViewId,
pub offset: Position,
@ -75,7 +80,11 @@ pub struct View {
pub last_modified_docs: [Option<DocumentId>; 2],
/// used to store previous selections of tree-sitter objects
pub object_selections: Vec<Selection>,
pub gutters: Vec<(Gutter, usize)>,
/// Gutter (constructor) and width of gutter, used to calculate
/// `gutter_offset`
gutters: Vec<(Gutter, usize)>,
/// cached total width of gutter
gutter_offset: u16,
}
impl fmt::Debug for View {
@ -91,12 +100,23 @@ impl fmt::Debug for View {
impl View {
pub fn new(doc: DocumentId, gutter_types: Vec<crate::editor::GutterType>) -> Self {
let mut gutters: Vec<(Gutter, usize)> = vec![];
let mut gutter_offset = 0;
use crate::editor::GutterType;
for gutter_type in &gutter_types {
match gutter_type {
GutterType::Diagnostics => gutters.push((gutter::diagnostics_or_breakpoints, 1)),
GutterType::LineNumbers => gutters.push((gutter::line_numbers, 5)),
}
let width = match gutter_type {
GutterType::Diagnostics => 1,
GutterType::LineNumbers => 5,
GutterType::Padding => 1,
};
gutter_offset += width;
gutters.push((
match gutter_type {
GutterType::Diagnostics => gutter::diagnostics_or_breakpoints,
GutterType::LineNumbers => gutter::line_numbers,
GutterType::Padding => gutter::padding,
},
width as usize,
));
}
Self {
id: ViewId::default(),
@ -108,6 +128,7 @@ impl View {
last_modified_docs: [None, None],
object_selections: Vec::new(),
gutters,
gutter_offset,
}
}
@ -119,14 +140,12 @@ impl View {
}
pub fn inner_area(&self) -> Rect {
// TODO: cache this
let offset = self
.gutters
.iter()
.map(|(_, width)| *width as u16)
.sum::<u16>()
+ 1; // +1 for some space between gutters and line
self.area.clip_left(offset).clip_bottom(1) // -1 for statusline
// TODO add abilty to not use cached offset for runtime configurable gutter
self.area.clip_left(self.gutter_offset).clip_bottom(1) // -1 for statusline
}
pub fn gutters(&self) -> &[(Gutter, usize)] {
&self.gutters
}
//
@ -327,7 +346,11 @@ mod tests {
fn test_text_pos_at_screen_coords() {
let mut view = View::new(
DocumentId::default(),
vec![GutterType::Diagnostics, GutterType::LineNumbers],
vec![
GutterType::Diagnostics,
GutterType::LineNumbers,
GutterType::Padding,
],
);
view.area = Rect::new(40, 40, 40, 40);
let rope = Rope::from_str("abc\n\tdef");
@ -374,7 +397,10 @@ mod tests {
#[test]
fn test_text_pos_at_screen_coords_without_line_numbers_gutter() {
let mut view = View::new(DocumentId::default(), vec![GutterType::Diagnostics]);
let mut view = View::new(
DocumentId::default(),
vec![GutterType::Diagnostics, GutterType::Padding],
);
view.area = Rect::new(40, 40, 40, 40);
let rope = Rope::from_str("abc\n\tdef");
let text = rope.slice(..);
@ -400,7 +426,11 @@ mod tests {
fn test_text_pos_at_screen_coords_cjk() {
let mut view = View::new(
DocumentId::default(),
vec![GutterType::Diagnostics, GutterType::LineNumbers],
vec![
GutterType::Diagnostics,
GutterType::LineNumbers,
GutterType::Padding,
],
);
view.area = Rect::new(40, 40, 40, 40);
let rope = Rope::from_str("Hi! こんにちは皆さん");
@ -440,7 +470,11 @@ mod tests {
fn test_text_pos_at_screen_coords_graphemes() {
let mut view = View::new(
DocumentId::default(),
vec![GutterType::Diagnostics, GutterType::LineNumbers],
vec![
GutterType::Diagnostics,
GutterType::LineNumbers,
GutterType::Padding,
],
);
view.area = Rect::new(40, 40, 40, 40);
let rope = Rope::from_str("Hèl̀l̀ò world!");

@ -381,6 +381,19 @@ indent = { tab-width = 2, unit = " " }
name = "css"
source = { git = "https://github.com/tree-sitter/tree-sitter-css", rev = "94e10230939e702b4fa3fa2cb5c3bc7173b95d07" }
[[language]]
name = "scss"
scope = "source.scss"
injection-regex = "scss"
file-types = ["scss"]
roots = []
language-server = { command = "vscode-css-language-server", args = ["--stdio"] }
indent = { tab-width = 2, unit = " " }
[[grammar]]
name = "scss"
source = { git = "https://github.com/serenadeai/tree-sitter-scss", rev = "c478c6868648eff49eb04a4df90d703dc45b312a" }
[[language]]
name = "html"
scope = "text.html.basic"
@ -1127,6 +1140,7 @@ file-types = ["gleam"]
roots = ["gleam.toml"]
comment-token = "//"
indent = { tab-width = 2, unit = " " }
language-server = { command = "gleam", args = ["lsp"] }
[[grammar]]
name = "gleam"
@ -1402,7 +1416,7 @@ source = { git = "https://github.com/vlang/vls", subpath = "tree_sitter_v", rev
[[language]]
name = "verilog"
scope = "source.verilog"
file-types = ["v", "sv", "svh"]
file-types = ["v", "vh", "sv", "svh"]
roots = []
comment-token = "//"
language-server = { command = "svlangserver", args = [] }

@ -1,6 +1,7 @@
; Scopes
(block) @local.scope
(closure_expression) @local.scope
; Definitions
@ -10,7 +11,7 @@
(let_declaration
pattern: (identifier) @local.definition)
(closure_parameters (identifier)) @local.definition
(closure_parameters (identifier) @local.definition)
; References
(identifier) @local.reference

@ -0,0 +1,86 @@
(comment) @comment
"~" @operator
">" @operator
"+" @operator
"-" @operator
"*" @operator
"/" @operator
"=" @operator
"^=" @operator
"|=" @operator
"~=" @operator
"$=" @operator
"*=" @operator
"in" @operator
"and" @operator
"or" @operator
"not" @operator
"only" @operator
"@apply" @constant.builtin
"@at-root" @constant.builtin
"@charset" @constant.builtin
"@debug" @constant.builtin
"@each" @keyword.control.repeat
"@else" @keyword.control.conditional
"@error" @constant.builtin
"@extend" @constant.builtin
"@for" @keyword.control.repeat
"@forward" @keyword.control.import
"@function" @function.method
"@if" @keyword.control.conditional
"@import" @keyword.control.import
"@include" @keyword.control.import
"@keyframes" @constant.builtin
"@media" @constant.builtin
"@mixin" @constant.builtin
"@namespace" @namespace
"@return" @keyword.control.return
"@supports" @constant.builtin
"@use" @keyword.control.import
"@warn" @constant.builtin
"@while" @keyword.control.repeat
((property_name) @variable
(match? @variable "^--"))
((plain_value) @variable
(match? @variable "^--"))
(tag_name) @tag
(universal_selector) @tag
(attribute_selector (plain_value) @string)
(nesting_selector) @variable.other.member
(pseudo_element_selector) @attribute
(pseudo_class_selector) @attribute
(identifier) @variable
(class_name) @variable
(id_name) @variable
(namespace_name) @variable
(property_name) @variable.other.member
(feature_name) @variable
(variable) @variable
(variable_name) @variable.other.member
(variable_value) @variable.other.member
(argument_name) @variable.parameter
(selectors) @variable.other.member
(attribute_name) @attribute
(function_name) @function
(to) @keyword
(from) @keyword
(important) @keyword
(string_value) @string
(color_value) @string.special
(integer_value) @constant.numeric.integer
(float_value) @constant.numeric.float
(unit) @type
"#" @punctuation.delimiter
"," @punctuation.delimiter

@ -0,0 +1,2 @@
((comment) @injection.content
(#set! injection.language "comment"))

@ -5,7 +5,7 @@
(#eq? @function "#match?"))
; highlight inheritance comments
((query . (comment) @keyword.directive)
(((comment) @keyword.directive)
(#match? @keyword.directive "^; +inherits *:"))
[

@ -35,7 +35,7 @@ label = "peach"
"diff.plus" = "#35bf86"
"diff.minus" = "#f22c86"
"diff.delta" = "#6f44f0"
"diff.delta" = "#6F44F0"
"ui.background" = { bg = "black_2" }
"ui.linenr" = { fg = "gray_0" }
@ -62,10 +62,10 @@ label = "peach"
diagnostic = { modifiers = ["underlined"] }
warning = "lightning"
error = "apricot"
info = "delta"
hint = "silver"
warning = "peach"
error = "maroon"
info = "blue"
hint = "lavender"
[palette]
flamingo = "#F2CDCD"

@ -22,7 +22,7 @@
"ui.background" = { fg = "foreground", bg = "background" }
"ui.cursor" = { fg = "background", bg = "orange", modifiers = ["dim"] }
"ui.cursor.match" = { fg = "green", modifiers = ["underlined"] }
"ui.cursor.primary" = { fg = "background", bg = "cyan", modifier = ["dim"] }
"ui.cursor.primary" = { fg = "background", bg = "cyan", modifiers = ["dim"] }
"ui.cursorline.primary" = { bg = "background_dark" }
"ui.help" = { fg = "foreground", bg = "background_dark" }
"ui.linenr" = { fg = "comment" }

@ -23,7 +23,7 @@
"ui.background" = { fg = "foreground", bg = "background" }
"ui.cursor" = { fg = "background", bg = "orange", modifiers = ["dim"] }
"ui.cursor.match" = { fg = "green", modifiers = ["underlined"] }
"ui.cursor.primary" = { fg = "background", bg = "cyan", modifier = ["dim"] }
"ui.cursor.primary" = { fg = "background", bg = "cyan", modifiers = ["dim"] }
"ui.help" = { fg = "foreground", bg = "background_dark" }
"ui.linenr" = { fg = "comment" }
"ui.linenr.selected" = { fg = "foreground" }

@ -6,7 +6,7 @@
## GENERAL ==============================
"warning" = { fg ="yellow", modifiers = ["bold"] } # Editor warnings.
"error" = { fg = "red", modifiers = ["bold"] } # Editor errors, like mis-typing a command.
"error" = { bg = "mid-green", fg = "red", modifiers = ["bold"] } # Editor errors, like mis-typing a command.
"info" = { fg = "mid-blue", bg = "mid-green" } # Code diagnostic info in gutter (LSP).
# ? Difference between info and hint ?
"hint" = { fg = "light-green", modifiers = ["bold"] } # Code diagnostics hint in gutter (LSP).
@ -30,8 +30,8 @@
'ui.cursor.select' = { fg = "light-blue" } # The cursor in select mode (v).
'ui.cursor.match' = { fg = "red", modifiers = ["bold", "reversed"] } # The matching parentheses of that under the cursor.
'ui.selection' = { bg = "mid-green" } # All currently selected text.
'ui.selection.primary' = { bg = "mid-green" } # The primary selection when there are multiple.
'ui.selection' = { bg = "autocomp-green" } # All currently selected text.
'ui.selection.primary' = { bg = "autocomp-green" } # The primary selection when there are multiple.
'ui.cursorline.primary' = { bg = 'mid-green' }
'ui.linenr' = { bg = "dark-green", fg = "gray" } # Line numbers.
@ -42,15 +42,19 @@
'ui.virtual.whitespace' = { fg = "gray"} # Whitespace markers in editing area.
'ui.virtual.indent-guide' = { fg = "gray" } # Indentation guides.
'ui.statusline' = { fg = "white", bg = "autocomp-green"} # Status line.
'ui.statusline' = { fg = "light-green", bg = "autocomp-green"} # Status line.
'ui.statusline.inactive' = { fg = "white", bg = "mid-green"} # Status line in unfocused windows.
"ui.statusline.normal" = { fg = "dark-green", bg = "mid-blue", modifiers = [ "bold" ] }
"ui.statusline.insert" = { fg = "dark-green", bg = "pink", modifiers = [ "bold" ] }
"ui.statusline.select" = { fg = "dark-green", bg = "yellow", modifiers = [ "bold" ] }
'ui.help' = { bg = "mid-green", fg = "white"} # `:command` descriptions above the command line.
'ui.highlight' = { bg = "mid-green"} # selected contents of symbol pickers (spc-s, spc-S) and current line in buffer picker (spc-b).
'ui.menu' = { fg = "gray", bg = "mid-green" } # Autocomplete menu.
'ui.menu.selected' = { fg = "white", bg = "autocomp-green" } # Selected autocomplete item.
'ui.menu' = { fg = "white", bg = "mid-green" } # Autocomplete menu.
'ui.menu.selected' = { fg = "light-green", bg = "autocomp-green" } # Selected autocomplete item.
'ui.popup' = { bg = "mid-green" } # Documentation popups (space-k).
# 'ui.ppopup.info' = { bg = "midgreen", fg = "gray", modifiers = ["bold"] } # Info popups box (space mode menu).
@ -124,7 +128,6 @@
'namespace' = { fg = "mid-blue" } # * Namespace keyword in java, C#, etc.
# Markup ==============================
# Colors for markup languages, like Markdown or XML.
@ -172,9 +175,9 @@
# Version control changes.
'diff.plus' = "light-green" # { } # Additions.
'diff.minus' = "yelllow" # { } # Deletions.
'diff.minus' = "yellow" # { } # Deletions.
'diff.delta' = "red" # { } # Modifications.
'diff.delta.moved' = { } # Renamed or moved files / changes.
# 'diff.delta.moved' = { } # Renamed or moved files / changes.
[palette] # Define your custom colors here.
@ -185,9 +188,9 @@ light-green = "#48e9a7" # strings
pink = "#df769b"
yellow = "#ffd800"
purple = "#6f60ea"
purple = "#918cff"
white = "#b1cace"
orange = "#e4b782"
orange = "#ffa864"
gray = "#5b858b" # mainly for comments/background text
red = "#e34e1b"

@ -65,6 +65,9 @@ diagnostic = { modifiers = ["underlined"] }
"ui.statusline" = { fg = "white", bg = "light-black" }
"ui.statusline.inactive" = { fg = "light-gray", bg = "light-black" }
"ui.statusline.normal" = { fg = "light-black", bg = "blue" }
"ui.statusline.insert" = { fg = "light-black", bg = "green" }
"ui.statusline.select" = { fg = "light-black", bg = "purple" }
"ui.text" = { fg = "white" }
"ui.text.focus" = { fg = "white", bg = "light-black", modifiers = ["bold"] }

@ -16,17 +16,19 @@
"function.macro" = { fg = "red" }
"keyword" = { fg = "purple" }
"keyword.function" = { fg = "purple" }
"keyword.control" = { fg = "purple" }
"keyword.control.import" = { fg = "purple" }
"keyword.directive" = { fg = "purple" }
"keyword.operator" = { fg = "purple" }
"keyword.storage.type" = { fg = "purple" }
"tag" = "cyan"
"label" = { fg = "cyan" }
"namespace" = { fg = "red" }
"operator" = { fg = "red" }
"property" = { fg = "red" }
"special" = { fg = "blue" }
"special" = { fg = "purple" }
"string" = { fg = "green" }
"module" = { fg = "cyan" }
@ -35,7 +37,7 @@
"punctuation" = { fg = "gray" }
"punctuation.delimiter" = { fg = "black" }
# "punctuation.bracket" = { fg="black" }
"punctuation.bracket" = { fg = "gray" }
"variable" = { fg = "black" }
"variable.builtin" = { fg = "light-blue" }
@ -43,12 +45,12 @@
"variable.other.member" = { fg = "red" }
"markup.heading" = { fg = "red" }
# "markup.raw" = { bg = "light-white" }
"markup.raw.inline" = { fg = "green", bg = "light-white" }
"markup.raw" = { fg = "gray" }
"markup.raw.inline" = { fg = "green", bg = "grey-200" }
"markup.bold" = { fg = "yellow", modifiers = ["bold"] }
"markup.italic" = { fg = "purple", modifiers = ["italic"] }
"markup.list" = { fg = "red" }
"markup.quote" = { fg = "yellow" }
"markup.list" = { fg = "light-blue" }
"markup.quote" = { fg = "gray" }
"markup.link.url" = { fg = "cyan", modifiers = ["underlined"] }
"markup.link.text" = { fg = "light-blue" }
"markup.heading.1" = { fg = "red", modifiers = ["bold"] }
@ -79,7 +81,7 @@
"ui.highlight" = { bg = "light-white" }
"ui.selection" = { modifiers = ["reversed"] }
"ui.selection" = { bg="light-white", modifiers = ["dim"] }
"ui.selection.primary" = { bg = "light-white" }
"ui.virtual" = { fg = "light-white" }
@ -92,6 +94,9 @@
"ui.statusline" = { fg = "black", bg = "light-white" }
"ui.statusline.inactive" = { fg = "gray", bg = "light-white" }
"ui.statusline.normal" = { fg = "light-white", bg = "light-blue" }
"ui.statusline.insert" = { fg = "light-white", bg = "green" }
"ui.statusline.select" = { fg = "light-white", bg = "purple" }
"ui.text" = { fg = "black" }
"ui.text.focus" = { fg = "red", bg = "light-white", modifiers = ["bold"] }

@ -56,8 +56,8 @@
"markup.heading.2" = { fg = "gold", modifiers = ["bold"] }
"markup.heading.3" = { fg = "rose", modifiers = ["bold"] }
"markup.heading.4" = { fg = "pine", modifiers = ["bold"] }
"markup.heading.5" = { fg = "form", modifiers = ["bold"] }
"markup.heading.6" = { fg = "fg", modifiers = ["bold"] }
"markup.heading.5" = { fg = "foam", modifiers = ["bold"] }
"markup.heading.6" = { fg = "iris", modifiers = ["bold"] }
"markup.list" = { fg = "love" }
"markup.bold" = { fg = "gold", modifiers = ["bold"] }
"markup.italic" = { fg = "iris", modifiers = ["italic"] }

@ -53,8 +53,8 @@
"markup.heading.2" = { fg = "gold", modifiers = ["bold"] }
"markup.heading.3" = { fg = "rose", modifiers = ["bold"] }
"markup.heading.4" = { fg = "pine", modifiers = ["bold"] }
"markup.heading.5" = { fg = "form", modifiers = ["bold"] }
"markup.heading.6" = { fg = "fg", modifiers = ["bold"] }
"markup.heading.5" = { fg = "foam", modifiers = ["bold"] }
"markup.heading.6" = { fg = "iris", modifiers = ["bold"] }
"markup.list" = { fg = "love" }
"markup.bold" = { fg = "gold", modifiers = ["bold"] }
"markup.italic" = { fg = "iris", modifiers = ["italic"] }

@ -35,7 +35,7 @@
"ui.background" = { bg = "bg0" }
"ui.cursor" = { fg = "bg0", bg = "fg" }
"ui.cursor.match" = { fg = "grey3", bg = "grey2" }
"ui.cursor.match" = { fg = "grey0", bg = "grey2" }
"ui.cursor.insert" = { fg = "bg0", bg = "bg_yellow" }
"ui.cursor.select" = { fg = "bg0", bg = "bg_yellow" }
"ui.linenr" = "yellow"
@ -56,7 +56,7 @@
"info" = "aqua"
"warning" = "yellow"
"error" = "nasty-red"
"diagnostic" = { fg = "dark-red", Modifiers = ["underlined"] }
"diagnostic" = { fg = "dark-red", modifiers = ["underlined"] }
"diff.plus" = { fg = "green" }
"diff.delta" = { fg = "orange" }
@ -69,7 +69,7 @@
"markup.link.url" = "cyan"
"markup.link.text" = "pink"
"markup.quote" = { fg = "yellow", modifiers = ["italic"] }
"markup.raw" = { fg = "foreground" }
"markup.raw" = { fg = "fg" }
"ui.explorer.file" = { fg = "fg" }
"ui.explorer.dir" = { fg = "blue" }
@ -104,3 +104,4 @@ purple = "#d0c4d4"
grey0 = "#aaaeb3"
grey1 = "#e1e1e3"
grey2 = "#646669"
pink = "#e06c75"

@ -56,7 +56,7 @@
"info" = "aqua"
"warning" = "yellow"
"error" = "nasty-red"
"diagnostic" = { fg = "dark-red", Modifiers = ["underlined"] }
"diagnostic" = { fg = "dark-red", modifiers = ["underlined"] }
"diff.plus" = { fg = "green" }
"diff.delta" = { fg = "orange" }
@ -69,7 +69,7 @@
"markup.link.url" = "cyan"
"markup.link.text" = "pink"
"markup.quote" = { fg = "yellow", modifiers = ["italic"] }
"markup.raw" = { fg = "foreground" }
"markup.raw" = { fg = "fg" }
"ui.explorer.file" = { fg = "fg" }
"ui.explorer.dir" = { fg = "blue" }
@ -96,7 +96,7 @@ red = "#621d28"
nasty-red = "#da3333"
dark-red = "#791717"
orange = "#57320f"
yellow = "#e2b714"
yellow = "#9a7d0e"
green = "#3f4b34"
aqua = "#455054"
blue = "#3f5673"
@ -104,3 +104,4 @@ purple = "#534059"
grey0 = "#aaaeb3"
grey1 = "#e1e1e3"
grey2 = "#646669"
pink = "#e06c75"

@ -24,7 +24,7 @@
"ui.background" = { fg = "foreground", bg = "background" }
"ui.cursor" = { fg = "background", bg = "blue", modifiers = ["dim"] }
"ui.cursor.match" = { fg = "green", modifiers = ["underlined"] }
"ui.cursor.primary" = { fg = "background", bg = "cyan", modifier = ["dim"] }
"ui.cursor.primary" = { fg = "background", bg = "cyan", modifiers = ["dim"] }
"ui.help" = { fg = "foreground", bg = "background_dark" }
"ui.linenr" = { fg = "comment" }
"ui.linenr.selected" = { fg = "foreground" }

Loading…
Cancel
Save