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 }