1 module workspaced.com.dlangui;
2 
3 import core.thread;
4 import std.algorithm;
5 import std.json;
6 import std.process;
7 import std.string;
8 import std.uni;
9 
10 import painlessjson;
11 
12 import workspaced.api;
13 import workspaced.completion.dml;
14 
15 @component("dlangui")
16 class DlanguiComponent : ComponentWrapper
17 {
18 	mixin DefaultComponentWrapper;
19 
20 	/// Queries for code completion at position `pos` in DML code
21 	/// Returns: `[{type: CompletionType, value: string, documentation: string, enumName: string}]`
22 	/// Where type is an integer
23 	Future!(CompletionItem[]) complete(string code, int pos)
24 	{
25 		auto ret = new Future!(CompletionItem[]);
26 		new Thread({
27 			try
28 			{
29 				LocationInfo info = getLocationInfo(code, pos);
30 				CompletionItem[] suggestions;
31 				string name = info.itemScope[$ - 1];
32 				string[] stack;
33 				if (info.itemScope.length > 1)
34 					stack = info.itemScope[0 .. $ - 1];
35 				string[][] curScope = stack.getProvidedScope();
36 				if (info.type == LocationType.RootMember)
37 				{
38 					foreach (CompletionLookup item; dmlCompletions)
39 					{
40 						if (item.item.type == CompletionType.Class)
41 						{
42 							if (name.length == 0 || item.item.value.canFind(name))
43 							{
44 								suggestions ~= item.item;
45 							}
46 						}
47 					}
48 				}
49 				else if (info.type == LocationType.Member)
50 				{
51 					foreach (CompletionLookup item; dmlCompletions)
52 					{
53 						if (item.item.type == CompletionType.Class)
54 						{
55 							if (name.length == 0 || item.item.value.canFind(name))
56 							{
57 								suggestions ~= item.item;
58 							}
59 						}
60 						else if (item.item.type != CompletionType.EnumDefinition)
61 						{
62 							if (curScope.canFind(item.requiredScope))
63 							{
64 								if (name.length == 0 || item.item.value.canFind(name))
65 								{
66 									suggestions ~= item.item;
67 								}
68 							}
69 						}
70 					}
71 				}
72 				else if (info.type == LocationType.PropertyValue)
73 				{
74 					foreach (CompletionLookup item; dmlCompletions)
75 					{
76 						if (item.item.type == CompletionType.EnumValue)
77 						{
78 							if (curScope.canFind(item.requiredScope))
79 							{
80 								if (item.item.value == name)
81 								{
82 									foreach (CompletionLookup enumdef; dmlCompletions)
83 									{
84 										if (enumdef.item.type == CompletionType.EnumDefinition)
85 										{
86 											if (enumdef.item.enumName == item.item.enumName)
87 												suggestions ~= enumdef.item;
88 										}
89 									}
90 									break;
91 								}
92 							}
93 						}
94 						else if (item.item.type == CompletionType.Boolean)
95 						{
96 							if (curScope.canFind(item.requiredScope))
97 							{
98 								if (item.item.value == name)
99 								{
100 									suggestions ~= CompletionItem(CompletionType.Keyword, "true");
101 									suggestions ~= CompletionItem(CompletionType.Keyword, "false");
102 									break;
103 								}
104 							}
105 						}
106 					}
107 				}
108 				ret.finish(suggestions);
109 			}
110 			catch (Throwable e)
111 			{
112 				ret.error(e);
113 			}
114 		}).start();
115 		return ret;
116 	}
117 }
118 
119 ///
120 enum CompletionType : ubyte
121 {
122 	///
123 	Undefined = 0,
124 	///
125 	Class = 1,
126 	///
127 	String = 2,
128 	///
129 	Number = 3,
130 	///
131 	Color = 4,
132 	///
133 	EnumDefinition = 5,
134 	///
135 	EnumValue = 6,
136 	///
137 	Rectangle = 7,
138 	///
139 	Boolean = 8,
140 	///
141 	Keyword = 9,
142 }
143 
144 /// Returned by list-completion
145 struct CompletionItem
146 {
147 	///
148 	CompletionType type;
149 	///
150 	string value;
151 	///
152 	string documentation = "";
153 	///
154 	string enumName = "";
155 }
156 
157 struct CompletionLookup
158 {
159 	CompletionItem item;
160 	string[][] providedScope = [];
161 	string[] requiredScope = [];
162 }
163 
164 private:
165 
166 string[][] getProvidedScope(string[] stack)
167 {
168 	if (stack.length == 0)
169 		return [];
170 	string[][] providedScope;
171 	foreach (CompletionLookup item; dmlCompletions)
172 	{
173 		if (item.item.type == CompletionType.Class)
174 		{
175 			if (item.item.value == stack[$ - 1])
176 			{
177 				providedScope ~= item.providedScope;
178 				break;
179 			}
180 		}
181 	}
182 	return providedScope;
183 }
184 
185 enum LocationType : ubyte
186 {
187 	RootMember,
188 	Member,
189 	PropertyValue,
190 	None
191 }
192 
193 struct LocationInfo
194 {
195 	LocationType type;
196 	string[] itemScope;
197 	string propertyName;
198 }
199 
200 LocationInfo getLocationInfo(in string code, int pos)
201 {
202 	LocationInfo current;
203 	current.type = LocationType.RootMember;
204 	current.itemScope = [];
205 	current.propertyName = "";
206 	string member = "";
207 	bool inString = false;
208 	bool escapeChar = false;
209 	foreach (i, c; code)
210 	{
211 		if (i == pos)
212 			break;
213 		if (inString)
214 		{
215 			if (escapeChar)
216 				escapeChar = false;
217 			else
218 			{
219 				if (c == '\\')
220 				{
221 					escapeChar = true;
222 				}
223 				else if (c == '"')
224 				{
225 					inString = false;
226 					current.type = LocationType.None;
227 					member = "";
228 					escapeChar = false;
229 				}
230 			}
231 			continue;
232 		}
233 		else
234 		{
235 			if (c == '{')
236 			{
237 				current.itemScope ~= member;
238 				current.propertyName = "";
239 				member = "";
240 				current.type = LocationType.Member;
241 			}
242 			else if (c == '\n' || c == '\r' || c == ';')
243 			{
244 				current.propertyName = "";
245 				member = "";
246 				current.type = LocationType.Member;
247 			}
248 			else if (c == ':')
249 			{
250 				current.propertyName = member;
251 				member = "";
252 				current.type = LocationType.PropertyValue;
253 			}
254 			else if (c == '"')
255 			{
256 				inString = true;
257 			}
258 			else if (c == '}')
259 			{
260 				if (current.itemScope.length > 0)
261 					current.itemScope.length--;
262 				current.type = LocationType.None;
263 				current.propertyName = "";
264 				member = "";
265 			}
266 			else if (c.isWhite)
267 			{
268 				if (current.type == LocationType.None)
269 					current.type = LocationType.Member;
270 				if (current.itemScope.length == 0)
271 					current.type = LocationType.RootMember;
272 			}
273 			else
274 			{
275 				if (current.type == LocationType.Member || current.type == LocationType.RootMember)
276 					member ~= c;
277 			}
278 		}
279 	}
280 	if (member.length)
281 		current.propertyName = member;
282 	current.itemScope ~= current.propertyName;
283 	return current;
284 }
285 
286 unittest
287 {
288 	import dunit.toolkit;
289 
290 	auto info = getLocationInfo(" ", 0);
291 	assertEqual(info.type, LocationType.RootMember);
292 	info = getLocationInfo(`TableLayout { mar }`, 17);
293 	assertEqual(info.itemScope, ["TableLayout", "mar"]);
294 	assertEqual(info.type, LocationType.Member);
295 	info = getLocationInfo(`TableLayout { margins: 20; paddin }`, 33);
296 	assertEqual(info.itemScope, ["TableLayout", "paddin"]);
297 	assertEqual(info.type, LocationType.Member);
298 	info = getLocationInfo(
299 			"TableLayout { margins: 20; padding : 10\n\t\tTextWidget { text: \"} foo } }", 70);
300 	assertEqual(info.itemScope, ["TableLayout", "TextWidget", "text"]);
301 	assertEqual(info.type, LocationType.PropertyValue);
302 	info = getLocationInfo(`TableLayout { margins: 2 }`, 24);
303 	assertEqual(info.itemScope, ["TableLayout", "margins"]);
304 	assertEqual(info.type, LocationType.PropertyValue);
305 	info = getLocationInfo(
306 			"TableLayout { margins: 20; padding : 10\n\t\tTextWidget { text: \"} foobar\" } } ", int.max);
307 	assertEqual(info.itemScope, [""]);
308 	assertEqual(info.type, LocationType.RootMember);
309 	info = getLocationInfo(
310 			"TableLayout { margins: 20; padding : 10\n\t\tTextWidget { text: \"} foobar\"; } }", 69);
311 	assertEqual(info.itemScope, ["TableLayout", "TextWidget", "text"]);
312 	assertEqual(info.type, LocationType.PropertyValue);
313 	info = getLocationInfo("TableLayout {\n\t", int.max);
314 	assertEqual(info.itemScope, ["TableLayout", ""]);
315 	assertEqual(info.type, LocationType.Member);
316 	info = getLocationInfo(`TableLayout {
317 	colCount: 2
318 	margins: 20; padding: 10
319 	backgroundColor: "#FFFFE0"
320 	TextWidget {
321 		t`, int.max);
322 	assertEqual(info.itemScope, ["TableLayout", "TextWidget", "t"]);
323 	assertEqual(info.type, LocationType.Member);
324 }