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 workspaced.api;
11 import workspaced.completion.dml;
12 
13 @component("dlangui")
14 class DlanguiComponent : ComponentWrapper
15 {
16 	mixin DefaultComponentWrapper;
17 
18 	/// Queries for code completion at position `pos` in DML code
19 	/// Returns: `[{type: CompletionType, value: string, documentation: string, enumName: string}]`
20 	/// Where type is an integer
21 	Future!(CompletionItem[]) complete(scope const(char)[] code, int pos)
22 	{
23 		auto ret = new typeof(return);
24 		gthreads.create({
25 			mixin(traceTask);
26 			try
27 			{
28 				LocationInfo info = getLocationInfo(code, pos);
29 				CompletionItem[] suggestions;
30 				string name = info.itemScope[$ - 1];
31 				string[] stack;
32 				if (info.itemScope.length > 1)
33 					stack = info.itemScope[0 .. $ - 1];
34 				string[][] curScope = stack.getProvidedScope();
35 				if (info.type == LocationType.RootMember)
36 				{
37 					foreach (CompletionLookup item; dmlCompletions)
38 					{
39 						if (item.item.type == CompletionType.Class)
40 						{
41 							if (name.length == 0 || item.item.value.canFind(name))
42 							{
43 								suggestions ~= item.item;
44 							}
45 						}
46 					}
47 				}
48 				else if (info.type == LocationType.Member)
49 				{
50 					foreach (CompletionLookup item; dmlCompletions)
51 					{
52 						if (item.item.type == CompletionType.Class)
53 						{
54 							if (name.length == 0 || item.item.value.canFind(name))
55 							{
56 								suggestions ~= item.item;
57 							}
58 						}
59 						else if (item.item.type != CompletionType.EnumDefinition)
60 						{
61 							if (curScope.canFind(item.requiredScope))
62 							{
63 								if (name.length == 0 || item.item.value.canFind(name))
64 								{
65 									suggestions ~= item.item;
66 								}
67 							}
68 						}
69 					}
70 				}
71 				else if (info.type == LocationType.PropertyValue)
72 				{
73 					foreach (CompletionLookup item; dmlCompletions)
74 					{
75 						if (item.item.type == CompletionType.EnumValue)
76 						{
77 							if (curScope.canFind(item.requiredScope))
78 							{
79 								if (item.item.value == name)
80 								{
81 									foreach (CompletionLookup enumdef; dmlCompletions)
82 									{
83 										if (enumdef.item.type == CompletionType.EnumDefinition)
84 										{
85 											if (enumdef.item.enumName == item.item.enumName)
86 												suggestions ~= enumdef.item;
87 										}
88 									}
89 									break;
90 								}
91 							}
92 						}
93 						else if (item.item.type == CompletionType.Boolean)
94 						{
95 							if (curScope.canFind(item.requiredScope))
96 							{
97 								if (item.item.value == name)
98 								{
99 									suggestions ~= CompletionItem(CompletionType.Keyword, "true");
100 									suggestions ~= CompletionItem(CompletionType.Keyword, "false");
101 									break;
102 								}
103 							}
104 						}
105 					}
106 				}
107 				ret.finish(suggestions);
108 			}
109 			catch (Throwable e)
110 			{
111 				ret.error(e);
112 			}
113 		});
114 		return ret;
115 	}
116 }
117 
118 ///
119 enum CompletionType : ubyte
120 {
121 	///
122 	Undefined = 0,
123 	///
124 	Class = 1,
125 	///
126 	String = 2,
127 	///
128 	Number = 3,
129 	///
130 	Color = 4,
131 	///
132 	EnumDefinition = 5,
133 	///
134 	EnumValue = 6,
135 	///
136 	Rectangle = 7,
137 	///
138 	Boolean = 8,
139 	///
140 	Keyword = 9,
141 }
142 
143 /// Returned by list-completion
144 struct CompletionItem
145 {
146 	///
147 	CompletionType type;
148 	///
149 	string value;
150 	///
151 	string documentation = "";
152 	///
153 	string enumName = "";
154 }
155 
156 struct CompletionLookup
157 {
158 	CompletionItem item;
159 	string[][] providedScope = [];
160 	string[] requiredScope = [];
161 }
162 
163 private:
164 
165 string[][] getProvidedScope(string[] stack)
166 {
167 	if (stack.length == 0)
168 		return [];
169 	string[][] providedScope;
170 	foreach (CompletionLookup item; dmlCompletions)
171 	{
172 		if (item.item.type == CompletionType.Class)
173 		{
174 			if (item.item.value == stack[$ - 1])
175 			{
176 				providedScope ~= item.providedScope;
177 				break;
178 			}
179 		}
180 	}
181 	return providedScope;
182 }
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(scope const(char)[] 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 	auto info = getLocationInfo(" ", 0);
288 	assert(info.type == LocationType.RootMember);
289 	info = getLocationInfo(`TableLayout { mar }`, 17);
290 	assert(info.itemScope == ["TableLayout", "mar"]);
291 	assert(info.type == LocationType.Member);
292 	info = getLocationInfo(`TableLayout { margins: 20; paddin }`, 33);
293 	assert(info.itemScope == ["TableLayout", "paddin"]);
294 	assert(info.type == LocationType.Member);
295 	info = getLocationInfo(
296 			"TableLayout { margins: 20; padding : 10\n\t\tTextWidget { text: \"} foo } }", 70);
297 	assert(info.itemScope == ["TableLayout", "TextWidget", "text"]);
298 	assert(info.type == LocationType.PropertyValue);
299 	info = getLocationInfo(`TableLayout { margins: 2 }`, 24);
300 	assert(info.itemScope == ["TableLayout", "margins"]);
301 	assert(info.type == LocationType.PropertyValue);
302 	info = getLocationInfo(
303 			"TableLayout { margins: 20; padding : 10\n\t\tTextWidget { text: \"} foobar\" } } ", int.max);
304 	assert(info.itemScope == [""]);
305 	assert(info.type == LocationType.RootMember);
306 	info = getLocationInfo(
307 			"TableLayout { margins: 20; padding : 10\n\t\tTextWidget { text: \"} foobar\"; } }", 69);
308 	assert(info.itemScope == ["TableLayout", "TextWidget", "text"]);
309 	assert(info.type == LocationType.PropertyValue);
310 	info = getLocationInfo("TableLayout {\n\t", int.max);
311 	assert(info.itemScope == ["TableLayout", ""]);
312 	assert(info.type == LocationType.Member);
313 	info = getLocationInfo(`TableLayout {
314 	colCount: 2
315 	margins: 20; padding: 10
316 	backgroundColor: "#FFFFE0"
317 	TextWidget {
318 		t`, int.max);
319 	assert(info.itemScope == ["TableLayout", "TextWidget", "t"]);
320 	assert(info.type == LocationType.Member);
321 }