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 @protocolMethod("served/buildTasks")
381 Task[] provideBuildTasks()
382 {
383 	Task[] ret;
384 	foreach (instance; backend.instances)
385 	{
386 		if (!instance.has!DubComponent)
387 			continue;
388 		auto dub = instance.get!DubComponent;
389 		auto workspace = .workspace(instance.cwd.uriFromFile, false);
390 		info("Found dub package to build at ", dub.recipePath);
391 		JSONValue currentMagicValue = JSONValue("$current");
392 		auto cwd = JSONValue(dub.recipePath.dirName.replace(workspace.folder.uri.uriToFile, "${workspaceFolder}"));
393 		{
394 			Task t;
395 			t.source = "dub";
396 			t.definition = JSONValue([
397 					"type": JSONValue("dub"),
398 					"run": JSONValue(true),
399 					"compiler": currentMagicValue,
400 					"archType": currentMagicValue,
401 					"buildType": currentMagicValue,
402 					"configuration": currentMagicValue,
403 					"cwd": cwd
404 					]);
405 			t.group = Task.Group.build;
406 			t.exec = [
407 				workspace.config.d.dubPath.userPath, "run", "--compiler=" ~ dub.compiler,
408 				"-a=" ~ dub.archType, "-b=" ~ dub.buildType, "-c=" ~ dub.configuration
409 			].fixEmptyArgs;
410 			t.scope_ = workspace.folder.uri;
411 			t.name = "Run " ~ dub.name;
412 			ret ~= t;
413 		}
414 		{
415 			Task t;
416 			t.source = "dub";
417 			t.definition = JSONValue([
418 					"type": JSONValue("dub"),
419 					"test": JSONValue(true),
420 					"compiler": currentMagicValue,
421 					"archType": currentMagicValue,
422 					"buildType": currentMagicValue,
423 					"configuration": currentMagicValue,
424 					"cwd": cwd
425 					]);
426 			t.group = Task.Group.test;
427 			t.exec = [
428 				workspace.config.d.dubPath.userPath, "test", "--compiler=" ~ dub.compiler,
429 				"-a=" ~ dub.archType, "-b=" ~ dub.buildType, "-c=" ~ dub.configuration
430 			].fixEmptyArgs;
431 			t.scope_ = workspace.folder.uri;
432 			t.name = "Test " ~ dub.name;
433 			ret ~= t;
434 		}
435 		{
436 			Task t;
437 			t.source = "dub";
438 			t.definition = JSONValue([
439 					"type": JSONValue("dub"),
440 					"run": JSONValue(false),
441 					"compiler": currentMagicValue,
442 					"archType": currentMagicValue,
443 					"buildType": currentMagicValue,
444 					"configuration": currentMagicValue,
445 					"cwd": cwd
446 					]);
447 			t.group = Task.Group.build;
448 			t.exec = [
449 				workspace.config.d.dubPath.userPath, "build", "--compiler=" ~ dub.compiler,
450 				"-a=" ~ dub.archType, "-b=" ~ dub.buildType, "-c=" ~ dub.configuration
451 			].fixEmptyArgs;
452 			t.scope_ = workspace.folder.uri;
453 			t.name = "Build " ~ dub.name;
454 			ret ~= t;
455 		}
456 		{
457 			Task t;
458 			t.source = "dub";
459 			t.definition = JSONValue([
460 					"type": JSONValue("dub"),
461 					"run": JSONValue(false),
462 					"force": JSONValue(true),
463 					"compiler": currentMagicValue,
464 					"archType": currentMagicValue,
465 					"buildType": currentMagicValue,
466 					"configuration": currentMagicValue,
467 					"cwd": cwd
468 					]);
469 			t.group = Task.Group.rebuild;
470 			t.exec = [
471 				workspace.config.d.dubPath.userPath, "build", "--force",
472 				"--compiler=" ~ dub.compiler, "-a=" ~ dub.archType,
473 				"-b=" ~ dub.buildType, "-c=" ~ dub.configuration
474 			].fixEmptyArgs;
475 			t.scope_ = workspace.folder.uri;
476 			t.name = "Rebuild " ~ dub.name;
477 			ret ~= t;
478 		}
479 	}
480 	return ret;
481 }
482 
483 // === Protocol Notifications starting here ===
484 
485 @protocolNotification("served/convertDubFormat")
486 void convertDubFormat(DubConvertRequest req)
487 {
488 	import std.process : execute, Config;
489 
490 	auto file = req.textDocument.uri.uriToFile;
491 	if (!fs.exists(file))
492 	{
493 		error("Specified file does not exist");
494 		return;
495 	}
496 
497 	if (!file.baseName.among!("dub.json", "dub.sdl", "package.json"))
498 	{
499 		rpc.window.showErrorMessage(translate!"d.dub.notRecipeFile");
500 		return;
501 	}
502 
503 	auto document = documents[req.textDocument.uri];
504 
505 	auto result = execute([
506 			workspace(req.textDocument.uri).config.d.dubPath.userPath, "convert",
507 			"-f", req.newFormat, "-s"
508 			], null, Config.stderrPassThrough, 1024 * 1024 * 4, file.dirName);
509 
510 	if (result.status != 0)
511 	{
512 		rpc.window.showErrorMessage(translate!"d.dub.convertFailed");
513 		return;
514 	}
515 
516 	auto newUri = req.textDocument.uri.setExtension("." ~ req.newFormat);
517 
518 	WorkspaceEdit edit;
519 	auto edits = [
520 		TextEdit(TextRange(Position(0, 0), document.offsetToPosition(document.length)), result.output)
521 	];
522 
523 	if (capabilities.workspace.workspaceEdit.resourceOperations.canFind(ResourceOperationKind.rename))
524 	{
525 		edit.documentChanges = JSONValue([
526 				toJSON(RenameFile(req.textDocument.uri, newUri)),
527 				toJSON(TextDocumentEdit(VersionedTextDocumentIdentifier(newUri,
528 					document.version_), edits))
529 				]);
530 	}
531 	else
532 		edit.changes[req.textDocument.uri] = edits;
533 	rpc.sendMethod("workspace/applyEdit", ApplyWorkspaceEditParams(edit));
534 }
535 
536 @protocolNotification("served/installDependency")
537 void installDependency(InstallRequest req)
538 {
539 	auto instance = activeInstance;
540 	auto uri = instance.cwd.uriFromFile;
541 	reportProgress(ProgressType.importUpgrades, 0, 10, uri);
542 	injectDependency(instance, req);
543 	if (instance.has!DubComponent)
544 		instance.get!DubComponent.upgrade();
545 	reportProgress(ProgressType.dubReload, 7, 10, uri);
546 	updateImports(UpdateImportsParams(false));
547 	reportProgress(ProgressType.dubReload, 10, 10, uri);
548 }
549 
550 @protocolNotification("served/updateDependency")
551 void updateDependency(UpdateRequest req)
552 {
553 	auto instance = activeInstance;
554 	auto uri = instance.cwd.uriFromFile;
555 	reportProgress(ProgressType.importUpgrades, 0, 10, uri);
556 	if (changeDependency(instance, req))
557 	{
558 		if (instance.has!DubComponent)
559 			instance.get!DubComponent.upgrade();
560 		reportProgress(ProgressType.dubReload, 7, 10, uri);
561 		updateImports(UpdateImportsParams(false));
562 	}
563 	reportProgress(ProgressType.dubReload, 10, 10, uri);
564 }
565 
566 @protocolNotification("served/uninstallDependency")
567 void uninstallDependency(UninstallRequest req)
568 {
569 	auto instance = activeInstance;
570 	auto uri = instance.cwd.uriFromFile;
571 	reportProgress(ProgressType.importUpgrades, 0, 10, uri);
572 	// TODO: add workspace argument
573 	removeDependency(instance, req.name);
574 	if (instance.has!DubComponent)
575 		instance.get!DubComponent.upgrade();
576 	reportProgress(ProgressType.dubReload, 7, 10, uri);
577 	updateImports(UpdateImportsParams(false));
578 	reportProgress(ProgressType.dubReload, 10, 10, uri);
579 }
580 
581 void injectDependency(WorkspaceD.Instance instance, InstallRequest req)
582 {
583 	auto sdl = buildPath(instance.cwd, "dub.sdl");
584 	if (fs.exists(sdl))
585 	{
586 		int depth = 0;
587 		auto content = fs.readText(sdl).splitLines(KeepTerminator.yes);
588 		auto insertAt = content.length;
589 		bool gotLineEnding = false;
590 		string lineEnding = "\n";
591 		foreach (i, line; content)
592 		{
593 			if (!gotLineEnding && line.length >= 2)
594 			{
595 				lineEnding = line[$ - 2 .. $];
596 				if (lineEnding[0] != '\r')
597 					lineEnding = line[$ - 1 .. $];
598 				gotLineEnding = true;
599 			}
600 			if (depth == 0 && line.strip.startsWith("dependency "))
601 				insertAt = i + 1;
602 			depth += line.count('{') - line.count('}');
603 		}
604 		content = content[0 .. insertAt] ~ ((insertAt == content.length ? lineEnding
605 				: "") ~ "dependency \"" ~ req.name ~ "\" version=\"~>" ~ req.version_ ~ "\"" ~ lineEnding)
606 			~ content[insertAt .. $];
607 		fs.write(sdl, content.join());
608 	}
609 	else
610 	{
611 		auto json = buildPath(instance.cwd, "dub.json");
612 		if (!fs.exists(json))
613 			json = buildPath(instance.cwd, "package.json");
614 		if (!fs.exists(json))
615 			return;
616 		auto content = fs.readText(json).splitLines(KeepTerminator.yes);
617 		auto insertAt = content.length ? content.length - 1 : 0;
618 		string lineEnding = "\n";
619 		bool gotLineEnding = false;
620 		int depth = 0;
621 		bool insertNext;
622 		string indent;
623 		bool foundBlock;
624 		foreach (i, line; content)
625 		{
626 			if (!gotLineEnding && line.length >= 2)
627 			{
628 				lineEnding = line[$ - 2 .. $];
629 				if (lineEnding[0] != '\r')
630 					lineEnding = line[$ - 1 .. $];
631 				gotLineEnding = true;
632 			}
633 			if (insertNext)
634 			{
635 				indent = line[0 .. $ - line.stripLeft.length];
636 				insertAt = i + 1;
637 				break;
638 			}
639 			if (depth == 1 && line.strip.startsWith(`"dependencies":`))
640 			{
641 				foundBlock = true;
642 				if (line.strip.endsWith("{"))
643 				{
644 					indent = line[0 .. $ - line.stripLeft.length];
645 					insertAt = i + 1;
646 					break;
647 				}
648 				else
649 				{
650 					insertNext = true;
651 				}
652 			}
653 			depth += line.count('{') - line.count('}') + line.count('[') - line.count(']');
654 		}
655 		if (foundBlock)
656 		{
657 			content = content[0 .. insertAt] ~ (
658 					indent ~ indent ~ `"` ~ req.name ~ `": "~>` ~ req.version_ ~ `",` ~ lineEnding)
659 				~ content[insertAt .. $];
660 			fs.write(json, content.join());
661 		}
662 		else if (content.length)
663 		{
664 			if (content.length > 1)
665 				content[$ - 2] = content[$ - 2].stripRight;
666 			content = content[0 .. $ - 1] ~ (
667 					"," ~ lineEnding ~ `	"dependencies": {
668 		"` ~ req.name ~ `": "~>` ~ req.version_ ~ `"
669 	}` ~ lineEnding)
670 				~ content[$ - 1 .. $];
671 			fs.write(json, content.join());
672 		}
673 		else
674 		{
675 			content ~= `{
676 	"dependencies": {
677 		"` ~ req.name ~ `": "~>` ~ req.version_ ~ `"
678 	}
679 }`;
680 			fs.write(json, content.join());
681 		}
682 	}
683 }
684 
685 bool changeDependency(WorkspaceD.Instance instance, UpdateRequest req)
686 {
687 	auto sdl = buildPath(instance.cwd, "dub.sdl");
688 	if (fs.exists(sdl))
689 	{
690 		int depth = 0;
691 		auto content = fs.readText(sdl).splitLines(KeepTerminator.yes);
692 		size_t target = size_t.max;
693 		foreach (i, line; content)
694 		{
695 			if (depth == 0 && line.strip.startsWith("dependency ")
696 					&& line.strip["dependency".length .. $].strip.startsWith('"' ~ req.name ~ '"'))
697 			{
698 				target = i;
699 				break;
700 			}
701 			depth += line.count('{') - line.count('}');
702 		}
703 		if (target == size_t.max)
704 			return false;
705 		auto ver = content[target].indexOf("version");
706 		if (ver == -1)
707 			return false;
708 		auto quotStart = content[target].indexOf("\"", ver);
709 		if (quotStart == -1)
710 			return false;
711 		auto quotEnd = content[target].indexOf("\"", quotStart + 1);
712 		if (quotEnd == -1)
713 			return false;
714 		content[target] = content[target][0 .. quotStart] ~ '"' ~ req.version_ ~ '"'
715 			~ content[target][quotEnd .. $];
716 		fs.write(sdl, content.join());
717 		return true;
718 	}
719 	else
720 	{
721 		auto json = buildPath(instance.cwd, "dub.json");
722 		if (!fs.exists(json))
723 			json = buildPath(instance.cwd, "package.json");
724 		if (!fs.exists(json))
725 			return false;
726 		auto content = fs.readText(json);
727 		auto replaced = content.replaceFirst(regex(`("` ~ req.name ~ `"\s*:\s*)"[^"]*"`),
728 				`$1"` ~ req.version_ ~ `"`);
729 		if (content == replaced)
730 			return false;
731 		fs.write(json, replaced);
732 		return true;
733 	}
734 }
735 
736 bool removeDependency(WorkspaceD.Instance instance, string name)
737 {
738 	auto sdl = buildPath(instance.cwd, "dub.sdl");
739 	if (fs.exists(sdl))
740 	{
741 		int depth = 0;
742 		auto content = fs.readText(sdl).splitLines(KeepTerminator.yes);
743 		size_t target = size_t.max;
744 		foreach (i, line; content)
745 		{
746 			if (depth == 0 && line.strip.startsWith("dependency ")
747 					&& line.strip["dependency".length .. $].strip.startsWith('"' ~ name ~ '"'))
748 			{
749 				target = i;
750 				break;
751 			}
752 			depth += line.count('{') - line.count('}');
753 		}
754 		if (target == size_t.max)
755 			return false;
756 		fs.write(sdl, (content[0 .. target] ~ content[target + 1 .. $]).join());
757 		return true;
758 	}
759 	else
760 	{
761 		auto json = buildPath(instance.cwd, "dub.json");
762 		if (!fs.exists(json))
763 			json = buildPath(instance.cwd, "package.json");
764 		if (!fs.exists(json))
765 			return false;
766 		auto content = fs.readText(json);
767 		auto replaced = content.replaceFirst(regex(`"` ~ name ~ `"\s*:\s*"[^"]*"\s*,\s*`), "");
768 		if (content == replaced)
769 			replaced = content.replaceFirst(regex(`\s*,\s*"` ~ name ~ `"\s*:\s*"[^"]*"`), "");
770 		if (content == replaced)
771 			replaced = content.replaceFirst(regex(
772 					`"dependencies"\s*:\s*\{\s*"` ~ name ~ `"\s*:\s*"[^"]*"\s*\}\s*,\s*`), "");
773 		if (content == replaced)
774 			replaced = content.replaceFirst(regex(
775 					`\s*,\s*"dependencies"\s*:\s*\{\s*"` ~ name ~ `"\s*:\s*"[^"]*"\s*\}`), "");
776 		if (content == replaced)
777 			return false;
778 		fs.write(json, replaced);
779 		return true;
780 	}
781 }