Search for Web Devs
In this tutorial, we’ll create a plugin that allows you to search for NPM packages and MDN pages.
Create your plugin using the template. You should have something like this now:
public class NpmAndMdnPlugin : ExtendedPlugin{}
Let’s add a response for when no command is selected:
[Search]public string EmptyQuery() => "Type 'npm' or 'mdn' to search for packages or MDN web docs";
NPM Search
Let’s add a response for when the user types npm
without any package name.
[Search(equalTo: "npm")]public string EmptyNpmQuery() => "Type 'npm' <package-name> to search for a package";
After googling for NPM search API, I found this documentation , which looks exactly like what we will need for the plugin. We’ll need the package name, version, description, and NPM link.
{ "objects": [ { "package": { "name": "yargs", "version": "6.6.0", "description": "yargs the modern, pirate-themed, successor to optimist.", "keywords": [ "argument", "args", "option", "parser", "parsing", "cli", "command" ], "date": "2016-12-30T16:53:16.023Z", "links": { "npm": "https://www.npmjs.com/package/yargs", "homepage": "http://yargs.js.org/", "repository": "https://github.com/yargs/yargs", "bugs": "https://github.com/yargs/yargs/issues" }, "publisher": { "username": "bcoe", }, "maintainers": [ { "username": "bcoe", }, { "username": "chevex", }, { "username": "nexdrew", }, { "username": "nylen", } ] }, "score": { "final": 0.9237841281241451, "detail": { "quality": 0.9270640902288084, "popularity": 0.8484861649808381, "maintenance": 0.9962706951777409 } }, "searchScore": 100000.914 } ], "total": 1, "time": "Wed Jan 25 2017 19:23:35 GMT+0000 (UTC)"}
Let’s describe this data in a class so we can deserialize JSON into this class.
Create a new file called NpmResponse.cs
next to your Main.cs
file.
This is where we will put all the classes that describe the data we get from the API.
As we see from the documentation, the API returns an object with an objects
property.
public record NpmResponse(NpmObject[] Objects);
Now let’s describe the NpmObject
class.
It has the following properties: package
, score
, searchScore
.
We’re only interested in the package
one, so let’s describe it.
public record NpmObject(NpmPackage Package);
The package
property has a few more properties interesting to us. Namely, these are:
name
— so we can display the name of the package we foundversion
— so we can display the version of the packagedescription
— so we can display the description of the packagelinks
— so we can let the user of our plugin go directly to the NPM page of the package
public record NpmPackage( string Name, string Version, string Description, NpmLinks Links);
And lastly, the links
property has two properties we’re interested in:
npm
and homepage
, so we could open these links for the user of our plugin.
public record NpmLinks(string Npm);
Now that we have our data classes, let’s add a method to our plugin that will search for NPM packages and display search results.
private const string NpmApiUrl = "https://registry.npmjs.com/-/v1/search?text=";
// ...
[Search(startsWith: "npm ")]public async Task<IEnumerable<ExtendedResult>?> SearchNpmQuery( ExtendedQuery query, CancellationToken token){ var response = await DownloadJson<NpmResponse>(NpmApiUrl, query.Search, token);
return response?.Objects.Select(v => new ExtendedResult { Title = $"{v.Package.Name} | v{v.Package.Version}", Subtitle = v.Package.Description, UrlToOpen = v.Package.Links.Npm, });}
We have successfully implemented the NPM search functionality.
Assuming you specified web
as your keyword when creating the plugin, here’s how it should look:
MDN Search
MDN search is a little bit different. We’ll be doing the search ourselves, locally. For that, we’ll need to download the full list of MDN pages.
But before that, let’s add a response for when the user types mdn
without any search query.
[Search(equalTo: "mdn")]public string EmptyMdnQuery() => "Type 'mdn' <search-term> to search MDN web docs";
Now, let’s download the MDN data.
It’s located
here .
This is an array of objects, each object representing an MDN page.
Each object only has two properties: title
and url
.
Simple enough.
Let’s begin!
First, create a new file called MdnResponse.cs
next to your Main.cs
file.
In this file, we’ll describe the object structure of the MDN data.
public record MdnArticle(string Title, string Url);
Now we need to actually download that data. We’ll do this on plugin startup, using the Init
attribute:
private const string MdnIndexUrl = "https://developer.mozilla.org/en-US/search-index.json";
private MdnArticle[] _mdnData = null!;
[Init]public async Task DownloadMdnData(){ var data = await DownloadJson<MdnArticle[]>(MdnIndexUrl);
_mdnData = data ?? Array.Empty<MdnArticle>();}
Now that we have the data, let’s add a method to our plugin that will search for MDN pages and display search results.
[Search(startsWith: "mdn ")]public IEnumerable<ExtendedResult> SearchMdnQuery( ExtendedQuery query, CancellationToken token){ return _mdnData .Where(v => Api.FuzzySearch(query.Search, v.Title).Success) .Select(v => new ExtendedResult { Title = v.Title, UrlToOpen = OpenMdnUrlPrefix + v.Url, });}
Let’s test it out.
It works! We have successfully implemented the MDN search functionality.
The Code
Here are all the files from this tutorial:
public class NpmAndMdnPlugin : ExtendedPlugin{ private const string OpenMdnUrlPrefix = "https://developer.mozilla.org";
private const string NpmApiUrl = "https://registry.npmjs.com/-/v1/search?text="; private const string MdnIndexUrl = "https://developer.mozilla.org/en-US/search-index.json";
private MdnArticle[] _mdnData = null!;
[Init] public async Task DownloadMdnData() { var data = await DownloadJson<MdnArticle[]>(MdnIndexUrl);
_mdnData = data ?? Array.Empty<MdnArticle>(); }
[Search] public string EmptyQuery() => "Type 'npm' or 'mdn' to search for packages or MDN web docs";
[Search(equalTo: "npm")] public string EmptyNpmQuery() => "Type 'npm' <package-name> to search for a package";
[Search(startsWith: "npm ")] public async Task<IEnumerable<ExtendedResult>?> SearchNpmQuery( ExtendedQuery query, CancellationToken token) { var response = await DownloadJson<NpmResponse>( NpmApiUrl, query.Search, token );
return response?.Objects.Select(v => new ExtendedResult { Title = $"{v.Package.Name} | v{v.Package.Version}", Subtitle = v.Package.Description, UrlToOpen = v.Package.Links.Npm, }); }
[Search(equalTo: "mdn")] public string EmptyMdnQuery() => "Type 'mdn' <search-term> to search MDN web docs";
[Search(startsWith: "mdn ")] public IEnumerable<ExtendedResult> SearchMdnQuery( ExtendedQuery query, CancellationToken token) { return _mdnData .Where(v => Api.FuzzySearch(query.Search, v.Title).Success) .Select(v => new ExtendedResult { Title = v.Title, UrlToOpen = OpenMdnUrlPrefix + v.Url, }); }}
public record NpmResponse(NpmObject[] Objects);public record NpmObject(NpmPackage Package);public record NpmPackage( string Name, string Version, string Description, NpmLinks Links);public record NpmLinks(string Npm);
public record MdnArticle(string Title, string Url);