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: #FF0000 } 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 static bool isSubdirOf(string path, string basePath) { 202 if (path.equal(basePath)) 203 return true; 204 if (path.length > basePath.length + 1 && path.startsWith(basePath)) { 205 char ch = path[basePath.length]; 206 return ch == '/' || ch == '\\'; 207 } 208 return false; 209 } 210 211 bool findSource(string path, ref string sourceFolderPath, ref string relativePath) { 212 foreach(dir; _sourcePaths) { 213 if (isSubdirOf(path, dir)) { 214 sourceFolderPath = dir; 215 relativePath = path[sourceFolderPath.length .. $]; 216 if (relativePath.length > 0 && (relativePath[0] == '\\' || relativePath[0] == '/')) 217 relativePath = relativePath[1 .. $]; 218 return true; 219 } 220 } 221 return false; 222 } 223 224 bool setError(dstring msg) { 225 _statusText.text = msg; 226 return msg.empty; 227 } 228 229 bool validate() { 230 string filename = _fileName; 231 string fullFileName = filename; 232 if (!_currentTemplate.fileExtension.empty && filename.endsWith(_currentTemplate.fileExtension)) 233 filename = filename[0 .. $ - _currentTemplate.fileExtension.length]; 234 else 235 fullFileName = fullFileName ~ _currentTemplate.fileExtension; 236 _fullPathName = buildNormalizedPath(_location, fullFileName); 237 _edFilePath.text = toUTF32(_fullPathName); 238 if (!isValidFileName(filename)) 239 return setError("Invalid file name"); 240 if (!exists(_location) || !isDir(_location)) 241 return setError("Location directory does not exist"); 242 243 if (_currentTemplate.kind == FileKind.MODULE || _currentTemplate.kind == FileKind.PACKAGE) { 244 string sourcePath, relativePath; 245 if (!findSource(_location, sourcePath, relativePath)) 246 return setError("Location is outside of source path"); 247 if (!isValidModuleName(filename)) 248 return setError("Invalid file name"); 249 _moduleName = filename; 250 char[] buf; 251 foreach(c; relativePath) { 252 char ch = c; 253 if (ch == '/' || ch == '\\') 254 ch = '.'; 255 else if (ch == '.') 256 ch = '_'; 257 if (ch == '.' && (buf.length == 0 || buf[$-1] == '.')) 258 continue; // skip duplicate . 259 buf ~= ch; 260 } 261 if (buf.length && buf[$-1] == '.') 262 buf.length--; 263 _packageName = buf.dup; 264 string m; 265 if (_currentTemplate.kind == FileKind.MODULE) { 266 m = !_packageName.empty ? _packageName ~ '.' ~ _moduleName : _moduleName; 267 } else { 268 m = _packageName; 269 } 270 _edModuleName.text = toUTF32(m); 271 _packageName = m; 272 if (_currentTemplate.kind == FileKind.PACKAGE && _packageName.length == 0) 273 return setError("Package should be located in subdirectory"); 274 } else { 275 string projectPath = _project.dir; 276 if (!isSubdirOf(_location, projectPath)) 277 return setError("Location is outside of project path"); 278 _edModuleName.text = ""; 279 _moduleName = ""; 280 _packageName = ""; 281 } 282 return true; 283 } 284 285 private FileCreationResult _result; 286 bool createItem() { 287 try { 288 if (_currentTemplate.kind == FileKind.MODULE) { 289 string txt = "module " ~ _packageName ~ ";\n\n" ~ _currentTemplate.srccode; 290 write(_fullPathName, txt); 291 } else if (_currentTemplate.kind == FileKind.PACKAGE) { 292 string txt = "module " ~ _packageName ~ ";\n\n" ~ _currentTemplate.srccode; 293 write(_fullPathName, txt); 294 } else { 295 write(_fullPathName, _currentTemplate.srccode); 296 } 297 } catch (Exception e) { 298 Log.e("Cannot create file", e); 299 return setError("Cannot create file"); 300 } 301 _result = new FileCreationResult(_project, _fullPathName); 302 return true; 303 } 304 305 override void close(const Action action) { 306 Action newaction = action.clone(); 307 if (action.id == IDEActions.FileNew) { 308 if (!validate()) { 309 window.showMessageBox(UIString.fromId("ERROR"c), UIString.fromId("ERROR_INVALID_PARAMETERS"c)); 310 return; 311 } 312 if (!createItem()) { 313 window.showMessageBox(UIString.fromId("ERROR"c), UIString.fromId("ERROR_INVALID_PARAMETERS"c)); 314 return; 315 } 316 newaction.objectParam = _result; 317 } 318 super.close(newaction); 319 } 320 321 protected void templateSelected(int index) { 322 if (_currentTemplateIndex == index) 323 return; 324 _currentTemplateIndex = index; 325 _currentTemplate = _templates[index]; 326 _templateDescription.text = _currentTemplate.description; 327 if (_currentTemplate.kind == FileKind.PACKAGE) { 328 _edFileName.enabled = false; 329 _edFileName.text = "package"d; 330 } else { 331 if (_edFileName.text == "package") 332 _edFileName.text = "newfile"; 333 _edFileName.enabled = true; 334 } 335 //updateDirLayout(); 336 validate(); 337 } 338 339 void initTemplates() { 340 _templates ~= new ProjectTemplate("Empty module"d, "Empty D module file."d, ".d", 341 "\n", FileKind.MODULE); 342 _templates ~= new ProjectTemplate("Package"d, "D package."d, ".d", 343 "\n", FileKind.PACKAGE); 344 _templates ~= new ProjectTemplate("Text file"d, "Empty text file."d, ".txt", 345 "\n", FileKind.TEXT); 346 _templates ~= new ProjectTemplate("JSON file"d, "Empty json file."d, ".json", 347 "{\n}\n", FileKind.TEXT); 348 _templates ~= new ProjectTemplate("Vibe-D Diet Template file"d, "Empty Vibe-D Diet Template."d, ".dt", 349 q{ 350 doctype html 351 html 352 head 353 title Hello, World 354 body 355 h1 Hello World 356 }, FileKind.TEXT); 357 } 358 } 359 360 enum FileKind { 361 MODULE, 362 PACKAGE, 363 TEXT, 364 } 365 366 class ProjectTemplate { 367 dstring name; 368 dstring description; 369 string fileExtension; 370 string srccode; 371 FileKind kind; 372 this(dstring name, dstring description, string fileExtension, string srccode, FileKind kind) { 373 this.name = name; 374 this.description = description; 375 this.fileExtension = fileExtension; 376 this.srccode = srccode; 377 this.kind = kind; 378 } 379 }