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 }