1 module served.utils.events;
2 
3 /// Called for requests (not notifications) from the client to the server. This
4 /// UDA must be used at most once per method for regular methods. For methods
5 /// returning arrays (T[]) it's possible to register multiple functions with the
6 /// same method. In this case, if the client supports it, partial results will
7 /// be sent for each returning method, meaning the results are streamed. In case
8 /// the client does not support partial methods, all results will be
9 /// concatenated together and returned as one.
10 struct protocolMethod
11 {
12 	string method;
13 }
14 
15 /// Called after the @protocolMethod for this method is handled. May have as
16 /// many handlers registered as needed. When the actual protocol method is a
17 /// partial method (multiple handlers, returning array) this will be ran on each
18 /// chunk returned by every handler. In that case the handler will be run
19 /// multiple times on different fibers.
20 struct postProtocolMethod
21 {
22 	string method;
23 }
24 
25 struct protocolNotification
26 {
27 	string method;
28 }
29 
30 /// Event called when all components have been registered but no workspaces have
31 /// been setup yet.
32 /// Signature: `()`
33 enum onRegisteredComponents;
34 
35 /// Event called when a project is available but not intended to be loaded yet.
36 /// Should not access any components, otherwise it will force a load, but only
37 /// show hints in the UI. When it's accessed and actually being loaded the
38 /// events `onAddingProject` and `onAddedProject` will be emitted.
39 /// Signature: `(WorkspaceD.Instance, string dir, string uri)`
40 enum onProjectAvailable;
41 
42 /// Event called when a new workspaced instance is created. Called before dub or
43 /// fsworkspace is loaded.
44 /// Signature: `(WorkspaceD.Instance, string dir, string uri)`
45 enum onAddingProject;
46 
47 /// Event called when a new project root is finished setting up. Called when all
48 /// components are loaded. DCD is loaded but not yet started at this point.
49 /// Signature: `(WorkspaceD.Instance, string dir, string rootFolderUri)`
50 enum onAddedProject;
51 
52 struct EventProcessorConfig
53 {
54 	string[] allowedDuplicateMethods = ["object", "served", "std", "io", "workspaced", "fs"];
55 }
56 
57 /// Implements the event processor for a given extension module exposing a
58 /// `members` field defining all potential methods.
59 mixin template EventProcessor(alias ExtensionModule, EventProcessorConfig config = EventProcessorConfig.init)
60 {
61 	static if (__traits(compiles, { import core.lifetime : forward; }))
62 		import core.lifetime : forward;
63 	else
64 		import std.functional : forward;
65 
66 	import std.algorithm;
67 	import std.json;
68 	import std.meta;
69 	import std.traits;
70 
71 	import painlessjson;
72 
73 	// duplicate method name check to avoid name clashes and unreadable error messages
74 	private string[] findDuplicates(string[] fields)
75 	{
76 		string[] dups;
77 		Loop: foreach (i, field; fields)
78 		{
79 			static foreach (allowed; config.allowedDuplicateMethods)
80 				if (field == allowed)
81 					continue Loop;
82 
83 			if (fields[0 .. i].canFind(field) || fields[i + 1 .. $].canFind(field))
84 				dups ~= field;
85 		}
86 		return dups;
87 	}
88 
89 	enum duplicates = findDuplicates([ExtensionModule.members]);
90 	static if (duplicates.length > 0)
91 	{
92 		pragma(msg, "duplicates: ", duplicates);
93 		static assert(false, "Found duplicate method handlers of same name");
94 	}
95 
96 	/// Calls all protocol methods in `ExtensionModule` matching a certain method
97 	/// and method type.
98 	/// Params:
99 	///  UDA = The UDA to filter the methods with. This must define a string member
100 	///     called `method` which is compared with the runtime `method` argument.
101 	///  callback = The callback which is called for every matching function with
102 	///     the given UDA and method name. Called with arguments `(string name,
103 	///     void delegate() callSymbol, UDA uda)` where the `callSymbol` function is
104 	///     a parameterless function which automatically converts the JSON params
105 	///     and additional available arguments based on the method overload and
106 	///     calls it.
107 	///  returnFirst = If `true` the callback will be called at most once with any
108 	///     unspecified matching method. If `false` the callback will be called with
109 	///     all matching methods.
110 	///  method = the runtime method name to compare the UDA names with
111 	///  params = the JSON arguments for this protocol event, automatically
112 	///     converted to method arguments on demand.
113 	///  availableExtraArgs = static extra arguments available to pass to the method
114 	///     calls. `out`, `ref` and `lazy` are perserved given the method overloads.
115 	///     overloads may consume anywhere between 0 to Args.length of these
116 	///     arguments.
117 	/// Returns: `true` if any method has been called, `false` otherwise.
118 	bool emitProtocol(alias UDA, alias callback, bool returnFirst, Args...)(string method,
119 			JSONValue params, Args availableExtraArgs)
120 	{
121 		return iterateExtensionMethodsByUDA!(UDA, (name, symbol, uda) {
122 			if (uda.method == method)
123 			{
124 				debug (PerfTraceLog) mixin(traceStatistics(uda.method ~ ":" ~ name));
125 
126 				alias symbolArgs = Parameters!symbol;
127 
128 				auto callSymbol()
129 				{
130 					static if (symbolArgs.length == 0)
131 					{
132 						return symbol();
133 					}
134 					else static if (symbolArgs.length == 1)
135 					{
136 						return symbol(fromJSON!(symbolArgs[0])(params));
137 					}
138 					else static if (availableExtraArgs.length > 0
139 						&& symbolArgs.length <= 1 + availableExtraArgs.length)
140 					{
141 						return symbol(fromJSON!(symbolArgs[0])(params), forward!(
142 							availableExtraArgs[0 .. symbolArgs.length + -1]));
143 					}
144 					else
145 					{
146 						static assert(0, "Function for " ~ name ~ " can't have more than one argument");
147 					}
148 				}
149 
150 				callback(name, &callSymbol, uda);
151 				return true;
152 			}
153 			else
154 				return false;
155 		}, returnFirst);
156 	}
157 
158 	/// Same as emitProtocol, but for the callback instead of getting a delegate
159 	/// to call, you get a function pointer and a tuple with the arguments for
160 	/// each instantiation that can be expanded.
161 	///
162 	/// So the callback gets called like `callback(name, symbol, arguments, uda)`
163 	/// and the implementation can then call the symbol function using
164 	/// `symbol(arguments.expand)`.
165 	///
166 	/// This works around scoping issues and copies the arguments once more on
167 	/// invocation, causing ref/out parameters to get lost however. Allows to
168 	/// copy the arguments to other fibers for parallel processing.
169 	bool emitProtocolRaw(alias UDA, alias callback, bool returnFirst)(string method,
170 			JSONValue params)
171 	{
172 		import std.typecons : tuple;
173 
174 		T parseParam(T)()
175 		{
176 			import served.lsp.protocol;
177 
178 			try
179 			{
180 				if (params.type == JSONType.array)
181 				{
182 					// positional parameter support
183 					// only supports passing a single argument
184 					auto arr = params.array;
185 					if (arr.length != 1)
186 						throw new Exception("Mismatched parameter count");
187 					return fromJSON!T(arr[0]);
188 				}
189 				else
190 				{
191 					// named parameter support
192 					// only supports passing structs (not parsing names of D method arguments)
193 					return fromJSON!T(params);
194 				}
195 			}
196 			catch (Exception e)
197 			{
198 				ResponseError error;
199 				error.code = ErrorCode.invalidParams;
200 				error.message = "Failed converting input parameter " ~ params.toPrettyString ~ " to needed type `" ~ T.stringof ~ "`: " ~ e.msg;
201 				error.data = JSONValue(e.toString);
202 				throw new MethodException(error);
203 			}
204 		}
205 
206 		return iterateExtensionMethodsByUDA!(UDA, (name, symbol, uda) {
207 			if (uda.method == method)
208 			{
209 				debug (PerfTraceLog) mixin(traceStatistics(uda.method ~ ":" ~ name));
210 
211 				alias symbolArgs = Parameters!symbol;
212 
213 				static if (symbolArgs.length == 0)
214 				{
215 					auto arguments = tuple();
216 				}
217 				else static if (symbolArgs.length == 1)
218 				{
219 					auto arguments = tuple(parseParam!(symbolArgs[0]));
220 				}
221 				else static if (availableExtraArgs.length > 0
222 					&& symbolArgs.length <= 1 + availableExtraArgs.length)
223 				{
224 					auto arguments = tuple(parseParam!(symbolArgs[0]), forward!(
225 						availableExtraArgs[0 .. symbolArgs.length + -1]));
226 				}
227 				else
228 				{
229 					static assert(0, "Function for " ~ name ~ " can't have more than one argument");
230 				}
231 
232 				callback(name, symbol, arguments, uda);
233 				return true;
234 			}
235 			else
236 				return false;
237 		}, returnFirst);
238 	}
239 
240 	bool emitExtensionEvent(alias UDA, Args...)(Args args)
241 	{
242 		return iterateExtensionMethodsByUDA!(UDA, (name, symbol, uda) {
243 			symbol(forward!args);
244 			return true;
245 		}, false);
246 	}
247 
248 	/// Iterates through all public methods in `ExtensionModule` annotated with the
249 	/// given UDA. For each matching function the callback paramter is called with
250 	/// the arguments being `(string name, Delegate symbol, UDA uda)`. `callback` is
251 	/// expected to return a boolean if the UDA values were a match.
252 	///
253 	/// Params:
254 	///  UDA = The UDA type to filter methods with. Methods can just have an UDA
255 	///     with this type and any values. See $(REF getUDAs, std.traits)
256 	///  callback = Called for every matching method. Expected to have 3 arguments
257 	///     being `(string name, Delegate symbol, UDA uda)` and returning `bool`
258 	///     telling if the uda values were a match or not. The Delegate is most
259 	///     often a function pointer to the given symbol and may differ between all
260 	///     calls.
261 	///
262 	///     If the UDA is a symbol and not a type (such as some enum manifest
263 	///     constant), then the UDA argument has no meaning and should not be used.
264 	///  returnFirst = if `true`, once callback returns `true` immediately return
265 	///     `true` for the whole function, otherwise `false`. If this is set to
266 	///     `false` then callback will be run on all symbols and this function
267 	///     returns `true` if any callback call has returned `true`.
268 	/// Returns: `true` if any callback returned `true`, `false` otherwise or if
269 	///     none were called. If `returnFirst` is set this function returns after
270 	///     the first successfull callback call.
271 	bool iterateExtensionMethodsByUDA(alias UDA, alias callback, bool returnFirst)()
272 	{
273 		bool found = false;
274 		foreach (name; ExtensionModule.members)
275 		{
276 			static if (__traits(compiles, __traits(getMember, ExtensionModule, name)))
277 			{
278 				// AliasSeq to workaround AliasSeq members
279 				alias symbols = AliasSeq!(__traits(getMember, ExtensionModule, name));
280 				static if (symbols.length == 1 && hasUDA!(symbols[0], UDA))
281 				{
282 					alias symbol = symbols[0];
283 					static if (isSomeFunction!(symbol) && __traits(getProtection, symbol) == "public")
284 					{
285 						static if (__traits(compiles, { enum uda = getUDAs!(symbol, UDA)[0]; }))
286 							enum uda = getUDAs!(symbol, UDA)[0];
287 						else
288 							enum uda = null;
289 
290 						static if (returnFirst)
291 						{
292 							if (callback(name, &symbol, uda))
293 								return true;
294 						}
295 						else
296 						{
297 							if (callback(name, &symbol, uda))
298 								found = true;
299 						}
300 					}
301 				}
302 			}
303 		}
304 
305 		return found;
306 	}
307 }