1 module served.commands.dub;
2 
3 import served.extension;
4 import served.types;
5 import served.utils.progress;
6 import served.utils.translate;
7 
8 import workspaced.api;
9 import workspaced.coms;
10 
11 import painlessjson : toJSON;
12 
13 import core.time;
14 
15 import std.algorithm : among, canFind, count, endsWith, map, remove, startsWith;
16 import std.array : array, replace;
17 import std.experimental.logger;
18 import std.json : JSONValue;
19 import std.path : baseName, buildPath, dirName, setExtension;
20 import std.regex : regex, replaceFirst;
21 import std.string : indexOf, join, KeepTerminator, splitLines, strip, stripLeft, stripRight;
22 
23 import fs = std.file;
24 import io = std.stdio;
25 
26 @protocolMethod("served/listConfigurations")
27 string[] listConfigurations()
28 {
29 	if (!activeInstance || !activeInstance.has!DubComponent)
30 		return null;
31 	return activeInstance.get!DubComponent.configurations;
32 }
33 
34 @protocolMethod("served/switchConfig")
35 bool switchConfig(string value)
36 {
37 	if (!activeInstance || !activeInstance.has!DubComponent)
38 		return false;
39 	return activeInstance.get!DubComponent.setConfiguration(value);
40 }
41 
42 @protocolMethod("served/getConfig")
43 string getConfig()
44 {
45 	if (!activeInstance || !activeInstance.has!DubComponent)
46 		return null;
47 	return activeInstance.get!DubComponent.configuration;
48 }
49 
50 @protocolMethod("served/listArchTypes")
51 string[] listArchTypes()
52 {
53 	if (!activeInstance || !activeInstance.has!DubComponent)
54 		return null;
55 	return activeInstance.get!DubComponent.archTypes;
56 }
57 
58 @protocolMethod("served/switchArchType")
59 bool switchArchType(string value)
60 {
61 	if (!activeInstance || !activeInstance.has!DubComponent)
62 		return false;
63 	return activeInstance.get!DubComponent.setArchType(JSONValue([
64 				"arch-type": JSONValue(value)
65 			]));
66 }
67 
68 @protocolMethod("served/getArchType")
69 string getArchType()
70 {
71 	if (!activeInstance || !activeInstance.has!DubComponent)
72 		return null;
73 	return activeInstance.get!DubComponent.archType;
74 }
75 
76 @protocolMethod("served/listBuildTypes")
77 string[] listBuildTypes()
78 {
79 	if (!activeInstance || !activeInstance.has!DubComponent)
80 		return null;
81 	return activeInstance.get!DubComponent.buildTypes;
82 }
83 
84 @protocolMethod("served/switchBuildType")
85 bool switchBuildType(string value)
86 {
87 	if (!activeInstance || !activeInstance.has!DubComponent)
88 		return false;
89 	return activeInstance.get!DubComponent.setBuildType(JSONValue([
90 				"build-type": JSONValue(value)
91 			]));
92 }
93 
94 @protocolMethod("served/getBuildType")
95 string getBuildType()
96 {
97 	if (!activeInstance || !activeInstance.has!DubComponent)
98 		return null;
99 	return activeInstance.get!DubComponent.buildType;
100 }
101 
102 @protocolMethod("served/getCompiler")
103 string getCompiler()
104 {
105 	if (!activeInstance || !activeInstance.has!DubComponent)
106 		return null;
107 	return activeInstance.get!DubComponent.compiler;
108 }
109 
110 @protocolMethod("served/switchCompiler")
111 bool switchCompiler(string value)
112 {
113 	if (!activeInstance || !activeInstance.has!DubComponent)
114 		return false;
115 	return activeInstance.get!DubComponent.setCompiler(value);
116 }
117 
118 /// Returns: at least
119 /// ```
120 /// {
121 ///     "packagePath": string,
122 ///     "packageName": string,
123 ///     "recipePath": string,
124 ///     "targetPath": string,
125 ///     "targetName": string,
126 ///     "targetType": string,
127 ///     "workingDirectory": string,
128 ///     "mainSourceFile": string,
129 ///
130 ///     "dflags": string[],
131 ///     "lflags": string[],
132 ///     "libs": string[],
133 ///     "linkerFiles": string[],
134 ///     "sourceFiles": string[],
135 ///     "copyFiles": string[],
136 ///     "versions": string[],
137 ///     "debugVersions": string[],
138 ///     "importPaths": string[],
139 ///     "stringImportPaths": string[],
140 ///     "importFiles": string[],
141 ///     "stringImportFiles": string[],
142 ///     "preGenerateCommands": string[],
143 ///     "postGenerateCommands": string[],
144 ///     "preBuildCommands": string[],
145 ///     "postBuildCommands": string[],
146 ///     "preRunCommands": string[],
147 ///     "postRunCommands": string[],
148 ///     "buildOptions": string[],
149 ///     "buildRequirements": string[],
150 /// }
151 /// ```
152 @protocolMethod("served/getActiveDubConfig")
153 JSONValue getActiveDubConfig()
154 {
155 	if (!activeInstance || !activeInstance.has!DubComponent)
156 		return JSONValue.init;
157 	auto ret = activeInstance.get!DubComponent.rootPackageBuildSettings();
158 	static assert(is(typeof(ret.packagePath) : string), "API guarantee broken");
159 	static assert(is(typeof(ret.packageName) : string), "API guarantee broken");
160 	static assert(is(typeof(ret.recipePath) : string), "API guarantee broken");
161 	static assert(is(typeof(ret.targetPath) : string), "API guarantee broken");
162 	static assert(is(typeof(ret.targetName) : string), "API guarantee broken");
163 	static assert(is(typeof(ret.targetType) : string), "API guarantee broken");
164 	static assert(is(typeof(ret.workingDirectory) : string), "API guarantee broken");
165 	static assert(is(typeof(ret.mainSourceFile) : string), "API guarantee broken");
166 	static assert(is(typeof(ret.dflags) : string[]), "API guarantee broken");
167 	static assert(is(typeof(ret.lflags) : string[]), "API guarantee broken");
168 	static assert(is(typeof(ret.libs) : string[]), "API guarantee broken");
169 	static assert(is(typeof(ret.linkerFiles) : string[]), "API guarantee broken");
170 	static assert(is(typeof(ret.sourceFiles) : string[]), "API guarantee broken");
171 	static assert(is(typeof(ret.copyFiles) : string[]), "API guarantee broken");
172 	static assert(is(typeof(ret.versions) : string[]), "API guarantee broken");
173 	static assert(is(typeof(ret.debugVersions) : string[]), "API guarantee broken");
174 	static assert(is(typeof(ret.importPaths) : string[]), "API guarantee broken");
175 	static assert(is(typeof(ret.stringImportPaths) : string[]), "API guarantee broken");
176 	static assert(is(typeof(ret.importFiles) : string[]), "API guarantee broken");
177 	static assert(is(typeof(ret.stringImportFiles) : string[]), "API guarantee broken");
178 	static assert(is(typeof(ret.preGenerateCommands) : string[]), "API guarantee broken");
179 	static assert(is(typeof(ret.postGenerateCommands) : string[]), "API guarantee broken");
180 	static assert(is(typeof(ret.preBuildCommands) : string[]), "API guarantee broken");
181 	static assert(is(typeof(ret.postBuildCommands) : string[]), "API guarantee broken");
182 	static assert(is(typeof(ret.preRunCommands) : string[]), "API guarantee broken");
183 	static assert(is(typeof(ret.postRunCommands) : string[]), "API guarantee broken");
184 	static assert(is(typeof(ret.buildOptions) : string[]), "API guarantee broken");
185 	static assert(is(typeof(ret.buildRequirements) : string[]), "API guarantee broken");
186 	return ret.toJSON;
187 }
188 
189 @protocolMethod("served/addImport")
190 auto addImport(AddImportParams params)
191 {
192 	auto document = documents[params.textDocument.uri];
193 	return backend.get!ImporterComponent.add(params.name.idup, document.rawText,
194 			params.location, params.insertOutermost);
195 }
196 
197 @protocolMethod("served/updateImports")
198 bool updateImports(UpdateImportsParams params)
199 {
200 	auto instance = activeInstance;
201 	bool success;
202 
203 	reportProgress(params.reportProgress, ProgressType.dubReload, 0, 5, instance.cwd.uriFromFile);
204 
205 	if (instance.has!DubComponent)
206 	{
207 		success = instance.get!DubComponent.update.getYield;
208 		if (success)
209 			rpc.notifyMethod("coded/updateDubTree");
210 	}
211 	reportProgress(params.reportProgress, ProgressType.importReload, 4, 5, instance.cwd.uriFromFile);
212 	if (instance.has!DCDComponent)
213 		instance.get!DCDComponent.refreshImports();
214 	reportProgress(params.reportProgress, ProgressType.importReload, 5, 5, instance.cwd.uriFromFile);
215 	return success;
216 }
217 
218 @protocolNotification("textDocument/didSave")
219 void onDidSaveDubRecipe(DidSaveTextDocumentParams params)
220 {
221 	auto fileName = params.textDocument.uri.uriToFile.baseName;
222 	if (!fileName.among!("dub.json", "dub.sdl"))
223 		return;
224 
225 	auto workspaceUri = workspace(params.textDocument.uri).folder.uri;
226 	auto workspaceRoot = workspaceUri.uriToFile;
227 
228 	info("Updating dependencies");
229 	reportProgress(ProgressType.importUpgrades, 0, 10, workspaceUri);
230 	if (!backend.has!DubComponent(workspaceRoot))
231 	{
232 		Exception err;
233 		const success = backend.attach(backend.getInstance(workspaceRoot), "dub", err);
234 		if (!success)
235 		{
236 			rpc.window.showMessage(MessageType.error, translate!"d.ext.dubUpgradeFail");
237 			error(err);
238 			reportProgress(ProgressType.importUpgrades, 10, 10, workspaceUri);
239 			return;
240 		}
241 	}
242 	else
243 	{
244 		if (backend.get!DubComponent(workspaceRoot).isRunning)
245 		{
246 			string syntaxCheck = backend.get!DubComponent(workspaceRoot)
247 				.validateRecipeSyntaxOnFileSystem();
248 
249 			if (syntaxCheck.length)
250 			{
251 				rpc.window.showMessage(MessageType.error,
252 						translate!"d.ext.dubInvalidRecipeSyntax"(syntaxCheck));
253 				error(syntaxCheck);
254 				reportProgress(ProgressType.importUpgrades, 10, 10, workspaceUri);
255 				return;
256 			}
257 
258 			rpc.window.runOrMessage(backend.get!DubComponent(workspaceRoot)
259 					.upgrade(), MessageType.warning, translate!"d.ext.dubUpgradeFail");
260 		}
261 		else
262 		{
263 			rpc.window.showMessage(MessageType.error, translate!"d.ext.dubUpgradeFail");
264 			reportProgress(ProgressType.importUpgrades, 10, 10, workspaceUri);
265 			return;
266 		}
267 	}
268 	reportProgress(ProgressType.importUpgrades, 6, 10, workspaceUri);
269 
270 	setTimeout({
271 		const successfulUpdate = rpc.window.runOrMessage(backend.get!DubComponent(workspaceRoot)
272 			.updateImportPaths(true), MessageType.warning, translate!"d.ext.dubImportFail");
273 		if (successfulUpdate)
274 		{
275 			rpc.window.runOrMessage(updateImports(UpdateImportsParams(false)),
276 				MessageType.warning, translate!"d.ext.dubImportFail");
277 		}
278 		else
279 		{
280 			try
281 			{
282 				updateImports(UpdateImportsParams(false));
283 			}
284 			catch (Exception e)
285 			{
286 				errorf("Failed updating imports: %s", e);
287 			}
288 		}
289 		reportProgress(ProgressType.importUpgrades, 10, 10, workspaceUri);
290 	}, 200.msecs);
291 
292 	setTimeout({
293 		if (!backend.get!DubComponent(workspaceRoot).isRunning)
294 		{
295 			Exception err;
296 			if (backend.attach(backend.getInstance(workspaceRoot), "dub", err))
297 			{
298 				rpc.window.runOrMessage(backend.get!DubComponent(workspaceRoot)
299 					.updateImportPaths(true), MessageType.warning,
300 					translate!"d.ext.dubRecipeMaybeBroken");
301 				error(err);
302 			}
303 		}
304 	}, 500.msecs);
305 	rpc.notifyMethod("coded/updateDubTree");
306 }
307 
308 @protocolMethod("served/listDependencies")
309 DubDependency[] listDependencies(string packageName)
310 {
311 	auto instance = activeInstance;
312 	DubDependency[] ret;
313 	if (!instance.has!DubComponent)
314 		return ret;
315 
316 	auto allDeps = instance.get!DubComponent.dependencies;
317 	if (!packageName.length)
318 	{
319 		auto deps = instance.get!DubComponent.rootDependencies;
320 		foreach (dep; deps)
321 		{
322 			DubDependency r;
323 			r.name = dep;
324 			r.root = true;
325 			foreach (other; allDeps)
326 				if (other.name == dep)
327 				{
328 					r.version_ = other.ver;
329 					r.path = other.path;
330 					r.description = other.description;
331 					r.homepage = other.homepage;
332 					r.authors = other.authors;
333 					r.copyright = other.copyright;
334 					r.license = other.license;
335 					r.subPackages = other.subPackages.map!"a.name".array;
336 					r.hasDependencies = other.dependencies.length > 0;
337 					break;
338 				}
339 			ret ~= r;
340 		}
341 	}
342 	else
343 	{
344 		string[string] aa;
345 		foreach (other; allDeps)
346 			if (other.name == packageName)
347 			{
348 				aa = other.dependencies;
349 				break;
350 			}
351 		foreach (name, ver; aa)
352 		{
353 			DubDependency r;
354 			r.name = name;
355 			r.version_ = ver;
356 			foreach (other; allDeps)
357 				if (other.name == name)
358 				{
359 					r.path = other.path;
360 					r.description = other.description;
361 					r.homepage = other.homepage;
362 					r.authors = other.authors;
363 					r.copyright = other.copyright;
364 					r.license = other.license;
365 					r.subPackages = other.subPackages.map!"a.name".array;
366 					r.hasDependencies = other.dependencies.length > 0;
367 					break;
368 				}
369 			ret ~= r;
370 		}
371 	}
372 	return ret;
373 }
374 
375 private string[] fixEmptyArgs(string[] args)
376 {
377 	return args.remove!(a => a.endsWith('='));
378 }
379 
380 __gshared bool useBuildTaskDollarCurrent = false;
381 @protocolMethod("served/buildTasks")
382 Task[] provideBuildTasks()
383 {
384 	Task[] ret;
385 	foreach (instance; backend.instances)
386 	{
387 		if (!instance.has!DubComponent)
388 			continue;
389 		auto dub = instance.get!DubComponent;
390 		auto workspace = .workspace(instance.cwd.uriFromFile, false);
391 		info("Found dub package to build at ", dub.recipePath);
392 
393 		JSONValue dollarMagicValue;
394 		if (useBuildTaskDollarCurrent)
395 			dollarMagicValue = JSONValue("$current");
396 
397 		JSONValue currentValue(string prop)()
398 		{
399 			if (useBuildTaskDollarCurrent)
400 				return JSONValue("$current");
401 			else
402 				return JSONValue(__traits(getMember, dub, prop));
403 		}
404 
405 		auto cwd = JSONValue(dub.recipePath.dirName.replace(workspace.folder.uri.uriToFile, "${workspaceFolder}"));
406 		{
407 			Task t;
408 			t.source = "dub";
409 			t.definition = JSONValue([
410 					"type": JSONValue("dub"),
411 					"run": JSONValue(true),
412 					"compiler": currentValue!"compiler",
413 					"archType": currentValue!"archType",
414 					"buildType": currentValue!"buildType",
415 					"configuration": currentValue!"configuration",
416 					"cwd": cwd
417 					]);
418 			t.group = Task.Group.build;
419 			t.exec = [
420 				workspace.config.d.dubPath.userPath, "run", "--compiler=" ~ dub.compiler,
421 				"-a=" ~ dub.archType, "-b=" ~ dub.buildType, "-c=" ~ dub.configuration
422 			].fixEmptyArgs;
423 			t.scope_ = workspace.folder.uri;
424 			t.name = "Run " ~ dub.name;
425 			ret ~= t;
426 		}
427 		{
428 			Task t;
429 			t.source = "dub";
430 			t.definition = JSONValue([
431 					"type": JSONValue("dub"),
432 					"test": JSONValue(true),
433 					"compiler": currentValue!"compiler",
434 					"archType": currentValue!"archType",
435 					"buildType": currentValue!"buildType",
436 					"configuration": currentValue!"configuration",
437 					"cwd": cwd
438 					]);
439 			t.group = Task.Group.test;
440 			t.exec = [
441 				workspace.config.d.dubPath.userPath, "test", "--compiler=" ~ dub.compiler,
442 				"-a=" ~ dub.archType, "-b=" ~ dub.buildType, "-c=" ~ dub.configuration
443 			].fixEmptyArgs;
444 			t.scope_ = workspace.folder.uri;
445 			t.name = "Test " ~ dub.name;
446 			ret ~= t;
447 		}
448 		{
449 			Task t;
450 			t.source = "dub";
451 			t.definition = JSONValue([
452 					"type": JSONValue("dub"),
453 					"run": JSONValue(false),
454 					"compiler": currentValue!"compiler",
455 					"archType": currentValue!"archType",
456 					"buildType": currentValue!"buildType",
457 					"configuration": currentValue!"configuration",
458 					"cwd": cwd
459 					]);
460 			t.group = Task.Group.build;
461 			t.exec = [
462 				workspace.config.d.dubPath.userPath, "build", "--compiler=" ~ dub.compiler,
463 				"-a=" ~ dub.archType, "-b=" ~ dub.buildType, "-c=" ~ dub.configuration
464 			].fixEmptyArgs;
465 			t.scope_ = workspace.folder.uri;
466 			t.name = "Build " ~ dub.name;
467 			ret ~= t;
468 		}
469 		{
470 			Task t;
471 			t.source = "dub";
472 			t.definition = JSONValue([
473 					"type": JSONValue("dub"),
474 					"run": JSONValue(false),
475 					"force": JSONValue(true),
476 					"compiler": currentValue!"compiler",
477 					"archType": currentValue!"archType",
478 					"buildType": currentValue!"buildType",
479 					"configuration": currentValue!"configuration",
480 					"cwd": cwd
481 					]);
482 			t.group = Task.Group.rebuild;
483 			t.exec = [
484 				workspace.config.d.dubPath.userPath, "build", "--force",
485 				"--compiler=" ~ dub.compiler, "-a=" ~ dub.archType,
486 				"-b=" ~ dub.buildType, "-c=" ~ dub.configuration
487 			].fixEmptyArgs;
488 			t.scope_ = workspace.folder.uri;
489 			t.name = "Rebuild " ~ dub.name;
490 			ret ~= t;
491 		}
492 	}
493 	return ret;
494 }
495 
496 // === Protocol Notifications starting here ===
497 
498 @protocolNotification("served/convertDubFormat")
499 void convertDubFormat(DubConvertRequest req)
500 {
501 	import std.process : execute, Config;
502 
503 	auto file = req.textDocument.uri.uriToFile;
504 	if (!fs.exists(file))
505 	{
506 		error("Specified file does not exist");
507 		return;
508 	}
509 
510 	if (!file.baseName.among!("dub.json", "dub.sdl", "package.json"))
511 	{
512 		rpc.window.showErrorMessage(translate!"d.dub.notRecipeFile");
513 		return;
514 	}
515 
516 	auto document = documents[req.textDocument.uri];
517 
518 	auto result = execute([
519 			workspace(req.textDocument.uri).config.d.dubPath.userPath, "convert",
520 			"-f", req.newFormat, "-s"
521 			], null, Config.stderrPassThrough, 1024 * 1024 * 4, file.dirName);
522 
523 	if (result.status != 0)
524 	{
525 		rpc.window.showErrorMessage(translate!"d.dub.convertFailed");
526 		return;
527 	}
528 
529 	auto newUri = req.textDocument.uri.setExtension("." ~ req.newFormat);
530 
531 	WorkspaceEdit edit;
532 	auto edits = [
533 		TextEdit(TextRange(Position(0, 0), document.offsetToPosition(document.length)), result.output)
534 	];
535 
536 	if (capabilities.workspace.workspaceEdit.resourceOperations.canFind(ResourceOperationKind.rename))
537 	{
538 		edit.documentChanges = JSONValue([
539 				toJSON(RenameFile(req.textDocument.uri, newUri)),
540 				toJSON(TextDocumentEdit(VersionedTextDocumentIdentifier(newUri,
541 					document.version_), edits))
542 				]);
543 	}
544 	else
545 		edit.changes[req.textDocument.uri] = edits;
546 	rpc.sendMethod("workspace/applyEdit", ApplyWorkspaceEditParams(edit));
547 }
548 
549 @protocolNotification("served/installDependency")
550 void installDependency(InstallRequest req)
551 {
552 	auto instance = activeInstance;
553 	auto uri = instance.cwd.uriFromFile;
554 	reportProgress(ProgressType.importUpgrades, 0, 10, uri);
555 	injectDependency(instance, req);
556 	if (instance.has!DubComponent)
557 		instance.get!DubComponent.upgrade();
558 	reportProgress(ProgressType.dubReload, 7, 10, uri);
559 	updateImports(UpdateImportsParams(false));
560 	reportProgress(ProgressType.dubReload, 10, 10, uri);
561 }
562 
563 @protocolNotification("served/updateDependency")
564 void updateDependency(UpdateRequest req)
565 {
566 	auto instance = activeInstance;
567 	auto uri = instance.cwd.uriFromFile;
568 	reportProgress(ProgressType.importUpgrades, 0, 10, uri);
569 	if (changeDependency(instance, req))
570 	{
571 		if (instance.has!DubComponent)
572 			instance.get!DubComponent.upgrade();
573 		reportProgress(ProgressType.dubReload, 7, 10, uri);
574 		updateImports(UpdateImportsParams(false));
575 	}
576 	reportProgress(ProgressType.dubReload, 10, 10, uri);
577 }
578 
579 @protocolNotification("served/uninstallDependency")
580 void uninstallDependency(UninstallRequest req)
581 {
582 	auto instance = activeInstance;
583 	auto uri = instance.cwd.uriFromFile;
584 	reportProgress(ProgressType.importUpgrades, 0, 10, uri);
585 	// TODO: add workspace argument
586 	removeDependency(instance, req.name);
587 	if (instance.has!DubComponent)
588 		instance.get!DubComponent.upgrade();
589 	reportProgress(ProgressType.dubReload, 7, 10, uri);
590 	updateImports(UpdateImportsParams(false));
591 	reportProgress(ProgressType.dubReload, 10, 10, uri);
592 }
593 
594 void injectDependency(WorkspaceD.Instance instance, InstallRequest req)
595 {
596 	auto sdl = buildPath(instance.cwd, "dub.sdl");
597 	if (fs.exists(sdl))
598 	{
599 		int depth = 0;
600 		auto content = fs.readText(sdl).splitLines(KeepTerminator.yes);
601 		auto insertAt = content.length;
602 		bool gotLineEnding = false;
603 		string lineEnding = "\n";
604 		foreach (i, line; content)
605 		{
606 			if (!gotLineEnding && line.length >= 2)
607 			{
608 				lineEnding = line[$ - 2 .. $];
609 				if (lineEnding[0] != '\r')
610 					lineEnding = line[$ - 1 .. $];
611 				gotLineEnding = true;
612 			}
613 			if (depth == 0 && line.strip.startsWith("dependency "))
614 				insertAt = i + 1;
615 			depth += line.count('{') - line.count('}');
616 		}
617 		content = content[0 .. insertAt] ~ ((insertAt == content.length ? lineEnding
618 				: "") ~ "dependency \"" ~ req.name ~ "\" version=\"~>" ~ req.version_ ~ "\"" ~ lineEnding)
619 			~ content[insertAt .. $];
620 		fs.write(sdl, content.join());
621 	}
622 	else
623 	{
624 		auto json = buildPath(instance.cwd, "dub.json");
625 		if (!fs.exists(json))
626 			json = buildPath(instance.cwd, "package.json");
627 		if (!fs.exists(json))
628 			return;
629 		auto content = fs.readText(json).splitLines(KeepTerminator.yes);
630 		auto insertAt = content.length ? content.length - 1 : 0;
631 		string lineEnding = "\n";
632 		bool gotLineEnding = false;
633 		int depth = 0;
634 		bool insertNext;
635 		string indent;
636 		bool foundBlock;
637 		foreach (i, line; content)
638 		{
639 			if (!gotLineEnding && line.length >= 2)
640 			{
641 				lineEnding = line[$ - 2 .. $];
642 				if (lineEnding[0] != '\r')
643 					lineEnding = line[$ - 1 .. $];
644 				gotLineEnding = true;
645 			}
646 			if (insertNext)
647 			{
648 				indent = line[0 .. $ - line.stripLeft.length];
649 				insertAt = i + 1;
650 				break;
651 			}
652 			if (depth == 1 && line.strip.startsWith(`"dependencies":`))
653 			{
654 				foundBlock = true;
655 				if (line.strip.endsWith("{"))
656 				{
657 					indent = line[0 .. $ - line.stripLeft.length];
658 					insertAt = i + 1;
659 					break;
660 				}
661 				else
662 				{
663 					insertNext = true;
664 				}
665 			}
666 			depth += line.count('{') - line.count('}') + line.count('[') - line.count(']');
667 		}
668 		if (foundBlock)
669 		{
670 			content = content[0 .. insertAt] ~ (
671 					indent ~ indent ~ `"` ~ req.name ~ `": "~>` ~ req.version_ ~ `",` ~ lineEnding)
672 				~ content[insertAt .. $];
673 			fs.write(json, content.join());
674 		}
675 		else if (content.length)
676 		{
677 			if (content.length > 1)
678 				content[$ - 2] = content[$ - 2].stripRight;
679 			content = content[0 .. $ - 1] ~ (
680 					"," ~ lineEnding ~ `	"dependencies": {
681 		"` ~ req.name ~ `": "~>` ~ req.version_ ~ `"
682 	}` ~ lineEnding)
683 				~ content[$ - 1 .. $];
684 			fs.write(json, content.join());
685 		}
686 		else
687 		{
688 			content ~= `{
689 	"dependencies": {
690 		"` ~ req.name ~ `": "~>` ~ req.version_ ~ `"
691 	}
692 }`;
693 			fs.write(json, content.join());
694 		}
695 	}
696 }
697 
698 bool changeDependency(WorkspaceD.Instance instance, UpdateRequest req)
699 {
700 	auto sdl = buildPath(instance.cwd, "dub.sdl");
701 	if (fs.exists(sdl))
702 	{
703 		int depth = 0;
704 		auto content = fs.readText(sdl).splitLines(KeepTerminator.yes);
705 		size_t target = size_t.max;
706 		foreach (i, line; content)
707 		{
708 			if (depth == 0 && line.strip.startsWith("dependency ")
709 					&& line.strip["dependency".length .. $].strip.startsWith('"' ~ req.name ~ '"'))
710 			{
711 				target = i;
712 				break;
713 			}
714 			depth += line.count('{') - line.count('}');
715 		}
716 		if (target == size_t.max)
717 			return false;
718 		auto ver = content[target].indexOf("version");
719 		if (ver == -1)
720 			return false;
721 		auto quotStart = content[target].indexOf("\"", ver);
722 		if (quotStart == -1)
723 			return false;
724 		auto quotEnd = content[target].indexOf("\"", quotStart + 1);
725 		if (quotEnd == -1)
726 			return false;
727 		content[target] = content[target][0 .. quotStart] ~ '"' ~ req.version_ ~ '"'
728 			~ content[target][quotEnd .. $];
729 		fs.write(sdl, content.join());
730 		return true;
731 	}
732 	else
733 	{
734 		auto json = buildPath(instance.cwd, "dub.json");
735 		if (!fs.exists(json))
736 			json = buildPath(instance.cwd, "package.json");
737 		if (!fs.exists(json))
738 			return false;
739 		auto content = fs.readText(json);
740 		auto replaced = content.replaceFirst(regex(`("` ~ req.name ~ `"\s*:\s*)"[^"]*"`),
741 				`$1"` ~ req.version_ ~ `"`);
742 		if (content == replaced)
743 			return false;
744 		fs.write(json, replaced);
745 		return true;
746 	}
747 }
748 
749 bool removeDependency(WorkspaceD.Instance instance, string name)
750 {
751 	auto sdl = buildPath(instance.cwd, "dub.sdl");
752 	if (fs.exists(sdl))
753 	{
754 		int depth = 0;
755 		auto content = fs.readText(sdl).splitLines(KeepTerminator.yes);
756 		size_t target = size_t.max;
757 		foreach (i, line; content)
758 		{
759 			if (depth == 0 && line.strip.startsWith("dependency ")
760 					&& line.strip["dependency".length .. $].strip.startsWith('"' ~ name ~ '"'))
761 			{
762 				target = i;
763 				break;
764 			}
765 			depth += line.count('{') - line.count('}');
766 		}
767 		if (target == size_t.max)
768 			return false;
769 		fs.write(sdl, (content[0 .. target] ~ content[target + 1 .. $]).join());
770 		return true;
771 	}
772 	else
773 	{
774 		auto json = buildPath(instance.cwd, "dub.json");
775 		if (!fs.exists(json))
776 			json = buildPath(instance.cwd, "package.json");
777 		if (!fs.exists(json))
778 			return false;
779 		auto content = fs.readText(json);
780 		auto replaced = content.replaceFirst(regex(`"` ~ name ~ `"\s*:\s*"[^"]*"\s*,\s*`), "");
781 		if (content == replaced)
782 			replaced = content.replaceFirst(regex(`\s*,\s*"` ~ name ~ `"\s*:\s*"[^"]*"`), "");
783 		if (content == replaced)
784 			replaced = content.replaceFirst(regex(
785 					`"dependencies"\s*:\s*\{\s*"` ~ name ~ `"\s*:\s*"[^"]*"\s*\}\s*,\s*`), "");
786 		if (content == replaced)
787 			replaced = content.replaceFirst(regex(
788 					`\s*,\s*"dependencies"\s*:\s*\{\s*"` ~ name ~ `"\s*:\s*"[^"]*"\s*\}`), "");
789 		if (content == replaced)
790 			return false;
791 		fs.write(json, replaced);
792 		return true;
793 	}
794 }