Movatterモバイル変換


[0]ホーム

URL:


Skip to content
DEV Community
Log in Create account

DEV Community

Mohan Raj
Mohan Raj

Posted on

Create a Neovim plugin for a CLI tool

Introduction

Ever Wondered How IDE Plugins Work? 🤔 Let's Build One for Neovim! Ever used a cool plugin in your IDE and thought, "How did they DO that?" Or maybe you've admired how CLI tools seamlessly integrate into your workflow.

Let's build one for PHPStan.

What is PHStan?

PHPStan is an open source CLI tools that helps to find bugs in PHP projects without actually running it or writing test cases.

To learn more,
https://phpstan.org
https://github.com/phpstan/phpstan

Let's see how it works

It is all started from Composer. Lets install phpstan package using composer.

composer require--dev phpstan/phpstan
Enter fullscreen modeExit fullscreen mode

To run the phpstan to figure out the potential bugs in our project.

./vendor/bin/phpstan analyse app
Enter fullscreen modeExit fullscreen mode

While adding the phpstan composer package, phpstan binary file will be created in the./vendor/bin folder. The second argumentanalyse is to tell phpstan to analyse. The third argumentapp is the folder that I want to analyse. We may not need to run it on entire project including vendor dependencies, so it is important to specify exact folders and/or files for a better performance.

Alright lets explore the Neovim Plugin for a moment.

How to create a Neovim Plugin?

Neovim provide a numerouslua apis to build a plugin. In fact the neovim source code has morelua code thanc. Wait what isLua? Lua is a powerful, efficient, lightweight, embeddable scripting language.

It is very simple to create a plugin in lua, all we have to do is to create alua/plugin.lua file in the neovim's config folder. It would be something looks like this~/.config/nvim/lua/phpstan.lua.

There are two ways to run a lua inside the neovim.
When the current buffer is a standalone lua file/plugin

:luafile %
Enter fullscreen modeExit fullscreen mode

It can be executed as a lua package using,

:lua require'phpstan'
Enter fullscreen modeExit fullscreen mode

How to show the diagnostic messages in Neovim?

Neovim has a diagnostic module and fluent api in lua. You can read the documenthere. As per the document,the flow would be,

  1. Create a namespace usingnvim_create_namespace()
  2. Generate a table of Diagnostic
  3. Set the diagnostics to the buffer

Take a moment to look at thevim.Diagnostic interfacehere

Most of them are optional keys, this is going to be our diagnostic table

localdiagnostic={lnum=1,col=0,message="",severity=vim.diagnostic.Severity.ERROR,code="class.NotFound"}
Enter fullscreen modeExit fullscreen mode

Plenary

The idea is to execute thephpstan command from lua and parse the output. But by defaultphpstan stdout is a table and it is difficult to parse. But it supports multiple output formats. We are gonna choosejson output because it is convenient for me to parse it and it has the more information than other formats. All we have to do is adding an additional option--error-format=json to the command.

In neovim we do not need to analyse the entire project but the current buffer. So the current buffers file name should be the last argument.

I choose the neovim’shttps://github.com/nvim-lua/plenary.nvim. It helps us to interact with system commands in async way by providing nice and apis.

localJob=require'plenary.job'Job:new({command='./vendor/bin/phpstan',args={'analyse','--error-format=json',vim.api.nvim_buf_get_name(vim.api.nvim_get_current_buf()),},cwd=vim.fn.getcwd(),on_exit=vim.schedule_wrap(function(j,return_val)-- Parse the output and generate the diagnosticsend),}):start()
Enter fullscreen modeExit fullscreen mode

Lets parse the output

First we should check thereturn_val . It the exit code of the command that we execute. It the current file does not have any errors, the exit code must be0 . So only the non zero exit code will have the output.

Lets eliminate the first

ifreturn_val==0thenreturnend
Enter fullscreen modeExit fullscreen mode

The objectj has the result in a table. Each line would be a separate element in the table. Since the error format isjson , all the json string will be in a single line. Vim has a built-in functionvim.json.decode to decode the json string and convert it to a lua table.

localresult=j:result()localresponse=vim.json.decode(result[1])
Enter fullscreen modeExit fullscreen mode

Wait, we first look in to the json output to parse it effectively.

