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 }