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 }