1 module served.utils.serverconfig; 2 3 /// UDA event called when configuration for any workspace or the unnamed 4 /// workspace got changed. 5 /// 6 /// Expected method signature: 7 /// ```d 8 /// @onConfigChanged 9 /// void changedConfig(ConfigWorkspace target, string[] paths, T config) 10 /// ``` 11 /// where `T` is the template argument to `mixin ConfigHandler!T`. 12 enum onConfigChanged; 13 14 /// UDA event called when all workspaces are processed in configuration 15 /// changes. 16 /// 17 /// Expected method signature: 18 /// ```d 19 /// @onConfigFinished 20 /// void configFinished(size_t count) 21 /// ``` 22 enum onConfigFinished; 23 24 /// 25 struct ConfigWorkspace 26 { 27 /// Workspace URI, resolved to a local workspace URI, or null if none found. 28 /// May be null for invalid workspaces or if this is the unnamed workspace. 29 /// Check `isUnnamedWorkspace` to see if this is the unnamed workspace. 30 string uri; 31 /// Only true _iff_ this config applies to the unnamed workspace (folder-less workspace) 32 bool isUnnamedWorkspace; 33 /// 0-based index which workspace is being processed out of the total count. (for progress reporting) 34 size_t index; 35 /// Number of workspaces which are being processed right now in total. (for progress reporting) 36 size_t numWorkspaces; 37 38 static ConfigWorkspace exactlyOne(string uri) 39 { 40 return ConfigWorkspace(uri, false, 0, 1); 41 } 42 43 static ConfigWorkspace unnamedWorkspace() 44 { 45 return ConfigWorkspace(null, true, 0, 1); 46 } 47 48 string toString() const @safe { 49 import std.conv : text; 50 51 return isUnnamedWorkspace 52 ? "(unnamed workspace)" 53 : uri; 54 } 55 } 56 57 mixin template ConfigHandler(TConfig) 58 { 59 import served.lsp.protocol; 60 import served.lsp.jsonops; 61 import served.utils.events; 62 63 private struct TConfigHolder 64 { 65 import std.array; 66 67 TConfig config; 68 alias config this; 69 70 private static void compare(string prefix, T)(ref Appender!(string[]) changed, ref T a, ref T b) 71 { 72 foreach (i, ref lhs; a.tupleof) 73 { 74 alias SubT = typeof(a.tupleof[i]); 75 // if the value is a simple struct, which is assumed to be user-defined, go through it 76 static if (is(SubT == struct) 77 && __traits(getAliasThis, SubT).length == 0 78 && !isVariant!SubT) 79 { 80 compare!(prefix ~ __traits(identifier, a.tupleof[i]) ~ ".")(changed, 81 a.tupleof[i], b.tupleof[i]); 82 } 83 else 84 { 85 if (a.tupleof[i] != b.tupleof[i]) 86 changed ~= (prefix ~ __traits(identifier, a.tupleof[i])); 87 } 88 } 89 } 90 91 string[] replace(TConfig newConfig) 92 { 93 string[] ret; 94 static foreach (i; 0 .. TConfig.tupleof.length) 95 ret ~= replaceSection!i(newConfig.tupleof[i]); 96 return ret; 97 } 98 99 string[] replaceSection(size_t tupleOfIdx)(typeof(TConfig.tupleof[tupleOfIdx]) newValue) 100 { 101 auto ret = appender!(string[]); 102 compare!(__traits(identifier, TConfig.tupleof[tupleOfIdx]) ~ ".")( 103 ret, config.tupleof[tupleOfIdx], newValue); 104 config.tupleof[tupleOfIdx] = newValue; 105 return ret.data; 106 } 107 108 string[] replaceAllSectionsJson(string[] settingJsons) 109 { 110 assert(settingJsons.length >= TConfig.tupleof.length); 111 auto changed = appender!(string[]); 112 static foreach (i; 0 .. TConfig.tupleof.length) 113 {{ 114 auto json = settingJsons[i]; 115 if (json == `null` || json.isEmptyJsonObject) 116 changed ~= this.replaceSection!i(typeof(TConfig.tupleof[i]).init); 117 else 118 changed ~= this.replaceSection!i(json.deserializeJson!(typeof(TConfig.tupleof[i]))); 119 }} 120 return changed.data; 121 } 122 } 123 124 TConfigHolder[DocumentUri] perWorkspaceConfigurationStore; 125 TConfigHolder* globalConfiguration; 126 127 __gshared bool syncedConfiguration = false; 128 __gshared bool syncingConfiguration = false; 129 130 private __gshared bool _hasConfigurationCapability = false; 131 private __gshared TConfig* initializeConfig = null; 132 133 private __gshared bool nonStandardConfiguration = false; 134 135 @onInitialize 136 void postInit_setupConfig(InitializeParams params) 137 { 138 auto workspaces = params.getWorkspaceFolders; 139 foreach (workspace; workspaces) 140 perWorkspaceConfigurationStore[workspace.uri] = TConfigHolder.init; 141 142 if (workspaces.length) 143 globalConfiguration = workspaces[0].uri in perWorkspaceConfigurationStore; 144 else 145 globalConfiguration = new TConfigHolder(); 146 147 _hasConfigurationCapability = capabilities 148 .workspace.orDefault 149 .configuration.orDefault; 150 151 if (!params.initializationOptions.isNone) { 152 // we might have the following options 153 // - nonStandardConfiguration - `bool` 154 // - startupConfiguration - a Configuration object 155 // 156 // this lets us initialize with a configuration right away, without 157 // waiting for a client - or an editor extension. 158 // 159 // Editor extensions can use `nonStandardConfiguration` to 160 // circumvent limitations in the LSP frameworks they have to work 161 // with. 162 auto options = params.initializationOptions.deref.get!(StringMap!JsonValue); 163 164 const nsc = "nonStandardConfiguration" in options; 165 if (nsc) { 166 nonStandardConfiguration = nsc.get!bool; 167 } 168 169 const settings = "startupConfiguration" in options; 170 if (settings) { 171 initializeConfig = new TConfig(); 172 *initializeConfig = jsonValueTo!TConfig(*settings); 173 } 174 } 175 } 176 177 @protocolNotification("initialized") 178 void setupConfig_Initialized(InitializedParams params) 179 { 180 import served.utils.async : setTimeout; 181 182 if (initializeConfig) 183 { 184 processConfigChange(*initializeConfig); 185 initializeConfig = null; 186 } 187 else 188 { 189 // add 250ms timeout after `initialized` notification to give clients 190 // the chance to send `workspace/didChangeConfiguration` proactively 191 // before requesting all configs ourselves. 192 enum waitTimeMs = 250; 193 setTimeout({ 194 setupConfig_loadAfterTimeout(); 195 }, waitTimeMs); 196 } 197 } 198 199 private void setupConfig_loadAfterTimeout() 200 { 201 if (!syncedConfiguration && !syncingConfiguration) 202 { 203 syncedConfiguration = true; 204 if (_hasConfigurationCapability) 205 { 206 if (!syncConfiguration(null, 0, perWorkspaceConfigurationStore.length + 1)) 207 error("Syncing user configuration failed!"); 208 209 warning( 210 "Didn't receive any configuration notification, manually requesting all configurations now"); 211 212 int i; 213 foreach (uri, cfg; perWorkspaceConfigurationStore) 214 syncConfiguration(uri, ++i, perWorkspaceConfigurationStore.length + 1); 215 216 emitExtensionEvent!onConfigFinished(perWorkspaceConfigurationStore.length); 217 } 218 else 219 { 220 warning("This Language Client doesn't support configuration requests and also didn't send any " 221 ~ "configuration to serve-d. Initializing using default configuration"); 222 223 assert(globalConfiguration, __FUNCTION__ ~ " called while globalConfiguration wasn't initialized"); 224 225 emitExtensionEvent!onConfigChanged(ConfigWorkspace.unnamedWorkspace, null, globalConfiguration.config); 226 } 227 } 228 } 229 230 @protocolNotification("workspace/didChangeConfiguration") 231 void didChangeConfiguration(RootJsonToken params) 232 { 233 import std.exception; 234 if (nonStandardConfiguration) { // client prefers non-standard API 235 return; 236 } 237 enforce(params.json.looksLikeJsonObject, "invalid non-object parameter to didChangeConfiguration"); 238 auto settings = params.json.parseKeySlices!"settings".settings; 239 enforce(settings.length, `didChangeConfiguration must contain a "settings" key`); 240 241 processConfigChange(settings.deserializeJson!TConfig); 242 } 243 244 @protocolNotification("served/didChangeConfiguration") 245 void didChangeConfigurationNonStd(RootJsonToken params) 246 { 247 import std.exception; 248 info("switching to nonstandard configuration mechanism"); 249 nonStandardConfiguration = true; // client prefers non-standard API 250 enforce(params.json.looksLikeJsonObject, "invalid non-object parameter to served/didChangeConfiguration"); 251 auto settings = params.json.parseKeySlices!"settings".settings; 252 enforce(settings.length, `served/didChangeConfiguration must contain a "settings key"`); 253 254 processConfigChange(settings.deserializeJson!TConfig); 255 } 256 257 private void processConfigChange(TConfig configuration, bool allowConfigurationRequest = true) 258 { 259 syncingConfiguration = true; 260 scope (exit) 261 { 262 syncingConfiguration = false; 263 syncedConfiguration = true; 264 } 265 266 if (_hasConfigurationCapability 267 && allowConfigurationRequest 268 && perWorkspaceConfigurationStore.length >= 2) 269 { 270 ConfigurationItem[] items; 271 items = getGlobalConfigurationItems(); // default workspace 272 const stride = TConfig.tupleof.length; 273 274 foreach (uri, cfg; perWorkspaceConfigurationStore) 275 items ~= getConfigurationItems(uri); 276 277 trace("Re-requesting configuration from client because there is more than 1 workspace"); 278 auto res = rpc.sendRequest("workspace/configuration", ConfigurationParams(items)); 279 280 const expected = perWorkspaceConfigurationStore.length + 1; 281 string[] settings = validateConfigurationItemsResponse(res, expected); 282 if (!settings.length) 283 { 284 trace("Config request failed, so falling back to global config..."); 285 return processConfigChange(configuration, false); 286 } 287 288 assert(globalConfiguration, __FUNCTION__ ~ " called while globalConfiguration wasn't initialized"); 289 290 for (size_t i = 0; i < expected; i++) 291 { 292 const isDefault = i == 0; 293 auto workspace = isDefault 294 ? globalConfiguration 295 : items[i * stride].scopeUri.deref in perWorkspaceConfigurationStore; 296 297 if (!workspace) 298 { 299 error("Could not find workspace URI response ", 300 items[i * stride].scopeUri.deref, 301 " in requested configurations?"); 302 continue; 303 } 304 305 string[] changed = workspace.replaceAllSectionsJson(settings[i * stride .. $]); 306 emitExtensionEvent!onConfigChanged( 307 ConfigWorkspace( 308 isDefault ? null : items[i * stride].scopeUri.deref, 309 isDefault, 310 i, 311 expected 312 ), changed, workspace.config); 313 } 314 } 315 else if (perWorkspaceConfigurationStore.length) 316 { 317 auto kv = perWorkspaceConfigurationStore.byKeyValue.front; 318 if (perWorkspaceConfigurationStore.length > 1) 319 error("Client does not support configuration request, only applying config for workspace ", kv.key); 320 auto changed = kv.value.replace(configuration); 321 emitExtensionEvent!onConfigChanged( 322 ConfigWorkspace.exactlyOne(kv.key), changed, kv.value.config); 323 } 324 else 325 { 326 info("initializing config for global fallback workspace"); 327 assert(globalConfiguration, __FUNCTION__ ~ " called while globalConfiguration wasn't initialized"); 328 329 auto changed = globalConfiguration.replace(configuration); 330 emitExtensionEvent!onConfigChanged( 331 ConfigWorkspace.unnamedWorkspace, changed, globalConfiguration.config); 332 } 333 334 emitExtensionEvent!onConfigFinished(perWorkspaceConfigurationStore.length); 335 } 336 337 bool syncConfiguration(string workspaceUri, size_t index = 0, size_t numConfigs = 0, bool addNew = false) 338 { 339 if (_hasConfigurationCapability) 340 { 341 if (addNew) 342 perWorkspaceConfigurationStore[workspaceUri] = TConfigHolder.init; 343 344 auto proj = workspaceUri in perWorkspaceConfigurationStore; 345 if (!proj && workspaceUri.length) 346 { 347 error("Did not find workspace ", workspaceUri, " when syncing config?"); 348 return false; 349 } 350 else if (!proj) 351 { 352 assert(globalConfiguration, __FUNCTION__ ~ " called while globalConfiguration wasn't initialized"); 353 proj = globalConfiguration; 354 } 355 356 ConfigurationItem[] items; 357 if (workspaceUri.length) 358 items = getConfigurationItems(workspaceUri); 359 else 360 items = getGlobalConfigurationItems(); 361 362 trace("Sending workspace/configuration request for ", workspaceUri); 363 auto res = rpc.sendRequest("workspace/configuration", ConfigurationParams(items)); 364 365 string[] settings = validateConfigurationItemsResponse(res); 366 if (!settings.length) 367 return false; 368 369 string[] changed = proj.replaceAllSectionsJson(settings); 370 emitExtensionEvent!onConfigChanged( 371 ConfigWorkspace(workspaceUri, workspaceUri.length == 0, index, numConfigs), 372 changed, proj.config); 373 return true; 374 } 375 else 376 return false; 377 } 378 379 private ConfigurationItem[] getGlobalConfigurationItems() 380 { 381 ConfigurationItem[] items = new ConfigurationItem[TConfig.tupleof.length]; 382 foreach (i, section; TConfig.init.tupleof) 383 items[i] = ConfigurationItem(Optional!string.init, opt(TConfig.tupleof[i].stringof)); 384 return items; 385 } 386 387 private ConfigurationItem[] getConfigurationItems(DocumentUri uri) 388 { 389 ConfigurationItem[] items = new ConfigurationItem[TConfig.tupleof.length]; 390 foreach (i, section; TConfig.init.tupleof) 391 items[i] = ConfigurationItem(opt(uri), opt(TConfig.tupleof[i].stringof)); 392 return items; 393 } 394 395 private string[] validateConfigurationItemsResponse(scope return ref ResponseMessageRaw res, 396 size_t expected = size_t.max) 397 { 398 if (!res.resultJson.looksLikeJsonArray) 399 { 400 error("Got invalid configuration response from language client. (not an array)"); 401 trace("Response: ", res); 402 return null; 403 } 404 405 string[] settings; 406 int i; 407 res.resultJson.visitJsonArray!(v => i++); 408 settings.length = i; 409 i = 0; 410 res.resultJson.visitJsonArray!(v => settings[i++] = v); 411 412 if (settings.length % TConfig.tupleof.length != 0) 413 { 414 error("Got invalid configuration response from language client. (invalid length)"); 415 trace("Response: ", res); 416 return null; 417 } 418 if (expected != size_t.max) 419 { 420 auto total = settings.length / TConfig.tupleof.length; 421 if (total > expected) 422 { 423 warning("Loading different amount of workspaces than requested: requested ", 424 expected, " but loading ", total); 425 } 426 else if (total < expected) 427 { 428 error("Didn't get all configs we asked for: requested ", expected, " but loading ", total); 429 return null; 430 } 431 } 432 return settings; 433 } 434 }