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 }