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 }