vim-composer icon indicating copy to clipboard operation
vim-composer copied to clipboard

Idea: find classes from composer class map

Open hiqsol opened this issue 6 years ago • 15 comments

This plugin could use class map built with composer dump-autoload -o to look for class name under cursor and use it for navigation and inserting of use statements. As far as I can see the plugin can't do it now.

Class map is written into file vendor/composer/autoload_static.php in $classMap property and holds list of all the classes found in the project like this:

public static $classMap = array (
    'Behat\\Gherkin\\Cache\\CacheInterface' => __DIR__ . '/..' . '/behat/gherkin/src/Behat/Gherkin/Cache/CacheInterface.php',
    'Behat\\Gherkin\\Cache\\FileCache' => __DIR__ . '/..' . '/behat/gherkin/src/Behat/Gherkin/Cache/FileCache.php',                                                                              
    ...

There could be classes that are available in several namespaces then plugin could show menu to select which class to proceed with.

Functionality like this is really missing now. There is no working solution for the moment. Ctags way is not good because tags file is huge (hundreds of megabytes) for big (not huge) projects. PHP language server is just not stable enough for the moment. So this could be a killing feature :)

hiqsol avatar Jun 27 '18 15:06 hiqsol

Thanks for the suggestion.

Note that navigation to class/interface/trait source files is already supported by <Plug>(composer-find) and insertion of use statements by <Plug>(composer-use). This is documented in :help composer-maps. The former uses Composer's autoloader to locate source files.

There could be classes that are available in several namespaces then plugin could show menu to select which class to proceed with.

Yes, this would be useful for various things. <Plug>(composer-use) currently can't guess the namespace you might want (it just uses the given namespace or namespace of the current file).

Whatever mechanism we use, I do not want to create or modify files in the project's vendor/ directory. I probably won't get to this until late next month, but meanwhile, a more specific proposal for how to accomplish this would be welcome.

noahfrederick avatar Jun 27 '18 16:06 noahfrederick

Yes, I've tried all features of the plugin!

I'm telling about finding (guessing) full class path by class name only with use of class map generated by composer dump-autoload -o. No modification of files in the project's vendor needed. You can read more about this composer feature here: https://getcomposer.org/doc/articles/autoloader-optimization.md

I can't make a whole PR now because I have no vim programming experience. But I want to make small PHP script to show the idea. The script will take class name and return suitable class path(s).

hiqsol avatar Jun 27 '18 18:06 hiqsol

So here is the script: https://gist.github.com/hiqsol/6c60b233816db0db8c324720e46f75dc

Run script php findClass.php ClassName

It just prints found class(es).

hiqsol avatar Jun 27 '18 19:06 hiqsol

No modification of files in the project's vendor needed.

You wrote:

Class map is written into file vendor/composer/autoload_static.php

This has the consequence of subsequent changes not being picked up by Composer until it's run again. All I'm saying is that the plug-in should not create/update this file automatically, and the user should not have to run composer dump-autoload -o either, in order to use this new feature. Hopefully there's a way to work around this, such as writing the file to a temporary location.

I can't make a whole PR now because I have no vim programming experience. But I want to make small PHP script to show the idea.

That's fine. The existing find feature already has a dependency on PHP, and PHP will be much faster than Vim Script anyway.

The script will take class name and return suitable class path(s).

👍

noahfrederick avatar Jun 27 '18 19:06 noahfrederick

You're right about not forcing user have optimized autoloader moreover it is not recommended in development (while highly recommended in production). I will study how to get class map outside of vendor.

hiqsol avatar Jun 27 '18 19:06 hiqsol

It seems there is no simple way to make composer dump-autoload to alternative directory. There is COMPOSER_VENDOR_DIR environment variable but it needs vendor dir content to be copied to alternative directory. It can be symlinked ... but I'm afraid it's dark and troublesome road.

Also I can try to make composer PR for option to only write autoloading files to alternative directory while reading from original but I'm afraid the PR will not be taken.

And there is non-composer solution with grep, here is example with ripgrep but any grep can be used:

rg --no-heading --type php '^namespace ' . vendor

It gives easy processable list of all class names with pathes. And t takes less then 1 second to scan big project with lots of dependencies totalling 9k+ classes.

hiqsol avatar Jun 27 '18 21:06 hiqsol

I've jury rigged almost working solution with ripgrep and fzf, and now I'm stuck and need help with vim.

Here is script that prints all classes in the project in format: fullclass filepath

#!/usr/bin/env php
<?php

$rg = popen("rg --no-heading --type php '^namespace .*;' . vendor", 'r');

while($str = fgets($rg)){
    if (preg_match('/^((.*)\.[a-z]+):namespace (.+);.*$/', $str, $ms)) {
        $path = $ms[1];
        $ns = $ms[3];
        $fs = explode('/', $ms[2]);
        $name = end($fs);
        $class = "$ns\\$name";
        printf("%s %s\n", $class, $path);
    }
}

