reddit / emacs

Project and EDE: A potential way to organize project types?

alecStewart1 · 2022-09-21 19:23
Hello, friends! I would like to have the ability to organize and create new project types in some way, like you can with Projectile , as shown in the [Projectile docs](https://docs.projectile.mx/projectile/projects.html#adding-custom-project-types) to create a new project type: (projectile-register-project-type 'npm '("package.json") :project-file "package.json" :compile "npm install" :test "npm test" :run "npm start" :test-suffix ".spec") Maybe the built-in project and EDE could potentially fill this role, but maybe not. I'm curious about what y'all think. Allow me to share what I found while looking around some of the Emacs source code. EDE, Emacs Development Environment, has an interesting interactive function `ede-new` which allows you to go through prompts to specify a new project. However if you do `M-x ede-new` you'll find that there's only 2 project types, Make and Automake. There is also the function `ede-eneable-generic-projects`, which looks like the following: ;;;###autoload (defun ede-enable-generic-projects () "Enable generic project loaders." (interactive) (ede-generic-new-autoloader "generic-makefile" "Generic Make" "Makefile" 'ede-generic-makefile-project) (ede-generic-new-autoloader "generic-scons" "Generic SCons" "SConstruct" 'ede-generic-scons-project) (ede-generic-new-autoloader "generic-cmake" "Generic CMake" "CMakeLists" 'ede-generic-cmake-project) ;; Super Generic found via revision control tags. (ede-generic-new-autoloader "generic-git" "Generic Git" ".git" 'ede-generic-vc-project) (ede-generic-new-autoloader "generic-bzr" "Generic Bazaar" ".bzr" 'ede-generic-vc-project) (ede-generic-new-autoloader "generic-hg" "Generic Mercurial" ".hg" 'ede-generic-vc-project) (ede-generic-new-autoloader "generic-svn" "Generic Subversions" ".svn" 'ede-generic-vc-project) (ede-generic-new-autoloader "generic-cvs" "Generic CVS" "CVS" 'ede-generic-vc-project) (ede-generic-new-autoloader "generic-mtn" "Generic Monotone" "_MTN" 'ede-generic-vc-project) ;; Take advantage of existing 'projectile' based projects. ;; @TODO - if projectile supports compile commands etc, can we ;; read that out? Howto if projectile is not part of core emacs. (ede-generic-new-autoloader "generic-projectile" "Generic .projectile" ".projectile" 'ede-generic-vc-project) ) These projects types (minues these autoloader ones), I assume, are defined with `ede-project`. It's docstring says: An EDE project controls a set of TARGETS, and can also contain multiple SUBPROJECTS. The project defines a set of features that need to be built from files, in addition as to controlling what to do with the file set, such as creating distributions, compilation, and web sites. Projects can also affect how EDE works, by changing what appears in the EDE menu, or how some keys are bound. But, as with the projects defined with `ede-generic-new-autoloader` they are `ede-generic-project`. If you look at ede-generic-vc-project: (defclass ede-generic-vc-project (ede-generic-project) () "Generic project found via Version Control files.") and then `ede-generic-project` itself: (defclass ede-generic-project (ede-project-with-config ede-project-with-config-build ede-project-with-config-program ede-project-with-config-c ede-project-with-config-java) ((config-class :initform 'ede-generic-config) (config-file-basename :initform "EDEConfig.el") (buildfile :initform "" :type string :allocation :class :documentation "The file name that identifies a project of this type. The class allocated value is replace by different sub classes.") ) "The baseclass for all generic EDE project types." :abstract t) So all of this to ask: what's the simplest way to define a new project type? I'll give a use case. Say I have a Common Lisp project. Your project could contain 1 or more ASDF files. If your project is `my-project` then the ASDF file will be `my-project.asd`, or whatever you choose to name the project. An easy example would be to look at [`qlot.asd`](https://github.com/fukamachi/qlot/blob/master/qlot.asd). You could also be a [roswell](https://github.com/roswell/roswell) user, in which case your project might have a `roswell` directory. Again, you can look at the [qlot](https://github.com/fukamachi/qlot/tree/master/roswell) project for reference. This all could be for nothing, as, at the very least, I could create functions for each mode to run shell commands (or things to send to the REPL in the case of Common Lisp) related to the project type. So say for an NPM project: (defun project:npm-run-dev () "This is a bit simple, but you get the idea." (interactive) (project-shell-command "npm run dev")) TL;DR - I'm curious as to how I could use Project and EDE to organize project types and create project specific functions, like with Projectile, or if this a bit more complicated and it'd be better to just create a set of functions for each project type that I work with. EDIT: As a note of interest and another reason I or someone else might be interested in doing this `project-root` is actually generic as well. ([For those not familiar with `defgeneric` in Common Lisp.](https://gigamonkeys.com/book/object-reorientation-generic-functions.html)) As an example of it's use, here's a bit of code from `ede.el`: (cl-defmethod project-root ((project ede-project)) (ede-project-root-directory project)) With `cl-defmethod`, `project-root` can now take an argument that is of the type (class) of `ede-project`, which is then used as an argument for `ede-project-root-directory`, which just returns the project root directory if it's a valid EDE project. Again, defining another use of `project-root` might not be necessary. One could likely just wrap it instead, like so: (defvar project:root-list '("Makefile" "GNUMakefile" "CMakeLists.txt" "pom.xml" "Cask" "Eldev" "Keg" "Eask" "package.json" "tsconfig.json" "jsconfig.json" ".volarrc" "volar.config.js" "manage.py" "requirements.txt" "setup.py" "tox.ini" "Pipfile" "poetry.lock" "pyproject.toml" "Gemfile" "info.rkt") "A list of named files that indicate the current directory is a project. Some people might need less or more file names.") (defun project:project-p (dir) "Check if the DIR if a project, containing a valid project file in ‘project:root-list’. Returns the directory containing the valid project file, or nil if there is no matching directory." (seq-some (lambda (f) (locate-dominating-file dir f)) project:root-list)) (defun project:project-root (&optional dir) "Return the current project root of DIR (defaults to ‘default-directory’). Returns nil if not in a project." (if (project:project-p dir) (project-root dir) (project-root (project-current)))) However this doesn't work with projects like the Common Lisp one I gave; where this indicating file that the directory is a project could be any named file with an `.asd` extension.
arthurno1 · 2022-09-21 21:45
I have a half done package that adresses similar issue, but I have not finished it yet and it is not on my public github yet. I have looked at Ede myself, but I don't really like the approach and complexity behind. Ede is meant to work as a code generator with already existing projects, which is great, but I am used to somewhat different workflow. If it helps, I am using myself org-capture as the gui, and bunch of callbacks to copy some template files and create directories. I don't have a good framework yet, it is just my personal hacks, so I don't want to publish it yet. It is little rematkaole that we have several project switching/finding/selecting libraries, but none to setup projects and automate boiletplate generation.
alecStewart1 · 2022-09-21 22:19
Yea EDE is something, having looked through it a few times now. But I guess, in fairness, it is trying to offer some IDE likeness out-of-the-box and I don't know how actively EDE/CEDET is developed.
eleven_cupfuls · 2022-09-21 22:38
This is quite interesting; I had not heard of EDE before and I will have to check it out. I will say though (without having looked at EDE) that project.el does already support extension to recognize different project types fairly easily. And it's also really straightforward because of the way it uses cl-lib to add methods to projects. The methods are freely customizable to whatever the project needs to do -- i.e., not just a string `compile-command` as with Projectile. For reference, here's [an example of some new project methods](https://gitlab.com/woolsweater/dotemacs.d/-/blob/9d94d811cc045005e005113fae8d460e83865836/modules/conjecture.el#L34-59). And here's three examples of new project types: [one that's denoted by an explicit marker file](https://gitlab.com/woolsweater/dotemacs.d/-/blob/9d94d811cc045005e005113fae8d460e83865836/modules/conjecture.el#L34-59), a [Swift Package type](https://gitlab.com/woolsweater/dotemacs.d/-/blob/9d94d811cc045005e005113fae8d460e83865836/modules/my-swift-mode.el#L190-264), and [Xcode-based projects](https://gitlab.com/woolsweater/dotemacs.d/-/blob/9d94d811cc045005e005113fae8d460e83865836/modules/convexed.el#L139-214).
alecStewart1 · 2022-09-22 18:29
Ah, interesting. That makes some sense. I will ask, where does a plist come from when using `project-root`? So in your `convexed` module: (cl-defmethod project-root ((project (head xcode))) "A project that is defined by an Xcode workspace document in its root directory. The workspace document may be either an .xcodeproj or an .xcworkspace." (let ((spec (cdr project))) (plist-get spec :root))) Where does a plist get introduced? In `project.el` there's this: (cl-defmethod project-root ((project (head vc))) (nth 2 project)) Where does, I'm getting also a plist, come in? Is it dependent on how you call `project-root`? I guess it's because it's not really explained how to "customize" this method in the project library. At the top of `project.el` in the decently long commentary comment section, it *sort of* does: How to create a new backend: - Consider whether you really should, or whether there are other ways to reach your goals. If the backend's performance is significantly lower than that of the built-in one, and it's first in the list, it will affect all commands that use it. Unless you are going to be using it only yourself or in special circumstances, you will probably want it to be fast, and it's unlikely to be a trivial endeavor. `project-files' is the method to optimize (the default implementation gets slower the more files the directory has, and the longer the list of ignores is). - Choose the format of the value that represents a project for your backend (we call it project instance). Don't use any of the formats from other backends. The format can be arbitrary, as long as the datatype is something `cl-defmethod' can dispatch on. The value should be stable (when compared with `equal') across invocations, meaning calls to that function from buffers belonging to the same project should return equal values. - Write a new function that will determine the current project based on the directory and add it to `project-find-functions' (which see) using `add-hook'. It is a good idea to depend on the directory only, and not on the current major mode, for example. Because the usual expectation is that all files in the directory belong to the same project (even if some/most of them are ignored). - Define new methods for some or all generic functions for this backend using `cl-defmethod'. A `project-root' method is mandatory, `project-files' is recommended, the rest are optional.
eleven_cupfuls · 2022-09-22 19:43
Ah, it doesn't have to be a plist: it's any list, as the doc says that "is something \`cl-defmethod' can dispatch on". So for the built-in "vc" project type it's just `'(vc . "path/to/project/root")`. The "recognizer" routine for a project type is what constructs the value representing the project. In the case of Xcode projects I wanted a little more info stored, so the recognizer builds a plist: https://gitlab.com/woolsweater/dotemacs.d/-/blob/2e1cad19e67d37dc630691189755df765a68a3d6/modules/convexed.el#L178-189 And then that value is what gets passed to the project methods. And they can extract from the plist as needed. It could be an alist or a struct or anything else, though.
alecStewart1 · 2022-09-23 21:06
Okay, I think I'm confusing myself because this feels so different from the regular Emacs customization style (or at least with what I've done). So `project-root` can be overload to return anything, but then the question would be is how that applies in the most simplest way of customization? I wouldn't say what you have is complex, but if someone were to ask "How do I customize project.el to recognize [X] kind of project" I don't know if I'd tell them they have to do extra work themselves that might even be them creating their own `defgeneric` to then overload. So, in my case, I have a Common Lisp project. The project name is `my-project` and the "dominating file" is `my-project.asd`, but it could technically be anything with the `.asd`extension. You could say the same for Nim projects if there's not a `nim.cfg` file: the Nim project will likely have a `.nimble` file. So basically we need to look for a file in a directory matching a wildcard and any particular file extension that corresponds to a give project type. I imagine that's what a lot of people want to start and that's what I want at the moment. EDIT: Oh, I was missing something. Having a `project-try-[some project type]` function and hooking it into `project-find-functions`. Now I think it's starting to make some sense.
eleven_cupfuls · 2022-09-24 17:01
> Oh, I was missing something. Having a project-try-[some project type] function and hooking it into project-find-functions That's the key, indeed! What I referred to as the "recognizer" function in my previous comment. I was forgetting the official term. That function returns _whatever you want_ as the project value, and then you can override the project methods (like `project-root`) as needed to match and act for that value. It is true that this is different than a lot of other Emacs customization, since it's using the EIEIO method system. (There's an Info manual for it, if you like: https://www.gnu.org/software/emacs/manual/html_mono/eieio.html#Writing-Methods) I think it's a really excellent fit for the kind of work that project.el needs to do (the flexibility is why I switched away from using Projectile).