SolutionHub Communication Interface

Return codes

General return codes when communicating with SolutionHub
enum SolutionHubResults {
	SH_NO_ERROR = 0,
	SH_ERROR,
	SH_ERROR_INCOMPATIBLE_VERSION,

	SH_ERROR_NO_CONNECTION,
	SH_ERROR_NO_SOLUTION,
	SH_ERROR_NO_ATTRIBUTE,

	SH_ERROR_HOOK_DEFINED,		// Registering same plugin twice
	SH_ERROR_BUFFER_TO_SMALL,
	SH_ERROR_INVALID_JSON
};

Registering a plugin to SolutionHub

If a plugin wants/needs a 'own' solution that can be bound, it needs to register to SolutionHub.
A plugin do not have to do this, if for example it only query attributes of a named solution (more on queries later).
#define NPPM_SOLUTIONHUB_START						100
#define NPP_SH_COM_INTERFACE_VERSION				2

#define NPP_SH_RCMASK_NONE							0x00000000	// No indexing needed
#define NPP_SH_RCMASK_INDEXING						0x00000001	// Indexing needed
#define NPP_SH_RCMASK_ATTACH						0x00000002	// Attach to another plugin alias

//! Hook a plugin with a named alias
struct SHMarker {
	static const int marker = NPP_SH_COM_INTERFACE_VERSION;
	SHMarker() : _marker(marker) {}
	const int _marker;
};

struct SolutionHubRegisterContext : SHMarker {
	const char *alias;
	unsigned int mask;

	int result;
};
#define NPPM_SOLUTIONHUB_HOOK_RECEIVER				NPPM_SOLUTIONHUB_START+1
#define NPPM_SOLUTIONHUB_UNHOOK_RECEIVER			NPPM_SOLUTIONHUB_START+2

Examples of registering a plugin

All communication uses the 'NPPM_MSGTOPLUGIN' message to communicate with SolutionHub.
NOTE :	If you have the habit of 'memset:ing' you structs this will fail as you overwrite the marker,
	and you will get 'SH_ERROR_INCOMPATIBLE_VERSION' even though you have the correct version.

Here is the register function TortoiseSVN plugin uses :
	void register_to_solutionhub() {
		#define SOLUTIONHUB_DLL_FILE_NAME L"nppplugin_solutionhub.dll"

		SolutionHubRegisterContext rc;
		rc.alias = "tsvn";
		rc.mask = NPP_SH_RCMASK_NONE;

		CommunicationInfo comm;
		comm.internalMsg = NPPM_SOLUTIONHUB_HOOK_RECEIVER;
		comm.srcModuleName = npp_plugin::getModuleName()->c_str();
		comm.info = &rc;

		bool msg_res = ::SendMessage(npp_plugin::hNpp(), NPPM_MSGTOPLUGIN, (WPARAM)SOLUTIONHUB_DLL_FILE_NAME, (LPARAM)&comm);
	}
As you can see it uses the 'NPP_SH_RCMASK_NONE' while registering. That basically means that it do not need indexing of files in order to work.
Open File In Solution(OFIS) on the other hand needs this in order to search for files and uses the mask 'NPP_SH_RCMASK_INDEXING' when registering.

The return values in the SolutionHubRegisterContext (the result member) is either :
	'SH_NO_ERROR' - All went OK.
	'SH_ERROR_HOOK_DEFINED' - Which means the alias is already used by another plugin.
	'SH_ERROR_INCOMPATIBLE_VERSION' - Which means that SolutionHub has been updated and you need to get the lastest headers and rebuild your plugin.

Query attributes in a solution present in the SolutionHub

A solution can have 'attributes' which basically is just a key-value storage. Just set the attibutes for the solution that is you are interested in
and use these queries to get them from the SolutionHub. Personally I use it for some extra data attached to a solution so I do not have to make a
new "config/setting" GUI for every plugin I make (and it is also nice to have all settings in one place).
//! Queries
struct AttributeQuery
{
	 //! Solution name is ignored if using 'HOOKED' query
	const char *solution_name;
	const char *attribute_name;

	char *result_buffer;
	unsigned int result_buffersize;

	int result;
};