pclose($rg);

I think it can be optimized but it is already very quick 0.142 seconds on a rather big project. So it's quick unough to be used without caching. I've compared classes list generated from composer optimized class map - they are essentially same, and it can be tuned.

And here is the simple completion script based on fzf#complete function:

function! s:add_namespace(lines)
    let s:full = split(join(a:lines), " ")[0]
    let s:name = split(s:full, "\\")[-1]

    call composer#namespace#use(1, s:full)

    return s:name
endfunction

inoremap <expr> <C-]> fzf#complete({
  \ 'source':   '/home/sol/bin/listGreppedClasses.php',
  \ 'reducer':  function('<sid>add_namespace'),
  \ 'down':     20})

It works the way I imagine ideal usage: I begin typing class name, then I press hotkey, then i find proper class with FZF menu then press enter and I have it - completed short class name and added use statement in the proper place.

The script finds class with FZF completion and adds use with your function composer#namespace#use. The use statement is added properly. But the completion puts short class name on the line before the use statement and not in the place the completion was performed. I don't know how to fix it. Maybe the cursor has to be returned back after calling composer#namespace#use. I just don't have enough vim knowledge.

hiqsol avatar Jun 30 '18 11:06 hiqsol

Very cool.

Maybe the cursor has to be returned back after calling composer#namespace#use.

Yes, this function (intentionally) moves the cursor to the new use statement. What happens if you add

execute "normal! \<C-o>"

before the return s:name line in your function?

noahfrederick avatar Jun 30 '18 13:06 noahfrederick

Now it puts short class name right under the use statement.

hiqsol avatar Jun 30 '18 13:06 hiqsol

I've tried getpos/setpos, and it gets even worse, it puts short class name in the middle of string two rows up from the initial position :)

hiqsol avatar Jun 30 '18 13:06 hiqsol

I failed to fix it with saving/restoring cursor position.

So I've rewritten composer#namespace#use with non changing cursor position let pos = search (..., 'n') and append(pos, line) and everything works fine. But not the uses sorting. It changes cursor position... While I would like to have it with sorting...

Will you take the PR? I wanted to add the listGreppedClasses.php script into the plugin and add something like <Plug>(composer-use-with-fzf-and-rg) with comments that it requires ripgrep and FZF.

But I think it would be better to fix it somehow with saving/restoring cursor position giving working sorting. Maybe you more ideas?

hiqsol avatar Jun 30 '18 15:06 hiqsol

I wanted to add the listGreppedClasses.php script into the plugin

Since this doesn't require PHP, I'd rather rewrite it in Vim Script. If you want to try yourself, look at :help systemlist(), :help map() to get started. Otherwise, I will rewrite it.

I would also like to fall back to grep in case rg isn't installed. Might as well also support ag. See :help executable() for how to check.

Feel free to open a pull request even if it isn't perfect.

I haven't had a chance to look into the cursor positioning issue yet. I'll see what I can do.

and add something like <Plug>(composer-use-with-fzf-and-rg) with comments that it requires ripgrep and FZF.

I think it would be cleaner and more flexible to instruct users to add their own FZF mapping, something along the lines of:

inoremap <expr> <C-]> fzf#complete({
  \ 'source':   composer#namespace#classes(),
  \ 'reducer':  function('composer#namespace#use'),
  \ 'down':     20})

That way we don't depend on fzf.vim at all, and users can configure it exactly the way they like.

noahfrederick avatar Jun 30 '18 16:06 noahfrederick

If you don't like PHP ;) I also tried classes preparation script in plain shell with sed:

#!/bin/sh

rg --no-heading --type php "^namespace .*;" . vendor | sed 's/^\(.*\/\(.*\)\.php\):namespace \(.*\);.*$/\3\\\2     \1/i'

But strangely it is slightly slower then PHP.

Agree about FZF mapping.

About PR: are you ok to change composer#namespace#use function to append use statement without moving cursor? Or another function like composer#namespace#append_use should be added? Also what about sorting? Is it possible to redo composer#namespace#sort_uses to don't move cursor?

hiqsol avatar Jun 30 '18 16:06 hiqsol

I just found this issue.

I experimented with this a while ago: https://github.com/noahfrederick/vim-composer/compare/master...adriaanzon:use-via-composer-classmap. It uses PHP to filter through the values of (require('vendor/autoload.php'))->getClassMap().

adriaanzon avatar Dec 10 '18 20:12 adriaanzon

@adriaanzon Hey, that's pretty clean. I'd still prefer to invert the relationship so that we have a generic composer#namespace#classes() (or something like that) that can be reused elsewhere, and to avoid a dependency on FZF. It should be painless to create a mapping like the one outlined here. I'd accept an implementation that addresses those requirements.

noahfrederick avatar Dec 11 '18 19:12 noahfrederick