1 module workspaced.com.importer; 2 3 import dparse.parser; 4 import dparse.lexer; 5 import dparse.ast; 6 import dparse.rollback_allocator; 7 8 import std.algorithm; 9 import std.array; 10 import std.stdio; 11 12 import workspaced.api; 13 14 @component("importer") : 15 16 /// Initializes the import parser. Call with `{"cmd": "load", "components": ["importer"]}` 17 @load void start() 18 { 19 config.stringBehavior = StringBehavior.source; 20 cache = new StringCache(StringCache.defaultBucketCount); 21 } 22 23 /// Has no purpose right now. 24 @unload void stop() 25 { 26 } 27 28 /// Returns all imports available at some code position. 29 /// Call_With: `{"subcmd": "get"}` 30 @arguments("subcmd", "get") 31 ImportInfo[] get(string code, int pos) 32 { 33 auto tokens = getTokensForParser(cast(ubyte[]) code, config, cache); 34 auto mod = parseModule(tokens, "code", &rba, &doNothing); 35 auto reader = new ImporterReaderVisitor(pos); 36 reader.visit(mod); 37 return reader.imports; 38 } 39 40 /// Returns a list of code patches for adding an import. 41 /// If `insertOutermost` is false, the import will get added to the innermost block. 42 /// Call_With: `{"subcmd": "add"}` 43 @arguments("subcmd", "add") 44 ImportModification add(string importName, string code, int pos, bool insertOutermost = true) 45 { 46 auto tokens = getTokensForParser(cast(ubyte[]) code, config, cache); 47 auto mod = parseModule(tokens, "code", &rba, &doNothing); 48 auto reader = new ImporterReaderVisitor(pos); 49 reader.visit(mod); 50 foreach (i; reader.imports) 51 { 52 if (i.name.join('.') == importName) 53 { 54 if (i.selectives.length == 0) 55 return ImportModification(i.rename, []); 56 else 57 insertOutermost = false; 58 } 59 } 60 string indentation = ""; 61 if (insertOutermost) 62 { 63 indentation = reader.outerImportLocation == 0 ? "" : (cast(ubyte[]) code) 64 .getIndentation(reader.outerImportLocation); 65 if (reader.isModule) 66 indentation = '\n' ~ indentation; 67 return ImportModification("", [CodeReplacement([reader.outerImportLocation, reader.outerImportLocation], 68 indentation ~ "import " ~ importName ~ ";" ~ (reader.outerImportLocation == 0 ? "\n" : ""))]); 69 } 70 else 71 { 72 indentation = (cast(ubyte[]) code).getIndentation(reader.innermostBlockStart); 73 if (reader.isModule) 74 indentation = '\n' ~ indentation; 75 return ImportModification("", [CodeReplacement([reader.innermostBlockStart, 76 reader.innermostBlockStart], indentation ~ "import " ~ importName ~ ";")]); 77 } 78 } 79 80 /// Information about how to add an import 81 struct ImportModification 82 { 83 /// Set if there was already an import which was renamed. (for example import io = std.stdio; would be "io") 84 string rename; 85 /// Array of replacements to add the import to the code 86 CodeReplacement[] replacements; 87 } 88 89 /// Describes what to insert/replace/delete to do something 90 struct CodeReplacement 91 { 92 /// Range what to replace. If both indices are the same its inserting. 93 size_t[2] range; 94 /// Content to replace it with. Empty means remove. 95 string content; 96 97 string apply(string code) 98 { 99 size_t min = range[0]; 100 size_t max = range[1]; 101 if (min > max) 102 { 103 min = range[1]; 104 max = range[0]; 105 } 106 if (min >= code.length) 107 return code ~ content; 108 if (max >= code.length) 109 return code[0 .. min] ~ content; 110 return code[0 .. min] ~ content ~ code[max .. $]; 111 } 112 } 113 114 /// Name and (if specified) rename of a symbol 115 struct SelectiveImport 116 { 117 /// Original name (always available) 118 string name; 119 /// Rename if specified 120 string rename; 121 } 122 123 /// Information about one import statement 124 struct ImportInfo 125 { 126 /// Parts of the imported module. (std.stdio -> ["std", "stdio"]) 127 string[] name; 128 /// Available if the module has been imported renamed 129 string rename; 130 /// Array of selective imports or empty if the entire module has been imported 131 SelectiveImport[] selectives; 132 } 133 134 private __gshared: 135 RollbackAllocator rba; 136 LexerConfig config; 137 StringCache* cache; 138 139 string getIndentation(ubyte[] code, size_t index) 140 { 141 import std.ascii : isWhite; 142 143 bool atLineEnd = false; 144 if (index < code.length && code[index] == '\n') 145 { 146 for (size_t i = index; i < code.length; i++) 147 if (!code[i].isWhite) 148 break; 149 atLineEnd = true; 150 } 151 while (index > 0) 152 { 153 if (code[index - 1] == cast(ubyte) '\n') 154 break; 155 index--; 156 } 157 size_t end = index; 158 while (end < code.length) 159 { 160 if (!code[end].isWhite) 161 break; 162 end++; 163 } 164 auto indent = cast(string) code[index .. end]; 165 if (!indent.length && index == 0 && !atLineEnd) 166 return " "; 167 return "\n" ~ indent.stripLeft('\n'); 168 } 169 170 unittest 171 { 172 auto code = cast(ubyte[]) "void foo() {\n\tfoo();\n}"; 173 auto indent = getIndentation(code, 20); 174 assert(indent == "\n\t", '"' ~ indent ~ '"'); 175 176 code = cast(ubyte[]) "void foo() { foo(); }"; 177 indent = getIndentation(code, 19); 178 assert(indent == " ", '"' ~ indent ~ '"'); 179 180 code = cast(ubyte[]) "import a;\n\nvoid foo() {\n\tfoo();\n}"; 181 indent = getIndentation(code, 9); 182 assert(indent == "\n", '"' ~ indent ~ '"'); 183 } 184 185 class ImporterReaderVisitor : ASTVisitor 186 { 187 this(int pos) 188 { 189 this.pos = pos; 190 inBlock = false; 191 } 192 193 alias visit = ASTVisitor.visit; 194 195 override void visit(const ModuleDeclaration decl) 196 { 197 if (decl.endLocation + 1 < outerImportLocation || inBlock) 198 return; 199 isModule = true; 200 outerImportLocation = decl.endLocation + 1; 201 } 202 203 override void visit(const ImportDeclaration decl) 204 { 205 if (decl.startIndex >= pos) 206 return; 207 isModule = false; 208 if (inBlock) 209 innermostBlockStart = decl.endIndex; 210 else 211 outerImportLocation = decl.endIndex; 212 foreach (i; decl.singleImports) 213 imports ~= ImportInfo(i.identifierChain.identifiers.map!(tok => tok.text.idup) 214 .array, i.rename.text); 215 if (decl.importBindings) 216 { 217 ImportInfo info; 218 if (!decl.importBindings.singleImport) 219 return; 220 info.name = decl.importBindings.singleImport.identifierChain.identifiers.map!( 221 tok => tok.text.idup).array; 222 info.rename = decl.importBindings.singleImport.rename.text; 223 foreach (bind; decl.importBindings.importBinds) 224 { 225 if (bind.right.text) 226 info.selectives ~= SelectiveImport(bind.right.text, bind.left.text); 227 else 228 info.selectives ~= SelectiveImport(bind.left.text); 229 } 230 if (info.selectives.length) 231 imports ~= info; 232 } 233 } 234 235 override void visit(const BlockStatement content) 236 { 237 if (content && pos >= content.startLocation && pos < content.endLocation) 238 { 239 if (content.startLocation + 1 >= innermostBlockStart) 240 innermostBlockStart = content.startLocation + 1; 241 inBlock = true; 242 return content.accept(this); 243 } 244 } 245 246 private int pos; 247 private bool inBlock; 248 ImportInfo[] imports; 249 bool isModule; 250 size_t outerImportLocation; 251 size_t innermostBlockStart; 252 } 253 254 void doNothing(string, size_t, size_t, string, bool) 255 { 256 } 257 258 unittest 259 { 260 import std.conv; 261 262 start(); 263 auto imports = get("import std.stdio; void foo() { import fs = std.file; import std.algorithm : map, each2 = each; writeln(\"hi\"); } void bar() { import std.string; import std.regex : ctRegex; }", 264 81); 265 bool equalsImport(ImportInfo i, string s) 266 { 267 return i.name.join('.') == s; 268 } 269 270 void assertEquals(T)(T a, T b) 271 { 272 assert(a == b, "'" ~ a.to!string ~ "' != '" ~ b.to!string ~ "'"); 273 } 274 275 assertEquals(imports.length, 3); 276 assert(equalsImport(imports[0], "std.stdio")); 277 assert(equalsImport(imports[1], "std.file")); 278 assertEquals(imports[1].rename, "fs"); 279 assert(equalsImport(imports[2], "std.algorithm")); 280 assertEquals(imports[2].selectives.length, 2); 281 assertEquals(imports[2].selectives[0].name, "map"); 282 assertEquals(imports[2].selectives[1].name, "each"); 283 assertEquals(imports[2].selectives[1].rename, "each2"); 284 285 string code = "void foo() { import std.stdio : stderr; writeln(\"hi\"); }"; 286 auto mod = add("std.stdio", code, 45); 287 assertEquals(mod.rename, ""); 288 assertEquals(mod.replacements.length, 1); 289 assertEquals(mod.replacements[0].apply(code), 290 "void foo() { import std.stdio : stderr; import std.stdio; writeln(\"hi\"); }"); 291 292 code = "void foo() {\n\timport std.stdio : stderr;\n\twriteln(\"hi\");\n}"; 293 mod = add("std.stdio", code, 45); 294 assertEquals(mod.rename, ""); 295 assertEquals(mod.replacements.length, 1); 296 assertEquals(mod.replacements[0].apply(code), 297 "void foo() {\n\timport std.stdio : stderr;\n\timport std.stdio;\n\twriteln(\"hi\");\n}"); 298 299 code = "void foo() {\n\timport std.file : readText;\n\twriteln(\"hi\");\n}"; 300 mod = add("std.stdio", code, 45); 301 assertEquals(mod.rename, ""); 302 assertEquals(mod.replacements.length, 1); 303 assertEquals(mod.replacements[0].apply(code), 304 "import std.stdio;\nvoid foo() {\n\timport std.file : readText;\n\twriteln(\"hi\");\n}"); 305 306 code = "void foo() { import io = std.stdio; io.writeln(\"hi\"); }"; 307 mod = add("std.stdio", code, 45); 308 assertEquals(mod.rename, "io"); 309 assertEquals(mod.replacements.length, 0); 310 311 code = "import std.file : readText;\n\nvoid foo() {\n\twriteln(\"hi\");\n}"; 312 mod = add("std.stdio", code, 45); 313 assertEquals(mod.rename, ""); 314 assertEquals(mod.replacements.length, 1); 315 assertEquals(mod.replacements[0].apply(code), 316 "import std.file : readText;\nimport std.stdio;\n\nvoid foo() {\n\twriteln(\"hi\");\n}"); 317 318 code = "import std.file;\nimport std.regex;\n\nvoid foo() {\n\twriteln(\"hi\");\n}"; 319 mod = add("std.stdio", code, 54); 320 assertEquals(mod.rename, ""); 321 assertEquals(mod.replacements.length, 1); 322 assertEquals(mod.replacements[0].apply(code), 323 "import std.file;\nimport std.regex;\nimport std.stdio;\n\nvoid foo() {\n\twriteln(\"hi\");\n}"); 324 325 code = "module a;\n\nvoid foo() {\n\twriteln(\"hi\");\n}"; 326 mod = add("std.stdio", code, 30); 327 assertEquals(mod.rename, ""); 328 assertEquals(mod.replacements.length, 1); 329 assertEquals(mod.replacements[0].apply(code), 330 "module a;\n\nimport std.stdio;\n\nvoid foo() {\n\twriteln(\"hi\");\n}"); 331 332 stop(); 333 }