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