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 }