Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ability to switch themes #267

Merged
merged 8 commits into from
Jun 19, 2021
Merged

Add ability to switch themes #267

merged 8 commits into from
Jun 19, 2021

Conversation

vv9k
Copy link
Contributor

@vv9k vv9k commented Jun 14, 2021

I know the scope of this PR might be a bit too wide so I'm very much open to agressively adjusting the goal. In the process of adding the ability to switch themes using a command I went into a pretty deep hole of static variables accessed from a few places where it's not as obvious how to pass the necessary values to the function. In the process I got rid of the static syntax::LOADER in helix-core and added the loader to main and most of the function signatures that used it. The highlight_config field of Document got removed as well, now calling highlight_config recomputes the configuration on each call and this is the part that I don't have figured out yet.

A global configuration file in ~/.config/helix/config.toml is added where users can specify the theme they want to use by default. Themes are now placed in ~/.config/helix/themes. During runtime one can set the theme with :theme <name> or list themes with :list-themes. For now the listing is pretty naive and just displays the names in status bar.

I'm looking for general comments on this approach, perhaps it's undesired to get rid of static variables I might be uninformed here.

@@ -144,34 +142,30 @@ fn read_query(language: &str, filename: &str) -> String {

impl LanguageConfiguration {
pub fn highlight_config(&self, scopes: &[String]) -> Option<Arc<HighlightConfiguration>> {
self.highlight_config
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the idea here was to only recalculate highlight_config if absolutely necessary (we only need to do this if the theme changed and we have different scopes now). HighlightConfiguration isn't cheap to construct and it also takes several fs reads so ideally we want to reuse them across files if they're the same language.

use helix_core::syntax;

// load $HOME/.config/helix/languages.toml, fallback to default config
let lang_config = std::fs::read(conf_dir.join("languages.toml"));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think all this was recently moved into Application::new, can we just do it there?

@@ -91,8 +92,39 @@ FLAGS:

setup_logging(logpath, args.verbosity).context("failed to initialize logging")?;

let themes = std::sync::Arc::new(Themes::load());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should really be delayed, we don't want to load all available themes into memory. list-themes should read the directory entries (I think we should move contrib/themes under runtime/themes), then when setting the theme we actually read the file.

let app_config = application::Configuration { theme: None };
let app_config = std::fs::read(conf_dir.join("config.toml"))
.map(|v| toml::from_slice(v.as_slice()))
.unwrap_or_else(|_| Ok(app_config.clone()))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't quite follow the double unwrap. I think the .map on the previous line should be a and_then

.unwrap_or_else(|_| Ok(app_config.clone()))
.unwrap_or_else(|_| app_config);

let theme = if let Some(theme) = &app_config.theme {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let theme = app_config.theme.as_ref().map(|theme| themes.get(theme)).unwrap_or_else(|_| themes.default())

use helix_view::editor::Action;
let mut compositor = Compositor::new()?;
let size = compositor.size();
let mut editor = Editor::new(size);
let theme = if let Some(theme) = &config.theme {
if let Some(theme) = themes.get(theme) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto here, also the to_owned branch is followed by a clone which is not ideal (it's a double clone)

option.detail.as_deref().unwrap_or_default(),
contents.clone()
),
cx.editor.config_loader.clone(),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah so this is why I switched to a global loader, it was annoying to pass it around everywhere

pub fn set_theme(&mut self, theme: Theme) {
let config_loader = self.config_loader.clone();
let (_, doc) = self.current();
doc.detect_language(Some(&theme), config_loader);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This really needs to be done in all documents, since the list of scopes might have changed. We also don't need to trash the whole syntax tree, simply update the highlight configuration on it I think.

@@ -161,7 +174,7 @@ impl Editor {
let id = if let Some(id) = id {
id
} else {
let mut doc = Document::load(path)?;
let mut doc = Document::load(path, Some(self.config_loader.clone()))?;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's no need to clone an arc around if you're just passing it as a reference (cloning then dropping results in extra operations to modify the refcount inside the Arc). &self.config_loader and then using &Loader on the type would work.

@@ -32,19 +36,41 @@ pub type LspCallback =
pub type LspCallbacks = FuturesUnordered<LspCallback>;
pub type LspCallbackWrapper = Box<dyn FnOnce(&mut Editor, &mut Compositor) + Send>;

#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Configuration {
pub theme: Option<String>,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is good and can also hold the keymap from #268 later, \cc @PabloMansanet

@vv9k vv9k force-pushed the config branch 2 times, most recently from d7aae56 to b3b6601 Compare June 15, 2021 07:41
@cessen
Copy link
Contributor

cessen commented Jun 15, 2021

Just want to comment that I'm definitely in favor of the general approach, from a user perspective. Haven't had a chance to look at the code itself yet.

@vv9k vv9k marked this pull request as draft June 16, 2021 06:35
@archseer
Copy link
Member

#268 landed so this can now share the config type :)

@vv9k vv9k changed the title Move configuration to main, add ability to switch themes Add ability to switch themes Jun 18, 2021
@vv9k
Copy link
Contributor Author

vv9k commented Jun 18, 2021

Not sure what to do about the LanguageConfiguration. Currently it is loaded once when asking for highlight_config and later it is impossible to configure HighlightConfiguration with scopes from a different theme. I was thinking of just cloning the HighlightConfiguration when already initialized and configuring it with different scopes but it doesn't implement Clone because Query from tree-sitter doesn't implement it.

impl LanguageConfiguration {
pub fn highlight_config(&self, scopes: &[String]) -> Option<Arc<HighlightConfiguration>> {
self.highlight_config
.get_or_init(|| {
let language = get_language_name(self.language_id).to_ascii_lowercase();
let highlights_query = read_query(&language, "highlights.scm");
// always highlight syntax errors
// highlights_query += "\n(ERROR) @error";
let injections_query = read_query(&language, "injections.scm");
let locals_query = "";
if highlights_query.is_empty() {
None
} else {
let language = get_language(self.language_id);
let mut config = HighlightConfiguration::new(
language,
&highlights_query,
&injections_query,
locals_query,
)
.unwrap(); // TODO: no unwrap
config.configure(scopes);
Some(Arc::new(config))
}
})
.clone()
}

@vv9k vv9k force-pushed the config branch 2 times, most recently from 7170494 to 136286a Compare June 19, 2021 06:53
@vv9k
Copy link
Contributor Author

vv9k commented Jun 19, 2021

Not sure what to do about the LanguageConfiguration...

I managed to come up with a solution that doesn't require mutating the HighlightConfiguration. Now all themes have all scopes defined on creation and the scopes that are missing from .toml file are replaced with the ones from the default theme.

Here's a demo of the commands:
themes

@vv9k vv9k marked this pull request as ready for review June 19, 2021 06:59
@archseer
Copy link
Member

I think we'll need to do the fix in a different way. We can't have a fixed set of scopes, because it's up to the theme to pick a range of scopes to support. For example, one can define a function.builtin in one theme to distinguish from function, but only use function in another theme. Tree sitter is smart and it will simply report function.builtin as function in those. If we always pass in a list of default scopes and fallback, then now function.builtin would be incorrectly highlighted.

@archseer
Copy link
Member

@archseer
Copy link
Member

Maybe we could use arc-swap here? Initialize a new highlight_config and swap it into the cell.

@archseer
Copy link
Member

If you replace the Arc contained inside syntax instead:

config: Arc<HighlightConfiguration>,
then we could potentially have a theme per document even.

@vv9k
Copy link
Contributor Author

vv9k commented Jun 19, 2021

But initializing highlight_config is exactly what takes up the longest, I've measured and even the file system reads are almost not noticable next to the operations that are then made on the .scm files when doing HighlightConfiguration::new. It takes about ~105ms in debug mode on my pc to initialize and the file reads happen in <1ms.

@archseer
Copy link
Member

Note: if you add a custom completer then we can also offer themes as completions on :theme

@vv9k
Copy link
Contributor Author

vv9k commented Jun 19, 2021

Note: if you add a custom completer then we can also offer themes as completions on :theme

I do plan to add a completer once we figure out a good way to have multiple highlight configs.

@archseer
Copy link
Member

But initializing highlight_config is exactly what takes up the longest, I've measured and even the file system reads are almost not noticable next to the operations that are then made on the .scm files when doing HighlightConfiguration::new. It takes about ~105ms in debug mode on my pc to initialize and the file reads happen in <1ms.

Yeah it's precisely because the highlight query needs to be reloaded and re-analyzed to support the new scopes. It's shared across all files of the same language though. Should be much faster in release mode.

@vv9k
Copy link
Contributor Author

vv9k commented Jun 19, 2021

Yeah it's precisely because the highlight query needs to be reloaded and re-analyzed to support the new scopes. It's shared across all files of the same language though. Should be much faster in release mode.

Doing just configure with new scopes on the HighlightConfiguration is actually quiet fast, it's the initial query construction that takes a lot of time.

One thing that I'm also wondering is if we were to have multiple HighlightConfigurations we'd have to reload the highlight files for each theme anyway as there is no Clone impl for it.

@archseer
Copy link
Member

Also, it's possible you're iterating over all languages over config.language? This will load up highlight configs for languages that aren't open in the editor. You probably need to use https://docs.rs/once_cell/1.8.0/once_cell/sync/struct.OnceCell.html#method.get to check if a cell is initialized first.

@archseer
Copy link
Member

Yeah all the files of the same type share a single highlight config inside an Arc, so iterating only over config.language, checking if cell has value via get().is_some(), then swapping the Arc contents via arc-swap should be enough to update all documents.

@vv9k
Copy link
Contributor Author

vv9k commented Jun 19, 2021

@pickfire
Copy link
Contributor

pickfire commented Jun 19, 2021

There is one thing I always want in switching theme, like previewing themes dynamically. I have the same issue in vim, neovim, kakoune, doom emacs. I need to manually type in the command to switch the theme one by one, I wish I can just tab and the whole thing is being previewed to that theme, of course we need some extra handler around the prompt if we want to do this. Usually these require addons and if I do it by hand it is going to be tedious. Only vscode get this right.

@vv9k vv9k force-pushed the config branch 2 times, most recently from 6685f4b to 16126e8 Compare June 19, 2021 09:45
@vv9k vv9k marked this pull request as draft June 19, 2021 09:46
@vv9k vv9k marked this pull request as ready for review June 19, 2021 11:02
Copy link
Member

@archseer archseer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verified on local. Thanks!

@archseer archseer merged commit 6825e19 into helix-editor:master Jun 19, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants