1 module served.workers.rename_listener; 2 3 import std.algorithm; 4 import std.array; 5 import std.datetime; 6 import std.experimental.logger; 7 import std.path; 8 import std.string; 9 10 import served.extension; 11 import served.types; 12 import served.utils.translate; 13 14 import workspaced.com.moduleman : ModulemanComponent; 15 16 /// Helper struct which contains information about a recently opened and/or created file 17 struct FileOpenInfo 18 { 19 /// When the file first was accessed recently 20 SysTime at; 21 /// The uri of the file 22 DocumentUri uri; 23 /// Whether the file has only been created, opened or both 24 bool created, opened; 25 26 /// Returns: true when the FileOpenInfo should no longer be used (after 5 seconds) 27 bool expired(SysTime now) const 28 { 29 return at == SysTime.init || (now - at) > 5.seconds; 30 } 31 32 /// Sets created to true and triggers onFullyCreate if it is also opened 33 void setCreated() 34 { 35 created = true; 36 if (opened) 37 onFullyCreate(uri); 38 } 39 40 /// Sets opened to true and triggers onFullyCreate if it is also created 41 void setOpened() 42 { 43 opened = true; 44 if (created) 45 onFullyCreate(uri); 46 } 47 } 48 49 /// Helper struct to keep track of recently opened & created files 50 struct RecentFiles 51 { 52 /// the last 8 files 53 FileOpenInfo[8] infos; 54 55 /// Returns a reference to some initialized FileOpenInfo with this uri 56 ref FileOpenInfo get(DocumentUri uri) return 57 { 58 auto now = Clock.currTime; 59 60 // find existing one 61 foreach (ref info; infos) 62 { 63 if (info.uri == uri) 64 { 65 if (info.expired(now)) 66 { 67 info.created = false; 68 info.opened = false; 69 } 70 return info; 71 } 72 } 73 74 // replace old one 75 size_t min; 76 SysTime minTime = now; 77 foreach (i, ref info; infos) 78 { 79 if (info.at < minTime) 80 { 81 minTime = info.at; 82 min = i; 83 } 84 } 85 86 infos[min].at = now; 87 infos[min].created = infos[min].opened = false; 88 infos[min].uri = uri; 89 return infos[min]; 90 } 91 } 92 93 package __gshared RecentFiles recentFiles; 94 95 @protocolNotification("workspace/didChangeWatchedFiles") 96 void markRecentlyChangedFile(DidChangeWatchedFilesParams params) 97 { 98 foreach (change; params.changes) 99 if (change.type == FileChangeType.created) 100 markRecentFileCreated(change.uri); 101 } 102 103 void markRecentFileCreated(DocumentUri uri) 104 { 105 recentFiles.get(uri).setCreated(); 106 } 107 108 @protocolNotification("textDocument/didOpen") 109 void markRecentFileOpened(DidOpenTextDocumentParams params) 110 { 111 recentFiles.get(params.textDocument.uri).setOpened(); 112 } 113 114 /// Called when a file has been created or renamed and opened within a short time frame by the user 115 /// Indicating it was created to be edited in the IDE 116 void onFullyCreate(DocumentUri uri) 117 { 118 trace("handle file creation/rename for ", uri); 119 if (uri.endsWith(".d")) 120 return onFullyCreateDSource(uri); 121 122 auto file = baseName(uri); 123 if (file == "dub.json") 124 onFullyCreateDubJson(uri); 125 else if (file == "dub.sdl") 126 onFullyCreateDubSdl(uri); 127 } 128 129 void onFullyCreateDSource(DocumentUri uri) 130 { 131 auto fileConfig = config(uri); 132 if (!fileConfig.d.generateModuleNames) 133 return; 134 135 string workspace = workspaceRootFor(uri); 136 auto document = documents[uri]; 137 // Sending applyEdit so it is undoable 138 auto patches = backend.get!ModulemanComponent(workspace) 139 .normalizeModules(uri.uriToFile, document.rawText); 140 if (patches.length) 141 { 142 WorkspaceEdit edit; 143 edit.changes[uri] = patches.map!(a => TextEdit(TextRange(document.bytesToPosition(a.range[0]), 144 document.bytesToPosition(a.range[1])), a.content)).array; 145 ApplyWorkspaceEditParams params = { edit: edit }; 146 rpc.sendMethod("workspace/applyEdit", params); 147 rpc.window.showInformationMessage(translate!"d.served.moduleNameAutoUpdated"); 148 } 149 } 150 151 void onFullyCreateDubJson(DocumentUri uri) 152 { 153 auto document = documents[uri]; 154 if (document.rawText.strip.length == 0) 155 { 156 string packageName = determineDubPackageName(uri.uriToFile.dirName); 157 WorkspaceEdit edit; 158 edit.changes[uri] = [ 159 TextEdit(TextRange(0, 0, 0, 0), "{\n\t\"name\": \"" ~ packageName ~ "\"\n}") 160 ]; 161 ApplyWorkspaceEditParams params = { edit: edit }; 162 rpc.sendMethod("workspace/applyEdit", params); 163 } 164 } 165 166 void onFullyCreateDubSdl(DocumentUri uri) 167 { 168 auto document = documents[uri]; 169 if (document.rawText.strip.length == 0) 170 { 171 string packageName = determineDubPackageName(uri.uriToFile.dirName); 172 WorkspaceEdit edit; 173 edit.changes[uri] = [ 174 TextEdit(TextRange(0, 0, 0, 0), `name "` ~ packageName ~ `"` ~ '\n') 175 ]; 176 ApplyWorkspaceEditParams params = { edit: edit }; 177 rpc.sendMethod("workspace/applyEdit", params); 178 } 179 } 180 181 /// Generates a package name for a given folder path 182 string determineDubPackageName(string directory) 183 { 184 import std.ascii : toLower, isUpper, isAlphaNum; 185 186 auto name = baseName(directory); 187 if (!name.length) 188 return ""; 189 190 auto ret = appender!string; 191 ret.put(toLower(name[0])); 192 bool wasUpper = name[0].isUpper; 193 bool wasDash = false; 194 foreach (char c; name[1 .. $]) 195 { 196 if (!isAlphaNum(c) && c != '_') 197 c = '-'; 198 199 if (wasDash && c == '-') 200 continue; 201 wasDash = c == '-'; 202 203 if (c.isUpper) 204 { 205 if (!wasUpper) 206 { 207 ret.put('-'); 208 ret.put(c.toLower); 209 } 210 wasUpper = true; 211 } 212 else 213 { 214 wasUpper = false; 215 ret.put(c); 216 } 217 } 218 auto packageName = ret.data; 219 while (packageName.startsWith("-")) 220 packageName = packageName[1 .. $]; 221 while (packageName.endsWith("-")) 222 packageName = packageName[0 .. $ - 1]; 223 return packageName; 224 }