1 module workspaced.com.snippets; 2 3 import dparse.lexer; 4 import dparse.parser; 5 import dparse.rollback_allocator; 6 7 import workspaced.api; 8 import workspaced.com.dfmt : DfmtComponent; 9 import workspaced.com.snippets.generator; 10 11 import std.algorithm; 12 import std.array; 13 import std.ascii; 14 import std.conv; 15 import std.json; 16 import std.string; 17 18 public import workspaced.com.snippets.plain; 19 public import workspaced.com.snippets.smart; 20 public import workspaced.com.snippets.dependencies; 21 22 /// Component for auto completing snippets with context information and formatting these snippets with dfmt. 23 @component("snippets") 24 class SnippetsComponent : ComponentWrapper 25 { 26 mixin DefaultComponentWrapper; 27 28 static PlainSnippetProvider plainSnippets; 29 static SmartSnippetProvider smartSnippets; 30 static DependencyBasedSnippetProvider dependencySnippets; 31 32 protected SnippetProvider[] providers; 33 34 protected void load() 35 { 36 config.stringBehavior = StringBehavior.source; 37 providers.reserve(16); 38 providers ~= plainSnippets = new PlainSnippetProvider(); 39 providers ~= smartSnippets = new SmartSnippetProvider(); 40 providers ~= dependencySnippets = new DependencyBasedSnippetProvider(); 41 } 42 43 /** 44 * Params: 45 * file = Filename to resolve dependencies relatively from. 46 * code = Code to complete snippet in. 47 * position = Byte offset of where to find scope in. 48 * 49 * Returns: a `SnippetInfo` object for all snippet information. 50 * 51 * `.loopScope` is set if a loop can be inserted at this position, Optionally 52 * with information about close ranges. Contains `SnippetLoopScope.init` if 53 * this is not a location where a loop can be inserted. 54 */ 55 SnippetInfo determineSnippetInfo(scope const(char)[] file, scope const(char)[] code, int position) 56 { 57 // each variable is 1 58 // maybe more expensive lookups with DCD in the future 59 enum LoopVariableAnalyzeMaxCost = 90; 60 61 scope tokens = getTokensForParser(cast(ubyte[]) code, config, &workspaced.stringCache); 62 // TODO: binary search 63 size_t loc; 64 foreach (i, tok; tokens) 65 if (tok.index >= position) 66 { 67 loc = i; 68 break; 69 } 70 71 auto leading = tokens[0 .. loc]; 72 foreach_reverse (t; leading) 73 { 74 if (t.type == tok!";") 75 break; 76 77 // test for tokens semicolon closed statements where we should abort to avoid incomplete syntax 78 if (t.type.among!(tok!"import", tok!"module")) 79 { 80 return SnippetInfo([SnippetLevel.global, SnippetLevel.other]); 81 } 82 else if (t.type.among!(tok!"=", tok!"+", tok!"-", tok!"*", tok!"/", 83 tok!"%", tok!"^^", tok!"&", tok!"|", tok!"^", tok!"<<", 84 tok!">>", tok!">>>", tok!"~", tok!"in")) 85 { 86 return SnippetInfo([SnippetLevel.global, SnippetLevel.value]); 87 } 88 } 89 90 scope parsed = parseModule(tokens, cast(string) file, &rba); 91 92 trace("determineSnippetInfo at ", position); 93 94 scope gen = new SnippetInfoGenerator(position); 95 gen.variableStack.reserve(64); 96 gen.visit(parsed); 97 98 gen.value.loopScope.supported = gen.value.level == SnippetLevel.method; 99 if (gen.value.loopScope.supported) 100 { 101 int cost = 0; 102 foreach_reverse (v; gen.variableStack) 103 { 104 if (fillLoopScopeInfo(gen.value.loopScope, v)) 105 break; 106 if (++cost > LoopVariableAnalyzeMaxCost) 107 break; 108 } 109 } 110 111 return gen.value; 112 } 113 114 Future!(Snippet[]) getSnippets(scope const(char)[] file, scope const(char)[] code, int position) 115 { 116 mixin(gthreadsAsyncProxy!`getSnippetsBlocking(file, code, position)`); 117 } 118 119 Snippet[] getSnippetsBlocking(scope const(char)[] file, scope const(char)[] code, int position) 120 { 121 auto futures = collectSnippets(file, code, position); 122 123 auto ret = appender!(Snippet[]); 124 foreach (fut; futures) 125 ret.put(fut.getBlocking()); 126 return ret.data; 127 } 128 129 Snippet[] getSnippetsYield(scope const(char)[] file, scope const(char)[] code, int position) 130 { 131 auto futures = collectSnippets(file, code, position); 132 133 auto ret = appender!(Snippet[]); 134 foreach (fut; futures) 135 ret.put(fut.getYield()); 136 return ret.data; 137 } 138 139 Future!Snippet resolveSnippet(scope const(char)[] file, scope const(char)[] code, 140 int position, Snippet snippet) 141 { 142 foreach (provider; providers) 143 { 144 if (typeid(provider).name == snippet.providerId) 145 { 146 const info = determineSnippetInfo(file, code, position); 147 return provider.resolveSnippet(instance, file, code, position, info, snippet); 148 } 149 } 150 151 return Future!Snippet.fromResult(snippet); 152 } 153 154 Future!string format(scope const(char)[] snippet, string[] arguments = []) 155 { 156 mixin(gthreadsAsyncProxy!`formatSync(snippet, arguments)`); 157 } 158 159 /// Will format the code passed in synchronously using dfmt. Might take a short moment on larger documents. 160 /// Returns: the formatted code as string or unchanged if dfmt is not active 161 string formatSync(scope const(char)[] snippet, string[] arguments = []) 162 { 163 if (!has!DfmtComponent) 164 return snippet.idup; 165 166 auto dfmt = get!DfmtComponent; 167 168 auto tmp = appender!string; 169 170 scope const(char)[][string] tokens; 171 172 ptrdiff_t dollar, last; 173 while (true) 174 { 175 dollar = snippet.indexOfAny(`$\`, last); 176 if (dollar == -1) 177 { 178 tmp ~= snippet[last .. $]; 179 break; 180 } 181 182 tmp ~= snippet[last .. dollar]; 183 last = dollar + 1; 184 if (last >= snippet.length) 185 break; 186 if (snippet[dollar] == '\\') 187 { 188 tmp ~= snippet[dollar + 1]; 189 last = dollar + 2; 190 } 191 else 192 { 193 string key = "__WspD_Snp_" ~ dollar.to!string; 194 const(char)[] str; 195 196 if (snippet[dollar + 1] == '{') 197 { 198 ptrdiff_t i = dollar + 2; 199 int depth = 1; 200 while (true) 201 { 202 auto next = snippet.indexOfAny(`\{}`, i); 203 if (next == -1) 204 { 205 i = snippet.length; 206 break; 207 } 208 209 if (snippet[next] == '\\') 210 i = next + 2; 211 else 212 { 213 if (snippet[next] == '{') 214 depth++; 215 else if (snippet[next] == '}') 216 depth--; 217 else 218 assert(false); 219 220 i = next + 1; 221 } 222 223 if (depth == 0) 224 break; 225 } 226 str = snippet[dollar .. i]; 227 last = i; 228 229 if (str.length > 5 || snippet[last .. $].stripLeft.startsWith(";", ".", "{")) 230 { 231 // let's insert some token in here instead of a comment because there is probably some default content 232 if (str[0 .. $ - 1].endsWith(';')) 233 key ~= ';'; 234 } 235 else 236 { 237 // empty default, put in comment 238 key = "/+++" ~ key ~ "+++/"; 239 } 240 } 241 else 242 { 243 size_t end = dollar + 1; 244 245 if (snippet[dollar + 1].isDigit) 246 { 247 while (end < snippet.length && snippet[end].isDigit) 248 end++; 249 } 250 else 251 { 252 while (end < snippet.length && (snippet[end].isAlphaNum || snippet[end] == '_')) 253 end++; 254 } 255 256 str = snippet[dollar .. end]; 257 last = end; 258 if (snippet[last .. $].stripLeft.startsWith(";", ".", "{")) 259 { 260 // keep value thing as token 261 } 262 else 263 { 264 // primitive placeholder as comment 265 key = "/+++" ~ key ~ "+++/"; 266 } 267 } 268 269 tokens[key] = str; 270 tmp ~= key; 271 } 272 } 273 274 auto res = dfmt.formatSync(tmp.data, arguments); 275 276 foreach (key, value; tokens) 277 { 278 // TODO: replacing using aho-corasick would be far more efficient but there is nothing like that in phobos 279 res = res.replace(key, value); 280 } 281 282 if (res.endsWith("\r\n") && !snippet.endsWith('\n')) 283 res.length -= 2; 284 else if (res.endsWith('\n') && !snippet.endsWith('\n')) 285 res.length--; 286 287 if (res.endsWith(";\n\n$0")) 288 res = res[0 .. $ - "\n$0".length] ~ "$0"; 289 else if (res.endsWith(";\r\n\r\n$0")) 290 res = res[0 .. $ - "\r\n$0".length] ~ "$0"; 291 292 return res; 293 } 294 295 /// Adds snippets which complete conditionally based on dub dependencies being present. 296 /// This function affects the global configuration of all instances. 297 /// Params: 298 /// requiredDependencies = The dependencies which must be present in order for this snippet to show up. 299 /// snippet = The snippet to suggest when the required dependencies are matched. 300 void addDependencySnippet(string[] requiredDependencies, PlainSnippet snippet) 301 { 302 // maybe application global change isn't such a good idea? Current config system seems too inefficient for this. 303 dependencySnippets.addSnippet(requiredDependencies, snippet); 304 } 305 306 private: 307 Future!(Snippet[])[] collectSnippets(scope const(char)[] file, 308 scope const(char)[] code, int position) 309 { 310 const inst = instance; 311 const info = determineSnippetInfo(file, code, position); 312 auto futures = appender!(Future!(Snippet[])[]); 313 foreach (provider; providers) 314 futures.put(provider.provideSnippets(inst, file, code, position, info)); 315 return futures.data; 316 } 317 318 RollbackAllocator rba; 319 LexerConfig config; 320 } 321 322 /// 323 enum SnippetLevel 324 { 325 /// Outside of functions or types, possibly inside templates 326 global, 327 /// Inside interfaces, classes, structs or unions 328 type, 329 /// Inside method body 330 method, 331 /// inside a variable value, argument call, default value or similar 332 value, 333 /// Other scope types (for example outside of braces but after a function definition or some other invalid syntax place) 334 other 335 } 336 337 /// 338 struct SnippetLoopScope 339 { 340 /// true if an loop expression can be inserted at this point 341 bool supported; 342 /// true if we know we are iterating over a string (possibly needing unicode decoding) or false otherwise 343 bool stringIterator; 344 /// Explicit type to use when iterating or null if none is known 345 string type; 346 /// Best variable to iterate over or null if none was found 347 string iterator; 348 /// Number of keys to iterate over 349 int numItems = 1; 350 } 351 352 /// 353 struct SnippetInfo 354 { 355 /// Levels this snippet location has gone through, latest one being the last 356 SnippetLevel[] stack = [SnippetLevel.global]; 357 /// Information about snippets using loop context 358 SnippetLoopScope loopScope; 359 360 /// Current snippet scope level of the location 361 SnippetLevel level() const @property 362 { 363 return stack.length ? stack[$ - 1] : SnippetLevel.other; 364 } 365 } 366 367 /// 368 interface SnippetProvider 369 { 370 Future!(Snippet[]) provideSnippets(scope const WorkspaceD.Instance instance, 371 scope const(char)[] file, scope const(char)[] code, int position, const SnippetInfo info); 372 373 Future!Snippet resolveSnippet(scope const WorkspaceD.Instance instance, 374 scope const(char)[] file, scope const(char)[] code, int position, 375 const SnippetInfo info, Snippet snippet); 376 } 377 378 /// Snippet to insert 379 struct Snippet 380 { 381 /// Internal ID for resolving this snippet 382 string id, providerId; 383 /// User-defined data for helping resolving this snippet 384 JSONValue data; 385 /// Label for this snippet 386 string title; 387 /// Shortcut to type for this snippet 388 string shortcut; 389 /// Markdown documentation for this snippet 390 string documentation; 391 /// Plain text to insert assuming global level indentation. 392 string plain; 393 /// Text with interactive snippet locations to insert assuming global indentation. 394 string snippet; 395 /// true if this snippet can be used as-is 396 bool resolved; 397 } 398 399 unittest 400 { 401 scope backend = new WorkspaceD(); 402 auto workspace = makeTemporaryTestingWorkspace; 403 auto instance = backend.addInstance(workspace.directory); 404 backend.register!SnippetsComponent; 405 backend.register!DfmtComponent; 406 SnippetsComponent snippets = backend.get!SnippetsComponent(workspace.directory); 407 408 auto args = ["--indent_style", "tab"]; 409 410 auto res = snippets.formatSync("void main(${1:string[] args}) {\n\t$0\n}", args); 411 shouldEqual(res, "void main(${1:string[] args})\n{\n\t$0\n}"); 412 413 res = snippets.formatSync("class ${1:MyClass} {\n\t$0\n}", args); 414 shouldEqual(res, "class ${1:MyClass}\n{\n\t$0\n}"); 415 416 res = snippets.formatSync("enum ${1:MyEnum} = $2;\n$0", args); 417 shouldEqual(res, "enum ${1:MyEnum} = $2;\n$0"); 418 419 res = snippets.formatSync("import ${1:std};\n$0", args); 420 shouldEqual(res, "import ${1:std};\n$0"); 421 }