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