dvcrn / proton

space-atom. spacemacs and sublimious style editing in atom
GNU General Public License v3.0
565 stars 55 forks source link

Add linters layer. #50

Closed geksilla closed 8 years ago

geksilla commented 8 years ago

Propose to add linters support within :tools/linters or :tools/error-check.

dvcrn commented 8 years ago

I think linter could / should go into core

sglyon commented 8 years ago

Agreed.

I have the start of rust and go layers and they would both rely on linter. It seems odd to include the linter package in those layers as well as its own dedicated layer.

dvcrn commented 8 years ago

On second thought, maybe including linting should be part of each lang layer. Core could implement a linter framework (if one exists) and the layers just plug into that. I think spacemacs is doing that with flycheck

sglyon commented 8 years ago

I'm pretty sure the linter package is the framework and then linters for specific languages hook into it. See the readme: https://atom.io/packages/linter

dvcrn commented 8 years ago

If that is the case then we should definitely put that into core and have linter support be part of the layers. I think that makes the most sense

sglyon commented 8 years ago

Great, it should require zero config, just a matter of adding :linter to the list of core packages and then maybe adding some toggles for turning it on and off

geksilla commented 8 years ago

Agree, linter package is required for atom. I notice about separate layer because we can setup separate Readme, configurations and other stuff. For each :lang layer we can put required linter package and related configuration, and enable linter if user selected this layer in .proton.

For example https://atom.io/packages/linter-clojure can be included in packages for :lang/clojure with appropriate configuration.

In spacemacs there is separate layer called syntax-checking which based on flycheck. For each lang layer they just adds hook to flycheck-mode with appropriate language using function layer-name/post-init-flycheck.

syl20bnr commented 8 years ago

Here how it works in Spacemacs (I'm interested in knowing the differences with the Atom ecosystem) :

The layer syntax-checking is the owner of the package flycheck which is a framework which has support for a bunch of languages and allows to add new one easily. The syntax-checking layer "declares" its ownership over flycheck by defining the init-flycheck function.

A language layer then declares a function post-init-flycheck which is evaluated only and only if an init-flycheck is defined i.e. The syntax-checking layer is enabled i.e. the flycheck package has a owner.

post-init functions are evaluated after the corresponding init function (there is also support for pre-init functions).

In these post-init-flycheck functions each language enable the linters by adding a hook to flycheck-mode-hook.

For completeness, the ownership of flycheck can be stolen by a layer redefining the init-flycheck function.

There are also hooks available for use-package which are advanced topics, feel free to ask by email or on gitter if you want explanations.

So linters can be toggled on and off globally by just adding the syntax-checking layer or not.

dvcrn commented 8 years ago

Oh hi @syl20bnr! Great to see you here! :smile: Thanks for the insights.

The way I understand it is that Linter (which is 'our' flycheck) does the same thing: Just providing a framework for sub-linters to hook in. It seems like Linter is collecting all installed Linters anyway on start so we wouldn't need a post-init hook. Atom is also lazy loaded so until we open a clojure file (in the case of the clojure layer), it wouldn't activate anything.

I guess a linter layer by itself that makes sure that Linter itself is installed and provides toggles for turning it off makes sense then. The question is how we should proceed with separate linters. The linter-clojure for example has a hard dependency on Linter and will force install it if it is not present yet. We don't want that because it would mess with our layer approach and stateful config. If we can somehow to turn this behaviour off, adding it to the clojure layer seems like the best approach. (Maybe we need to think about forking it and patching that behaviour out?)

Is post-init-{{something}} that we want / need as well? It would give layers more control but I think we don't have a usecase for that yet.

syl20bnr commented 8 years ago

The linter-clojure for example has a hard dependency on Linter and will force install it if it is not present yet. We don't want that because it would mess with our layer approach and stateful config.

In Spacemacs those linter addons are guarded with a (when (configuration-layer/package-usedp 'flycheck) ...). Since the flycheck package is treated as used if and only if there is an init-flycheck function defined, using or not using the layer that defines (owns) this init-flycheck acts as a toggle for all the addons.

Is post-init-{{something}} that we want / need as well? It would give layers more control but I think we don't have a usecase for that yet.

You'll surely need it at one point, pre-init is rarely used though but it does not hurt to have them when there is support for post-init.

One important detail: spacemacs only installs packages with a owner which means it installs only packages with a defined init-xxx function. So when the layer linter is not used then there is no init-linter function, then the addons init-xxx functions are not defined (since linter is not considered to be used, see guard above) so they are not considered to be used, in the end nothing is installed.

dvcrn commented 8 years ago

I thought a little bit more about this. So the current problem is:

  1. proton initialises all layers
  2. once all packages are collected, proton proceeds with installing
  3. when linter-clojure (or friends) are installed, they force install linter
  4. on next start, if :tools/linter is not enabled, it will remove linter and install it again

Here are some ideas:

1. We might really need a post-init-{{layer}} or something like that. Once the linter layer is enabled, all sub-linters are getting installed and activated. This completely lazy loads the linter package to when the linter layer is enabled. This is probably the cleanest approach but requires the most work. The problem here is though that clojurescript is compiled so we might need some sort of multimethod for each and every layer. This would increase boilerplate code by a good amount. (Correct me please if this is wrong). Maybe we can use @geksilla modes somehow for this?

2. We keep all linter packages disabled and only activate them on some sort of post-init. This solution is a bit easier since we don't need to change the entire lifecycle. Sub-layers just need to activate/deactivate the package accordingly. Though we need to make somehow sure that the package stays disabled even directly after installation. I think adding a package to disabledPackages and installing it afterwards prevents the package from being activated.

3. Even simpler (though hacky) is to let the tools/linter layer broadcast a config key. Something like linter.enabled. Proton collects all configurations first before proceeding with initialisation. The layers could check if linter.enabled exists. if it does, it means that the tools/linter layer is enabled and we just require the language linter packages. If it doesn't, then we can ignore the linter packages. This might be the easiest and fastest approach since all tools we need are already implemented. A variation of this would be to just check if linter is inside the enabled layers and do something based on that. We could pass this vector to the init-layer! function.

Any other thoughts?

geksilla commented 8 years ago

I like the first idea. For now not sure how we can use mode. I was experimenting with multimethods and post-init hook. @dvcrn you can check this one experimental branch. I've found that we can avoid boilerplate code using get-method and check if we have defmethod for associated dispatch value. I wrote this filter function and used it to execute methods for post-init hooks defined inside the layer. Also added package-deps multimethod which can help to manage additional dependencies like linter -> linter-clojure, e.g. we can disable/enable or install/remove linter-clojure package on linter event.

For testing purpose i've added linter package to core layer. For example usage check clojure layer. Thanks.

dvcrn commented 8 years ago

Wow I didn't know you were already this far. I will cherry-pick some of the changes into the branch I'm currently working on and see if I can get this merged today :+1:

dvcrn commented 8 years ago

Just to repeat what I already wrote in https://github.com/dvcrn/proton/pull/56 (so we have it in 1 place):

The problem with this approach is that clojure will just assign the multimethod to the latest defined one so we can't go with that.

I introduced a new function called register-{{layer/package}}-dependencies:

(register-layer-dependencies :tools/linter 
    [:linter-pep8])

It's a little bit more of state management but it basically collects all layers and builds a small dependency map. On package retrieval, it checks which layers/packages are enabled and fetches all sub-dependencies as well.

syl20bnr commented 8 years ago

Not sure it will help but here is in more details how Spacemacs loads:

1) Collect data

2) Install missing packages

3) Configure packages

4) Uninstall not used packages

Important remarks: