1 module dlangide.ui.newfile; 2 3 import dlangui.core.types; 4 import dlangui.core.i18n; 5 import dlangui.platforms.common.platform; 6 import dlangui.dialogs.dialog; 7 import dlangui.dialogs.filedlg; 8 import dlangui.widgets.widget; 9 import dlangui.widgets.layouts; 10 import dlangui.widgets.editors; 11 import dlangui.widgets.controls; 12 import dlangui.widgets.lists; 13 import dlangui.dml.parser; 14 import dlangui.core.stdaction; 15 import dlangui.core.files; 16 import dlangide.workspace.project; 17 import dlangide.workspace.workspace; 18 import dlangide.ui.commands; 19 import dlangide.ui.frame; 20 21 import std.algorithm : startsWith, endsWith, equal; 22 import std.array : empty; 23 import std.utf : toUTF32; 24 import std.file; 25 import std.path; 26 27 class FileCreationResult { 28 Project project; 29 string filename; 30 this(Project project, string filename) { 31 this.project = project; 32 this.filename = filename; 33 } 34 } 35 36 class NewFileDlg : Dialog { 37 IDEFrame _ide; 38 Project _project; 39 ProjectFolder _folder; 40 string[] _sourcePaths; 41 this(IDEFrame parent, Project currentProject, ProjectFolder folder) { 42 super(UIString.fromId("OPTION_NEW_SOURCE_FILE"c), parent.window, 43 DialogFlag.Modal | DialogFlag.Resizable | DialogFlag.Popup, 500, 400); 44 _ide = parent; 45 _icon = "dlangui-logo1"; 46 this._project = currentProject; 47 this._folder = folder; 48 _location = folder ? folder.filename : currentProject.dir; 49 _sourcePaths = currentProject.sourcePaths; 50 if (_sourcePaths.length) 51 _location = _sourcePaths[0]; 52 if (folder) 53 _location = folder.filename; 54 } 55 /// override to implement creation of dialog controls 56 override void initialize() { 57 super.initialize(); 58 initTemplates(); 59 Widget content; 60 try { 61 content = parseML(q{ 62 VerticalLayout { 63 id: vlayout 64 padding: Rect { 5, 5, 5, 5 } 65 layoutWidth: fill; layoutHeight: fill 66 HorizontalLayout { 67 layoutWidth: fill; layoutHeight: fill 68 VerticalLayout { 69 margins: 5 70 layoutWidth: 50%; layoutHeight: fill 71 TextWidget { text: OPTION_PROJECT_TEMPLATE } 72 StringListWidget { 73 id: projectTemplateList 74 layoutWidth: wrap; layoutHeight: fill 75 } 76 } 77 VerticalLayout { 78 margins: 5 79 layoutWidth: 50%; layoutHeight: fill 80 TextWidget { text: OPTION_TEMPLATE_DESCR } 81 EditBox { 82 id: templateDescription; readOnly: true 83 layoutWidth: fill; layoutHeight: fill 84 } 85 } 86 } 87 TableLayout { 88 margins: 5 89 colCount: 2 90 layoutWidth: fill; layoutHeight: wrap 91 TextWidget { text: NAME } 92 EditLine { id: edName; text: "newfile"; layoutWidth: fill } 93 TextWidget { text: LOCATION } 94 DirEditLine { id: edLocation; layoutWidth: fill } 95 TextWidget { text: OPTION_MODULE_NAME } 96 EditLine { id: edModuleName; text: ""; layoutWidth: fill; readOnly: true } 97 TextWidget { text: OPTION_FILE_PATH } 98 EditLine { id: edFilePath; text: ""; layoutWidth: fill; readOnly: true } 99 } 100 TextWidget { id: statusText; text: ""; layoutWidth: fill; textColor: 0xFF0000 } 101 } 102 }); 103 } catch (Exception e) { 104 Log.e("Exceptin while parsing DML", e); 105 throw e; 106 } 107 108 109 _projectTemplateList = content.childById!StringListWidget("projectTemplateList"); 110 _templateDescription = content.childById!EditBox("templateDescription"); 111 _edFileName = content.childById!EditLine("edName"); 112 _edFilePath = content.childById!EditLine("edFilePath"); 113 _edModuleName = content.childById!EditLine("edModuleName"); 114 _edLocation = content.childById!DirEditLine("edLocation"); 115 _edLocation.text = toUTF32(_location); 116 _statusText = content.childById!TextWidget("statusText"); 117 118 _edLocation.filetypeIcons[".d"] = "text-d"; 119 _edLocation.filetypeIcons["dub.json"] = "project-d"; 120 _edLocation.filetypeIcons["package.json"] = "project-d"; 121 _edLocation.filetypeIcons[".dlangidews"] = "project-development"; 122 _edLocation.addFilter(FileFilterEntry(UIString.fromId("IDE_FILES"c), "*.dlangidews;*.d;*.dd;*.di;*.ddoc;*.dh;*.json;*.xml;*.ini;*.dt")); 123 _edLocation.caption = "Select directory"d; 124 125 _edFileName.enterKey.connect(&onEnterKey); 126 _edFilePath.enterKey.connect(&onEnterKey); 127 _edModuleName.enterKey.connect(&onEnterKey); 128 _edLocation.enterKey.connect(&onEnterKey); 129 130 _edFileName.setDefaultPopupMenu(); 131 _edFilePath.setDefaultPopupMenu(); 132 _edModuleName.setDefaultPopupMenu(); 133 _edLocation.setDefaultPopupMenu(); 134 135 // fill templates 136 dstring[] names; 137 foreach(t; _templates) 138 names ~= t.name; 139 _projectTemplateList.items = names; 140 _projectTemplateList.selectedItemIndex = 0; 141 142 templateSelected(0); 143 144 // listeners 145 _edLocation.contentChange = delegate (EditableContent source) { 146 _location = toUTF8(source.text); 147 validate(); 148 }; 149 150 _edFileName.contentChange = delegate (EditableContent source) { 151 _fileName = toUTF8(source.text); 152 validate(); 153 }; 154 155 _projectTemplateList.itemSelected = delegate (Widget source, int itemIndex) { 156 templateSelected(itemIndex); 157 return true; 158 }; 159 _projectTemplateList.itemClick = delegate (Widget source, int itemIndex) { 160 templateSelected(itemIndex); 161 return true; 162 }; 163 164 addChild(content); 165 addChild(createButtonsPanel([ACTION_FILE_NEW_SOURCE_FILE, ACTION_CANCEL], 0, 0)); 166 167 } 168 169 /// called after window with dialog is shown 170 override void onShow() { 171 super.onShow(); 172 _edFileName.selectAll(); 173 _edFileName.setFocus(); 174 } 175 176 protected bool onEnterKey(EditWidgetBase editor) { 177 if (!validate()) 178 return false; 179 close(_buttonActions[0]); 180 return true; 181 } 182 183 StringListWidget _projectTemplateList; 184 EditBox _templateDescription; 185 DirEditLine _edLocation; 186 EditLine _edFileName; 187 EditLine _edModuleName; 188 EditLine _edFilePath; 189 TextWidget _statusText; 190 191 string _fileName = "newfile"; 192 string _location; 193 string _moduleName; 194 string _packageName; 195 string _fullPathName; 196 197 int _currentTemplateIndex = -1; 198 ProjectTemplate _currentTemplate; 199 ProjectTemplate[] _templates; 200 201 bool setError(dstring msg) { 202 _statusText.text = msg; 203 return msg.empty; 204 } 205 206 bool validate() { 207 string filename = _fileName; 208 string fullFileName = filename; 209 if (!_currentTemplate.fileExtension.empty && filename.endsWith(_currentTemplate.fileExtension)) 210 filename = filename[0 .. $ - _currentTemplate.fileExtension.length]; 211 else 212 fullFileName = fullFileName ~ _currentTemplate.fileExtension; 213 _fullPathName = buildNormalizedPath(_location, fullFileName); 214 _edFilePath.text = toUTF32(_fullPathName); 215 if (!isValidFileName(filename)) 216 return setError("Invalid file name"); 217 if (!exists(_location) || !isDir(_location)) 218 return setError("Location directory does not exist"); 219 220 if (_currentTemplate.kind == FileKind.MODULE || _currentTemplate.kind == FileKind.PACKAGE) { 221 string sourcePath, relativePath; 222 if (!findSource(_sourcePaths, _location, sourcePath, relativePath)) 223 return setError("Location is outside of source path"); 224 if (!isValidModuleName(filename)) 225 return setError("Invalid file name"); 226 _moduleName = filename; 227 _packageName = getPackageName(sourcePath, relativePath); 228 string m; 229 if (_currentTemplate.kind == FileKind.MODULE) { 230 m = !_packageName.empty ? _packageName ~ '.' ~ _moduleName : _moduleName; 231 } else { 232 m = _packageName; 233 } 234 _edModuleName.text = toUTF32(m); 235 _packageName = m; 236 if (_currentTemplate.kind == FileKind.PACKAGE && _packageName.length == 0) 237 return setError("Package should be located in subdirectory"); 238 } else { 239 string projectPath = _project.dir; 240 if (!isSubdirOf(_location, projectPath)) 241 return setError("Location is outside of project path"); 242 _edModuleName.text = ""; 243 _moduleName = ""; 244 _packageName = ""; 245 } 246 return true; 247 } 248 249 private FileCreationResult _result; 250 bool createItem() { 251 if(!createFile(_fullPathName, _currentTemplate.kind, _packageName, _currentTemplate.srccode)) { 252 return setError("Cannot create file"); 253 } 254 255 _result = new FileCreationResult(_project, _fullPathName); 256 return true; 257 } 258 259 override void close(const Action action) { 260 Action newaction = action.clone(); 261 if (action.id == IDEActions.FileNew) { 262 if (!validate()) { 263 window.showMessageBox(UIString.fromId("ERROR"c), UIString.fromId("ERROR_INVALID_PARAMETERS"c)); 264 return; 265 } 266 if (!createItem()) { 267 window.showMessageBox(UIString.fromId("ERROR"c), UIString.fromId("ERROR_INVALID_PARAMETERS"c)); 268 return; 269 } 270 newaction.objectParam = _result; 271 } 272 super.close(newaction); 273 } 274 275 protected void templateSelected(int index) { 276 if (_currentTemplateIndex == index) 277 return; 278 _currentTemplateIndex = index; 279 _currentTemplate = _templates[index]; 280 _templateDescription.text = _currentTemplate.description; 281 if (_currentTemplate.kind == FileKind.PACKAGE) { 282 _edFileName.enabled = false; 283 _edFileName.text = "package"d; 284 } else { 285 if (_edFileName.text == "package") 286 _edFileName.text = "newfile"; 287 _edFileName.enabled = true; 288 } 289 //updateDirLayout(); 290 validate(); 291 } 292 293 void initTemplates() { 294 _templates ~= new ProjectTemplate("Empty module"d, "Empty D module file."d, ".d", 295 "\n", FileKind.MODULE); 296 _templates ~= new ProjectTemplate("Package"d, "D package."d, ".d", 297 "\n", FileKind.PACKAGE); 298 _templates ~= new ProjectTemplate("Text file"d, "Empty text file."d, ".txt", 299 "\n", FileKind.TEXT); 300 _templates ~= new ProjectTemplate("JSON file"d, "Empty json file."d, ".json", 301 "{\n}\n", FileKind.TEXT); 302 _templates ~= new ProjectTemplate("Vibe-D Diet Template file"d, "Empty Vibe-D Diet Template."d, ".dt", 303 q{ 304 doctype html 305 html 306 head 307 title Hello, World 308 body 309 h1 Hello World 310 }, FileKind.TEXT); 311 } 312 } 313 314 enum FileKind { 315 MODULE, 316 PACKAGE, 317 TEXT, 318 } 319 320 class ProjectTemplate { 321 dstring name; 322 dstring description; 323 string fileExtension; 324 string srccode; 325 FileKind kind; 326 this(dstring name, dstring description, string fileExtension, string srccode, FileKind kind) { 327 this.name = name; 328 this.description = description; 329 this.fileExtension = fileExtension; 330 this.srccode = srccode; 331 this.kind = kind; 332 } 333 } 334 335 bool createFile(string fullPathName, FileKind fileKind, string packageName, string sourceCode) { 336 try { 337 if (fileKind == FileKind.MODULE) { 338 string txt = "module " ~ packageName ~ ";\n\n" ~ sourceCode; 339 write(fullPathName, txt); 340 } else if (fileKind == FileKind.PACKAGE) { 341 string txt = "module " ~ packageName ~ ";\n\n" ~ sourceCode; 342 write(fullPathName, txt); 343 } else { 344 write(fullPathName, sourceCode); 345 } 346 return true; 347 } 348 catch(Exception e) { 349 Log.e("Cannot create file", e); 350 return false; 351 } 352 } 353 354 string getPackageName(string path, string[] sourcePaths){ 355 string sourcePath, relativePath; 356 if(!findSource(sourcePaths, path, sourcePath, relativePath)) return ""; 357 return getPackageName(sourcePath, relativePath); 358 } 359 360 string getPackageName(string sourcePath, string relativePath){ 361 362 char[] buf; 363 foreach(c; relativePath) { 364 char ch = c; 365 if (ch == '/' || ch == '\\') 366 ch = '.'; 367 else if (ch == '.') 368 ch = '_'; 369 if (ch == '.' && (buf.length == 0 || buf[$-1] == '.')) 370 continue; // skip duplicate . 371 buf ~= ch; 372 } 373 if (buf.length && buf[$-1] == '.') 374 buf.length--; 375 return buf.dup; 376 } 377 private bool findSource(string[] sourcePaths, string path, ref string sourceFolderPath, ref string relativePath) { 378 foreach(dir; sourcePaths) { 379 if (isSubdirOf(path, dir)) { 380 sourceFolderPath = dir; 381 relativePath = path[sourceFolderPath.length .. $]; 382 if (relativePath.length > 0 && (relativePath[0] == '\\' || relativePath[0] == '/')) 383 relativePath = relativePath[1 .. $]; 384 return true; 385 } 386 } 387 return false; 388 } 389 private bool isSubdirOf(string path, string basePath) { 390 if (path.equal(basePath)) 391 return true; 392 if (path.length > basePath.length + 1 && path.startsWith(basePath)) { 393 char ch = path[basePath.length]; 394 return ch == '/' || ch == '\\'; 395 } 396 return false; 397 }