#define NPPM_SOLUTIONHUB_QUERY_ATTRIBUTE_HOOKED			NPPM_SOLUTIONHUB_START+3
#define NPPM_SOLUTIONHUB_QUERY_ATTRIBUTE				NPPM_SOLUTIONHUB_START+4
As stated in the comment above, you do not have to register to SolutionHub in order to query attributes of a solution. If you use 'NPPM_SOLUTIONHUB_QUERY_ATTRIBUTE'
then it is expected that you enter a solution name in the query. It will get the attribute of that solution and not the solution that has been connected to the plugin,
as it is not a requirement that a plugin register to SolutionHub in order to use its facilities (it means that you will have to keep track of the solution(s) to be queried
by yourself though). I personally does not use this but use 'NPPM_SOLUTIONHUB_QUERY_ATTRIBUTE_HOOKED' instead (as I always want to use the solution that is bound to the plugins connection).
But I am keeping options open.

Here comes a usage example (from TortoiseSVN plugin again) :
	void get_svn_root_directory() {
		#define SOLUTIONHUB_DLL_FILE_NAME L"nppplugin_solutionhub.dll"

		char t[256] = {};
		AttributeQuery aq; memset(&aq, 0, sizeof(aq));
		aq.attribute_name = "svn_root_dir";
		aq.result_buffer = t;
		aq.result_buffersize = 256;

		CommunicationInfo comm;
		comm.internalMsg = NPPM_SOLUTIONHUB_QUERY_ATTRIBUTE_HOOKED;
		comm.srcModuleName = npp_plugin::getModuleName()->c_str();

		comm.info = &aq;

		BOOL msg_res = ::SendMessage(npp_plugin::hNpp(), NPPM_MSGTOPLUGIN, (WPARAM)SOLUTIONHUB_DLL_FILE_NAME, (LPARAM)&comm);

		if(aq.result == SolutionHubResults::SH_NO_ERROR && *t) {
			// success
		} else {
			// failure
		}
	}

All attributes is stored as strings so if you want to store integers you have to convert the string when the result comes back.

For convenience I also made it possible to get all attributes in one call. As all my communication is through Json you will get a
Json object with all key-value pairs in the result.

Here is the needed information (same return codes as when using 'query attribute') :
struct GetAttributesQuery
{
	const char *solution_name; //! Solution name is ignored if using 'HOOKED' query

	char *result_buffer;
	unsigned int result_buffersize;

	int result;
};

#define NPPM_SOLUTIONHUB_GET_ATTRIBUTES_HOOKED			NPPM_SOLUTIONHUB_START+5
#define NPPM_SOLUTIONHUB_GET_ATTRIBUTES					NPPM_SOLUTIONHUB_START+6

Searching for files in a solution present in the SolutionHub

If you register you plugin with the flag 'NPP_SH_RCMASK_INDEXING' you are telling SolutionHub to index your files (alternatively you are using 'NPP_SH_RCMASK_ATTACH'
while registering to a plugin that is known to index).
I have provided a simple UI for managing your solutions, as SolutionHub does not have a UI by default. SolutionHub have a different
set of messages to manage(save/delete/modify) the solutions but I will document them elsewhere as they are only interesting to a handful of people(if any).

Anyway here is the search structs :
struct SearchRequest
{
	const wchar_t *searchstring;
	unsigned int result_notification;

	void *userdata;
	unsigned int userdata_size;

	int result; // all other that 0 is fail
};

struct SearchResponse
{
	const char *data;
	unsigned data_size;

	void *userdata;
	unsigned int userdata_size;
};

#define NPPM_SOLUTIONHUB_SEARCH_SOLUTION			NPPM_SOLUTIONHUB_START+7
As you can see there is a option to store data with the search request that will be provided in the search response.
I do not use it atm but thought of a couple of places where/when it might come in handy so I implemented it. I think it
is straightforward how it works, if not, let me know and I will provide some examples.

NOTE : If you are *NOT* attaching userdata, be sure to set the field 'userdata_size' to 0(zero).

