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 }