{"totals":{"errors":0,"file_errors":3},"files":{"/home/praem90/projects/seolve-app/app/Http/Controllers/Twitter/TwitterAuthorizeController.php":{"errors":1,"messages":[{"message":"Instantiated class Abraham\\TwitterOAuth\\TwitterOAuth not found.","line":19,"ignorable":true,"tip":"Learn more at https://phpstan.org/user-guide/discovering-symbols","identifier":"class.notFound"}]},"/home/praem90/projects/seolve-app/app/Http/Controllers/Twitter/TwitterCallbackController.php":{"errors":2,"messages":[{"message":"Instantiated class Abraham\\TwitterOAuth\\TwitterOAuth not found.","line":20,"ignorable":true,"tip":"Learn more at https://phpstan.org/user-guide/discovering-symbols","identifier":"class.notFound"},{"message":"Instantiated class Abraham\\TwitterOAuth\\TwitterOAuth not found.","line":31,"ignorable":true,"tip":"Learn more at https://phpstan.org/user-guide/discovering-symbols","identifier":"class.notFound"}]},},"errors":[]}
Enter fullscreen modeExit fullscreen mode

The output is pretty clean. Thefiles element contains all the files. The each file contains an array of messages. Yes, you got it.

localnamespace=vim.api.nvim_create_namespace('phpstan')-- 1. Remember the first step of the vim.Diagnostic above-- 2. Generating the diagnostic messages tableforfile_pathinpairs(response.files)dolocalbufnr=vim.fn.bufnr(file_path);localdiagnostics={}foriinpairs(response.files[file_path].messages)dolocalmessage=response.files[file_path].messages[i]localdiagnostic={bufnr=bufnr,lnum=message.line,col=0,-- Since phpstan does not support columns, setting it to zeromessage=message.message,code=message.identifier,}ifmessage.ignorablethendiagnostic.severity=vim.diagnostic.severity.WARNelsediagnostic.severity=vim.diagnostic.severity.ERRORendtable.insert(diagnostics,diagnostic)endend
Enter fullscreen modeExit fullscreen mode

Now we have the table of diagnostic messages. Lets just publish it to the buffer.

vim.diagnostic.set(namespace,bufnr,diagnostics)-- 3. Publishing
Enter fullscreen modeExit fullscreen mode

Final notes

We cannot run this script manually. So we must bind it to aautocmd . For our use it must executed when we open up a file and modify it. Neovim provides a lot of events where we can add listeners to execute our custom scripts and that makes it so flexible and scalable.

The final code would be

localJob=require'plenary.job'localM={}localnamespace=vim.api.nvim_create_namespace('pream90.phpstan')M.analyse=function()Job:new({command='./vendor/bin/phpstan',args={'analyse','--error-format=json',vim.api.nvim_buf_get_name(vim.api.nvim_get_current_buf()),},cwd=vim.fn.getcwd(),on_exit=vim.schedule_wrap(function(j,return_val)ifreturn_val==0thenreturnendlocalresult=j:result()localresponse=vim.json.decode(result[1])forfile_pathinpairs(response.files)dolocalbufnr=vim.fn.bufnr(file_path);localdiagnostics={}foriinpairs(response.files[file_path].messages)dolocalmessage=response.files[file_path].messages[i]localdiagnostic={bufnr=bufnr,lnum=message.line,col=0,message=message.message,source=message.tip,code=message.identifier,namespace=namespace}ifmessage.ignorablethendiagnostic.severity=vim.diagnostic.severity.WARNelsediagnostic.severity=vim.diagnostic.severity.ERRORendtable.insert(diagnostics,diagnostic)endvim.diagnostic.set(namespace,bufnr,diagnostics)endend),}):start()endM.setup=function()-- Registering auto command to the files that ends with the php extensionvim.api.nvim_create_autocmd({"BufReadPre","BufWritePost"},{pattern={"*.php"},callback=M.analyse})endreturnM
Enter fullscreen modeExit fullscreen mode

We are returning a Lua table, we should call thesetup in your neovim’s init file.

In my case itsinit.vim

luarequire('phpstan').setup()
Enter fullscreen modeExit fullscreen mode

Reopen your neovim and then you will see the diagnostic messages.

Thanks

Top comments(0)

Subscribe
pic
Create template

Templates let you quickly answer FAQs or store snippets for re-use.

Dismiss

Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment'spermalink.

For further actions, you may consider blocking this person and/orreporting abuse

Yet another passionate and enthusiastic software engineer
  • Location
    Chennai, IN
  • Education
    Engg - dropout
  • Pronouns
    Mr
  • Work
    Travelnet Solutions
  • Joined

Trending onDEV CommunityHot

DEV Community

We're a place where coders share, stay up-to-date and grow their careers.

Log in Create account

[8]ページ先頭

©2009-2025 Movatter.jp