It is easier to explain if I show a example(not userdata), this time from the Open File In Solution(OFIS) plugin :
	void solution_search(const wchar_t *s) {
		#define SOLUTIONHUB_DLL_FILE_NAME L"nppplugin_solutionhub.dll"

		SearchRequest sr; memset(&sr, 0, sizeof(sr));
		sr.result_notification = NPPM_OFIS_ON_SEARCH_RESPONSE;
		sr.searchstring = s;

		CommunicationInfo comm;
		comm.internalMsg = NPPM_SOLUTIONHUB_SEARCH_SOLUTION;	// a define
		comm.srcModuleName = npp_plugin::getModuleName()->c_str();
		comm.info = &sr;

		bool msg_res = ::SendMessage(npp_plugin::hNpp(), NPPM_MSGTOPLUGIN, (WPARAM)SOLUTIONHUB_DLL_FILE_NAME, (LPARAM)&comm);

		if(msg_res && sr.result != SolutionHubResults::SH_NO_ERROR) {
			if(sr.result == SolutionHubResults::SH_ERROR_NO_CONNECTION) {
				// no connection setup
			}
		} else if(msg_res) {
			// success
		}
	}
All search queries is asynchronous(you do not get the result back at the callpoint). As you can see there a variable named 'result_notification' in the SearchRequest,
this is used to send back the result to your plugin. All plugins export a messageproc that will get notifications from N++ and here is the usage of it in OFIS :
extern "C" __declspec(dllexport) LRESULT messageProc(UINT Message, WPARAM wParam, LPARAM lParam)
{
	using namespace npp_plugin;

	switch (Message)
	{
		case NPPM_MSGTOPLUGIN:
		{
			CommunicationInfo *comm = (CommunicationInfo *)(lParam);

			switch (comm->internalMsg)
			{
				case NPPM_OFIS_ON_SEARCH_RESPONSE :
					{
						npp_plugin_ofis::on_search_response(comm->info);

						return TRUE;
					}
					break;
				default:
					return FALSE;
			}
		}
		default:
			return FALSE;
	}
}
As you can see we will get back the same notification message that was sent when we made our search.
Now we will look at the result handling.
	void on_search_response(void *data)
	{
		SearchResponse &sr = *((SearchResponse*)data);
		const char *buffer = (const char *)sr.data;

		const unsigned datasize = sr.data_size;

		record_data.resize(datasize);	// record_data is a std::vector
		memcpy(&record_data[0], buffer, datasize);

		filerecords = FileRecords(&record_data[0]);

		// This is obviously not present in the plugin, just to show.
		for(unsigned i=0; i < filerecords.num_records;++i) {
			FileRecord fr = filerecords.filerecord(i);
			printf("filerecord(%d) : filename(%S), path(%S), date(%S)", i, fr.filename, fr.path, fr.date);
		}
	}
An important note is that the buffer is only valid in this call (so do not try to save the pointer).
If you need to keep the data you have to allocate storage for it and copy it (as above).
FileRecords is a struct that wraps the indexing of the individual filerecords present, the exact layout is not important
and but it is present in the distribution.

Quick note on 'search-format'

Searching has a couple of keywords, namely '\' and '-'. The default search is 'token' based and every space in the search string makes a new token.
The tokens is then searched in the filename(usually, more on that later) and if the string to be seached has all tokens IN ORDER that was defined by the search,
we have a match.

Examples (of the basic search) :

Lets say we have 4 files in our directory, foobar.h, foobar.cpp, barfoo.h and barfoo.cpp.

Search strings :
'foo' - will match all files as 'foo' is present in all files.
'foo cpp' - will match foobar.cpp and barfoo.cpp
'foo bar' - will match foobar.h and foobar.cpp.

Now lets explain the keywords.
If the token starts with a '\', it will signal that the whole path(and filename ofcourse) will be used for searching.
You only have to include this once per search string so there is no point of searching like '\svn \work foo bar' (this could also be formatted like '\svn\work foo bar'.
The searchstring just explained will search in your (I imagine) folders 'svn\work' for files that includes 'foo' and 'bar'.

If the token starts with a '-' this token is used to exclude tokens in your searchstring. It does *NOT* respect order, in other words if the token that follows the '-'
is present in the filename(or path+filename) it will NOT be included in the result.
So there is no difference between the searchstrings : 'foo -cpp' and '-cpp foo' (if you see it from the results point of view)