1 module served.types; 2 3 public import served.protocol; 4 public import served.protoext; 5 public import served.textdocumentmanager; 6 7 import std.algorithm; 8 import std.array; 9 import std.conv; 10 import std.json; 11 import std.meta; 12 import std.path; 13 import std.range; 14 15 import workspaced.api; 16 17 import served.jsonrpc; 18 19 struct protocolMethod 20 { 21 string method; 22 } 23 24 struct protocolNotification 25 { 26 string method; 27 } 28 29 enum IncludedFeatures = ["d", "workspaces"]; 30 31 TextDocumentManager documents; 32 33 string[] compare(string prefix, T)(ref T a, ref T b) 34 { 35 string[] changed; 36 foreach (member; __traits(allMembers, T)) 37 if (__traits(getMember, a, member) != __traits(getMember, b, member)) 38 changed ~= prefix ~ member; 39 return changed; 40 } 41 42 alias configurationTypes = AliasSeq!(Configuration.D, Configuration.DFmt, 43 Configuration.Editor, Configuration.Git); 44 static immutable string[] configurationSections = ["d", "dfmt", "editor", "git"]; 45 46 struct Configuration 47 { 48 struct D 49 { 50 JSONValue stdlibPath = JSONValue("auto"); 51 string dcdClientPath = "dcd-client", dcdServerPath = "dcd-server"; 52 string dscannerPath = "dscanner"; 53 string dfmtPath = "dfmt"; 54 string dubPath = "dub"; 55 string dmdPath = "dmd"; 56 bool enableLinting = true; 57 bool enableSDLLinting = true; 58 bool enableStaticLinting = true; 59 bool enableDubLinting = true; 60 bool enableAutoComplete = true; 61 bool enableFormatting = true; 62 bool enableDMDImportTiming = false; 63 bool neverUseDub = false; 64 string[] projectImportPaths; 65 string dubConfiguration; 66 string dubArchType; 67 string dubBuildType; 68 string dubCompiler; 69 bool overrideDfmtEditorconfig = true; 70 bool aggressiveUpdate = true; 71 bool argumentSnippets = false; 72 bool scanAllFolders = true; 73 string[] disabledRootGlobs; 74 string[] extraRoots; 75 } 76 77 struct DFmt 78 { 79 bool alignSwitchStatements = true; 80 string braceStyle = "allman"; 81 bool outdentAttributes = true; 82 bool spaceAfterCast = true; 83 bool splitOperatorAtLineEnd = false; 84 bool selectiveImportSpace = true; 85 bool compactLabeledStatements = true; 86 string templateConstraintStyle = "conditional_newline_indent"; 87 } 88 89 struct Editor 90 { 91 int[] rulers; 92 } 93 94 struct Git 95 { 96 string path = "git"; 97 } 98 99 D d; 100 DFmt dfmt; 101 Editor editor; 102 Git git; 103 104 string[] stdlibPath() 105 { 106 auto p = d.stdlibPath; 107 if (p.type == JSON_TYPE.ARRAY) 108 return p.array.map!"a.str".array; 109 else 110 { 111 if (p.type != JSON_TYPE.STRING || p.str == "auto") 112 { 113 version (Windows) 114 return [`C:\D\dmd2\src\druntime\import`, `C:\D\dmd2\src\phobos`]; 115 else version (OSX) 116 return [`/Library/D/dmd/src/druntime/import`, `/Library/D/dmd/src/phobos`]; 117 else version (Posix) 118 return [`/usr/include/dmd/druntime/import`, `/usr/include/dmd/phobos`]; 119 else 120 { 121 pragma(msg, 122 __FILE__ ~ "(" ~ __LINE__ 123 ~ "): Note: Unknown target OS. Please add default D stdlib path"); 124 return []; 125 } 126 } 127 else 128 return [p.str]; 129 } 130 } 131 132 string[] replace(Configuration newConfig) 133 { 134 string[] ret; 135 ret ~= replaceSection!"d"(newConfig.d); 136 ret ~= replaceSection!"dfmt"(newConfig.dfmt); 137 ret ~= replaceSection!"editor"(newConfig.editor); 138 ret ~= replaceSection!"git"(newConfig.git); 139 return ret; 140 } 141 142 string[] replaceSection(string section : "d")(D newD) 143 { 144 auto ret = compare!"d."(d, newD); 145 d = newD; 146 return ret; 147 } 148 149 string[] replaceSection(string section : "dfmt")(DFmt newDfmt) 150 { 151 auto ret = compare!"dfmt."(dfmt, newDfmt); 152 dfmt = newDfmt; 153 return ret; 154 } 155 156 string[] replaceSection(string section : "editor")(Editor newEditor) 157 { 158 auto ret = compare!"editor."(editor, newEditor); 159 editor = newEditor; 160 return ret; 161 } 162 163 string[] replaceSection(string section : "git")(Git newGit) 164 { 165 auto ret = compare!"git."(git, newGit); 166 git = newGit; 167 return ret; 168 } 169 } 170 171 struct Workspace 172 { 173 WorkspaceFolder folder; 174 Configuration config; 175 bool initialized, disabled; 176 string[string] startupErrorNotifications; 177 bool selected; 178 179 void startupError(string folder, string error) 180 { 181 if (folder !in startupErrorNotifications) 182 startupErrorNotifications[folder] = ""; 183 string errors = startupErrorNotifications[folder]; 184 if (errors.length) 185 { 186 if (errors.endsWith(".", "\n\n")) 187 startupErrorNotifications[folder] ~= " " ~ error; 188 else if (errors.endsWith(". ")) 189 startupErrorNotifications[folder] ~= error; 190 else 191 startupErrorNotifications[folder] ~= "\n\n" ~ error; 192 } 193 else 194 startupErrorNotifications[folder] = error; 195 } 196 } 197 198 deprecated string workspaceRoot() @property 199 { 200 return firstWorkspaceRootUri.uriToFile; 201 } 202 203 string selectedWorkspaceRoot() @property 204 { 205 foreach (ref workspace; workspaces) 206 if (workspace.selected) 207 return workspace.folder.uri.uriToFile; 208 return firstWorkspaceRootUri.uriToFile; 209 } 210 211 string firstWorkspaceRootUri() @property 212 { 213 return workspaces.length ? workspaces[0].folder.uri : ""; 214 } 215 216 Workspace fallbackWorkspace; 217 Workspace[] workspaces; 218 ClientCapabilities capabilities; 219 RPCProcessor rpc; 220 221 size_t workspaceIndex(string uri) 222 { 223 if (!uri.startsWith("file://")) 224 throw new Exception("Passed a non file:// uri to workspace(uri): '" ~ uri ~ "'"); 225 size_t best = size_t.max; 226 size_t bestLength = 0; 227 foreach (i, ref workspace; workspaces) 228 { 229 if (workspace.folder.uri.length > bestLength 230 && uri.startsWith(workspace.folder.uri) && !workspace.disabled) 231 { 232 best = i; 233 bestLength = workspace.folder.uri.length; 234 if (uri.length == workspace.folder.uri.length) // startsWith + same length => same string 235 return i; 236 } 237 } 238 return best; 239 } 240 241 ref Workspace handleThings(ref Workspace workspace, string uri, bool userExecuted, 242 string file = __FILE__, size_t line = __LINE__) 243 { 244 if (userExecuted) 245 { 246 string f = uri.uriToFile; 247 foreach (key, error; workspace.startupErrorNotifications) 248 { 249 if (f.startsWith(key)) 250 { 251 //dfmt off 252 debug 253 rpc.window.showErrorMessage( 254 error ~ "\n\nFile: " ~ file ~ ":" ~ line.to!string); 255 else 256 rpc.window.showErrorMessage(error); 257 //dfmt on 258 workspace.startupErrorNotifications.remove(key); 259 } 260 } 261 262 bool notifyChange, changedOne; 263 foreach (ref w; workspaces) 264 { 265 if (w.selected) 266 { 267 if (w.folder.uri != workspace.folder.uri) 268 notifyChange = true; 269 changedOne = true; 270 w.selected = false; 271 } 272 } 273 workspace.selected = true; 274 if (notifyChange || !changedOne) 275 rpc.notifyMethod("coded/changedSelectedWorkspace", workspace.folder); 276 } 277 return workspace; 278 } 279 280 ref Workspace workspace(string uri, bool userExecuted = true, 281 string file = __FILE__, size_t line = __LINE__) 282 { 283 auto best = workspaceIndex(uri); 284 if (best == size_t.max) 285 return bestWorkspaceByDependency(uri).handleThings(uri, userExecuted, file, line); 286 return workspaces[best].handleThings(uri, userExecuted, file, line); 287 } 288 289 ref Workspace bestWorkspaceByDependency(string uri) 290 { 291 size_t best = size_t.max; 292 size_t bestLength; 293 foreach (i, ref workspace; workspaces) 294 { 295 auto inst = backend.getInstance(workspace.folder.uri.uriToFile); 296 if (!inst) 297 continue; 298 foreach (folder; chain(inst.importPaths, inst.importFiles, inst.stringImportPaths)) 299 { 300 string folderUri = folder.uriFromFile; 301 if (folderUri.length > bestLength && uri.startsWith(folderUri)) 302 { 303 best = i; 304 bestLength = folderUri.length; 305 if (uri.length == folderUri.length) // startsWith + same length => same string 306 return workspace; 307 } 308 } 309 } 310 if (best == size_t.max) 311 return fallbackWorkspace; 312 return workspaces[best]; 313 } 314 315 string workspaceRootFor(string uri) 316 { 317 return workspace(uri).folder.uri.uriToFile; 318 } 319 320 bool hasWorkspace(string uri) 321 { 322 foreach (i, ref workspace; workspaces) 323 if (uri.startsWith(workspace.folder.uri)) 324 return true; 325 return false; 326 } 327 328 ref Configuration config(string uri, bool userExecuted = true, 329 string file = __FILE__, size_t line = __LINE__) 330 { 331 return workspace(uri, userExecuted, file, line).config; 332 } 333 334 ref Configuration firstConfig() 335 { 336 if (!workspaces.length) 337 throw new Exception("No config available"); 338 return workspaces[0].config; 339 } 340 341 DocumentUri uriFromFile(string file) 342 { 343 import std.uri : encodeComponent; 344 345 if (!isAbsolute(file)) 346 throw new Exception("Tried to pass relative path '" ~ file ~ "' to uriFromFile"); 347 file = file.buildNormalizedPath.replace("\\", "/"); 348 if (file.length == 0) 349 return ""; 350 if (file[0] != '/') 351 file = '/' ~ file; // always triple slash at start but never quad slash 352 if (file.length >= 2 && file[0 .. 2] == "//") // Shares (\\share\bob) are different somehow 353 file = file[2 .. $]; 354 return "file://" ~ file.encodeComponent.replace("%2F", "/"); 355 } 356 357 string uriToFile(DocumentUri uri) 358 { 359 import std.uri : decodeComponent; 360 import std.string : startsWith; 361 362 if (uri.startsWith("file://")) 363 { 364 string ret = uri["file://".length .. $].decodeComponent; 365 if (ret.length >= 3 && ret[0] == '/' && ret[2] == ':') 366 return ret[1 .. $].replace("/", "\\"); 367 else if (ret.length >= 1 && ret[0] != '/') 368 return "\\\\" ~ ret.replace("/", "\\"); 369 return ret; 370 } 371 else 372 return null; 373 } 374 375 @system unittest 376 { 377 void testUri(string a, string b) 378 { 379 void assertEqual(A, B)(A a, B b) 380 { 381 import std.conv : to; 382 383 assert(a == b, a.to!string ~ " is not equal to " ~ b.to!string); 384 } 385 386 assertEqual(a.uriFromFile, b); 387 assertEqual(a, b.uriToFile); 388 assertEqual(a.uriFromFile.uriToFile, a); 389 } 390 391 testUri(`/home/pi/.bashrc`, `file:///home/pi/.bashrc`); 392 // taken from vscode-uri 393 testUri(`c:\test with %\path`, `file:///c%3A/test%20with%20%25/path`); 394 testUri(`c:\test with %25\path`, `file:///c%3A/test%20with%20%2525/path`); 395 testUri(`c:\test with %25\c#code`, `file:///c%3A/test%20with%20%2525/c%23code`); 396 testUri(`\\shäres\path\c#\plugin.json`, `file://sh%C3%A4res/path/c%23/plugin.json`); 397 testUri(`\\localhost\c$\GitDevelopment\express`, `file://localhost/c%24/GitDevelopment/express`); 398 } 399 400 DocumentUri uri(string scheme, string authority, string path, string query, string fragment) 401 { 402 return scheme ~ "://" ~ (authority.length ? authority : "") ~ (path.length ? path 403 : "/") ~ (query.length ? "?" ~ query : "") ~ (fragment.length ? "#" ~ fragment : ""); 404 } 405 406 int toInt(JSONValue value) 407 { 408 if (value.type == JSON_TYPE.UINTEGER) 409 return cast(int) value.uinteger; 410 else 411 return cast(int) value.integer; 412 } 413 414 WorkspaceD backend; 415 416 /// Quick function to check if a package.json can not not be a dub package file. 417 /// Returns: false if fields are used which aren't usually used in dub but in nodejs. 418 bool seemsLikeDubJson(JSONValue packageJson) 419 { 420 if ("main" in packageJson || "engines" in packageJson || "publisher" in packageJson 421 || "private" in packageJson || "devDependencies" in packageJson) 422 return false; 423 if ("name" !in packageJson) 424 return false; 425 return true; 426 }