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.fromRaw("New source file"d), 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: "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: "Template description" } 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: "Module name" } 96 EditLine { id: edModuleName; text: ""; layoutWidth: fill; readOnly: true } 97 TextWidget { text: "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.fromRaw("DlangIDE files"d), "*.dlangidews;*.d;*.dd;*.di;*.ddoc;*.dh;*.json;*.xml;*.ini;*.dt")); 123 _edLocation.caption = "Select directory"d; 124 125 _edFileName.editorAction.connect(&onEditorAction); 126 _edFilePath.editorAction.connect(&onEditorAction); 127 _edModuleName.editorAction.connect(&onEditorAction); 128 _edLocation.editorAction.connect(&onEditorAction); 129 130 // fill templates 131 dstring[] names; 132 foreach(t; _templates) 133 names ~= t.name; 134 _projectTemplateList.items = names; 135 _projectTemplateList.selectedItemIndex = 0; 136 137 templateSelected(0); 138 139 // listeners 140 _edLocation.contentChange = delegate (EditableContent source) { 141 _location = toUTF8(source.text); 142 validate(); 143 }; 144 145 _edFileName.contentChange = delegate (EditableContent source) { 146 _fileName = toUTF8(source.text); 147 validate(); 148 }; 149 150 _projectTemplateList.itemSelected = delegate (Widget source, int itemIndex) { 151 templateSelected(itemIndex); 152 return true; 153 }; 154 _projectTemplateList.itemClick = delegate (Widget source, int itemIndex) { 155 templateSelected(itemIndex); 156 return true; 157 }; 158 159 addChild(content); 160 addChild(createButtonsPanel([ACTION_FILE_NEW_SOURCE_FILE, ACTION_CANCEL], 0, 0)); 161 162 } 163 164 /// called after window with dialog is shown 165 override void onShow() { 166 super.onShow(); 167 _edFileName.selectAll(); 168 _edFileName.setFocus(); 169 } 170 171 protected bool onEditorAction(const Action action) { 172 if (action.id == EditorActions.InsertNewLine) { 173 if (!validate()) 174 return false; 175 close(_buttonActions[0]); 176 return true; 177 } 178 return false; 179 } 180 181 StringListWidget _projectTemplateList; 182 EditBox _templateDescription; 183 DirEditLine _edLocation; 184 EditLine _edFileName; 185 EditLine _edModuleName; 186 EditLine _edFilePath; 187 TextWidget _statusText; 188 189 string _fileName = "newfile"; 190 string _location; 191 string _moduleName; 192 string _packageName; 193 string _fullPathName; 194 195 int _currentTemplateIndex = -1; 196 ProjectTemplate _currentTemplate; 197 ProjectTemplate[] _templates; 198 199 static bool isSubdirOf(string path, string basePath) { 200 if (path.equal(basePath)) 201 return true; 202 if (path.length > basePath.length + 1 && path.startsWith(basePath)) { 203 char ch = path[basePath.length]; 204 return ch == '/' || ch == '\\'; 205 } 206 return false; 207 } 208 209 bool findSource(string path, ref string sourceFolderPath, ref string relativePath) { 210 foreach(dir; _sourcePaths) { 211 if (isSubdirOf(path, dir)) { 212 sourceFolderPath = dir; 213 relativePath = path[sourceFolderPath.length .. $]; 214 if (relativePath.length > 0 && (relativePath[0] == '\\' || relativePath[0] == '/')) 215 relativePath = relativePath[1 .. $]; 216 return true; 217 } 218 } 219 return false; 220 } 221 222 bool setError(dstring msg) { 223 _statusText.text = msg; 224 return msg.empty; 225 } 226 227 bool validate() { 228 string filename = _fileName; 229 string fullFileName = filename; 230 if (!_currentTemplate.fileExtension.empty && filename.endsWith(_currentTemplate.fileExtension)) 231 filename = filename[0 .. $ - _currentTemplate.fileExtension.length]; 232 else 233 fullFileName = fullFileName ~ _currentTemplate.fileExtension; 234 _fullPathName = buildNormalizedPath(_location, fullFileName); 235 _edFilePath.text = toUTF32(_fullPathName); 236 if (!isValidFileName(filename)) 237 return setError("Invalid file name"); 238 if (!exists(_location) || !isDir(_location)) 239 return setError("Location directory does not exist"); 240 241 if (_currentTemplate.kind == FileKind.MODULE || _currentTemplate.kind == FileKind.PACKAGE) { 242 string sourcePath, relativePath; 243 if (!findSource(_location, sourcePath, relativePath)) 244 return setError("Location is outside of source path"); 245 if (!isValidModuleName(filename)) 246 return setError("Invalid file name"); 247 _moduleName = filename; 248 char[] buf; 249 foreach(c; relativePath) { 250 char ch = c; 251 if (ch == '/' || ch == '\\') 252 ch = '.'; 253 else if (ch == '.') 254 ch = '_'; 255 if (ch == '.' && (buf.length == 0 || buf[$-1] == '.')) 256 continue; // skip duplicate . 257 buf ~= ch; 258 } 259 if (buf.length && buf[$-1] == '.') 260 buf.length--; 261 _packageName = buf.dup; 262 string m; 263 if (_currentTemplate.kind == FileKind.MODULE) { 264 m = !_packageName.empty ? _packageName ~ '.' ~ _moduleName : _moduleName; 265 } else { 266 m = _packageName; 267 } 268 _edModuleName.text = toUTF32(m); 269 _packageName = m; 270 if (_currentTemplate.kind == FileKind.PACKAGE && _packageName.length == 0) 271 return setError("Package should be located in subdirectory"); 272 } else { 273 string projectPath = _project.dir; 274 if (!isSubdirOf(_location, projectPath)) 275 return setError("Location is outside of project path"); 276 _edModuleName.text = ""; 277 _moduleName = ""; 278 _packageName = ""; 279 } 280 return true; 281 } 282 283 private FileCreationResult _result; 284 bool createItem() { 285 try { 286 if (_currentTemplate.kind == FileKind.MODULE) { 287 string txt = "module " ~ _packageName ~ ";\n\n" ~ _currentTemplate.srccode; 288 write(_fullPathName, txt); 289 } else if (_currentTemplate.kind == FileKind.PACKAGE) { 290 string txt = "module " ~ _packageName ~ ";\n\n" ~ _currentTemplate.srccode; 291 write(_fullPathName, txt); 292 } else { 293 write(_fullPathName, _currentTemplate.srccode); 294 } 295 } catch (Exception e) { 296 Log.e("Cannot create file", e); 297 return setError("Cannot create file"); 298 } 299 _result = new FileCreationResult(_project, _fullPathName); 300 return true; 301 } 302 303 override void close(const Action action) { 304 Action newaction = action.clone(); 305 if (action.id == IDEActions.FileNew) { 306 if (!validate()) { 307 window.showMessageBox(UIString.fromRaw("Error"d), UIString.fromRaw("Invalid parameters")); 308 return; 309 } 310 if (!createItem()) { 311 window.showMessageBox(UIString.fromRaw("Error"d), UIString.fromRaw("Failed to create project item")); 312 return; 313 } 314 newaction.objectParam = _result; 315 } 316 super.close(newaction); 317 } 318 319 protected void templateSelected(int index) { 320 if (_currentTemplateIndex == index) 321 return; 322 _currentTemplateIndex = index; 323 _currentTemplate = _templates[index]; 324 _templateDescription.text = _currentTemplate.description; 325 if (_currentTemplate.kind == FileKind.PACKAGE) { 326 _edFileName.enabled = false; 327 _edFileName.text = "package"d; 328 } else { 329 if (_edFileName.text == "package") 330 _edFileName.text = "newfile"; 331 _edFileName.enabled = true; 332 } 333 //updateDirLayout(); 334 validate(); 335 } 336 337 void initTemplates() { 338 _templates ~= new ProjectTemplate("Empty module"d, "Empty D module file."d, ".d", 339 "\n", FileKind.MODULE); 340 _templates ~= new ProjectTemplate("Package"d, "D package."d, ".d", 341 "\n", FileKind.PACKAGE); 342 _templates ~= new ProjectTemplate("Text file"d, "Empty text file."d, ".txt", 343 "\n", FileKind.TEXT); 344 _templates ~= new ProjectTemplate("JSON file"d, "Empty json file."d, ".json", 345 "{\n}\n", FileKind.TEXT); 346 _templates ~= new ProjectTemplate("Vibe-D Diet Template file"d, "Empty Vibe-D Diet Template."d, ".dt", 347 q{ 348 doctype html 349 html 350 head 351 title Hello, World 352 body 353 h1 Hello World 354 }, FileKind.TEXT); 355 } 356 } 357 358 enum FileKind { 359 MODULE, 360 PACKAGE, 361 TEXT, 362 } 363 364 class ProjectTemplate { 365 dstring name; 366 dstring description; 367 string fileExtension; 368 string srccode; 369 FileKind kind; 370 this(dstring name, dstring description, string fileExtension, string srccode, FileKind kind) { 371 this.name = name; 372 this.description = description; 373 this.fileExtension = fileExtension; 374 this.srccode = srccode; 375 this.kind = kind; 376 } 377 }