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