1 module dlangide.workspace.workspace;
2 
3 import dlangide.workspace.project;
4 import dlangide.workspace.workspacesettings;
5 import dlangide.ui.frame;
6 import dlangui.core.logger;
7 import dlangui.core.settings;
8 import std.algorithm : map, equal, endsWith;
9 import std.array : empty;
10 import std.conv;
11 import std.file;
12 import std.path;
13 import std.range : array;
14 import std.utf;
15 
16 import ddebug.common.debugger;
17 
18 enum BuildOperation {
19     Build,
20     Clean,
21     Rebuild,
22     Run,
23     Upgrade
24 }
25 
26 enum BuildConfiguration {
27     Debug,
28     Release,
29     Unittest
30 }
31 
32 
33 /**
34     Exception thrown on Workspace errors
35 */
36 class WorkspaceException : Exception
37 {
38     this(string msg, string file = __FILE__, size_t line = __LINE__)
39     {
40         super(msg, file, line);
41     }
42 }
43 
44 immutable string WORKSPACE_EXTENSION = ".dlangidews";
45 immutable string WORKSPACE_SETTINGS_EXTENSION = ".wssettings";
46 
47 /// return true if filename matches rules for workspace file names
48 bool isWorkspaceFile(string filename) {
49     return filename.endsWith(WORKSPACE_EXTENSION);
50 }
51 
52 /// DlangIDE workspace
53 class Workspace : WorkspaceItem {
54     protected Project[] _projects;
55     protected SettingsFile _workspaceFile;
56     protected WorkspaceSettings _settings;
57     
58     protected IDEFrame _frame;
59     
60     this(IDEFrame frame, string fname = WORKSPACE_EXTENSION) {
61         super(fname);
62         _workspaceFile = new SettingsFile(fname);
63         _settings = new WorkspaceSettings(fname ? fname ~ WORKSPACE_SETTINGS_EXTENSION : null);
64         _frame = frame;
65     }
66 
67     ProjectSourceFile findSourceFile(string projectFileName, string fullFileName) {
68         foreach(p; _projects) {
69             ProjectSourceFile res = p.findSourceFile(projectFileName, fullFileName);
70             if (res)
71                 return res;
72         }
73         return null;
74     }
75 
76     @property Setting includePath(){
77         Setting res = _workspaceFile.objectByPath("includePath", true);
78         return res;
79     }    
80 
81     @property Project[] projects() { return _projects; }
82 
83     @property BuildConfiguration buildConfiguration() {
84         return cast(BuildConfiguration)_settings.buildConfiguration;
85     }
86 
87     @property void buildConfiguration(BuildConfiguration config) {
88         _settings.buildConfiguration = cast(int)config;
89     }
90 
91     protected Project _startupProject;
92 
93     @property Project startupProject() { return _startupProject; }
94     @property void startupProject(Project project) { 
95         if (_startupProject is project)
96             return;
97         _startupProject = project;
98         _settings.startupProjectName = toUTF8(project.name);
99         _frame.updateProjectConfigurations();
100     }
101     
102     /// Last opened files in workspace
103     @property WorkspaceFile[] files() {
104         return _settings.files();
105     }
106     
107     /// Last opened files in workspace
108     @property void files(WorkspaceFile[] fs) {
109         _settings.files(fs);
110     }
111 
112     /// read list of expanded items from settings
113     @property string[] expandedItems() {
114         return _settings.expandedItems();
115     }
116 
117     /// update list of expanded items in settings
118     @property void expandedItems(string[] items) {
119         _settings.expandedItems(items);
120     }
121 
122     /// last selected workspace item in workspace explorer
123     @property string selectedWorkspaceItem() {
124         return _settings.selectedWorkspaceItem;
125     }
126 
127     /// update last selected workspace item in workspace explorer
128     @property void selectedWorkspaceItem(string item) {
129         if (_settings.selectedWorkspaceItem != item)
130             _settings.selectedWorkspaceItem = item;
131     }
132 
133     /// setups currrent project configuration by name
134     void setStartupProjectConfiguration(string conf)
135     {
136         if(_startupProject) {
137             _startupProject.projectConfiguration = conf;
138         }
139     }
140 
141     private void updateBreakpointFiles(Breakpoint[] breakpoints) {
142         foreach(bp; breakpoints) {
143             Project project = findProjectByName(bp.projectName);
144             if (project)
145                 bp.fullFilePath = project.relativeToAbsolutePath(bp.projectFilePath);
146         }
147     }
148 
149     private void updateBookmarkFiles(EditorBookmark[] bookmarks) {
150         foreach(bp; bookmarks) {
151             Project project = findProjectByName(bp.projectName);
152             if (project)
153                 bp.fullFilePath = project.relativeToAbsolutePath(bp.projectFilePath);
154         }
155     }
156 
157     Breakpoint[] getSourceFileBreakpoints(ProjectSourceFile file) {
158         Breakpoint[] res = _settings.getProjectBreakpoints(toUTF8(file.project.name), file.projectFilePath);
159         updateBreakpointFiles(res);
160         return res;
161     }
162     
163     void setSourceFileBreakpoints(ProjectSourceFile file, Breakpoint[] breakpoints) {
164         _settings.setProjectBreakpoints(toUTF8(file.project.name), file.projectFilePath, breakpoints);
165     }
166 
167     EditorBookmark[] getSourceFileBookmarks(ProjectSourceFile file) {
168         EditorBookmark[] res = _settings.getProjectBookmarks(toUTF8(file.project.name), file.projectFilePath);
169         updateBookmarkFiles(res);
170         return res;
171     }
172     
173     void setSourceFileBookmarks(ProjectSourceFile file, EditorBookmark[] bookmarks) {
174         _settings.setProjectBookmarks(toUTF8(file.project.name), file.projectFilePath, bookmarks);
175     }
176 
177     /// returns all workspace breakpoints
178     Breakpoint[] getBreakpoints() {
179         Breakpoint[] res = _settings.getBreakpoints();
180         updateBreakpointFiles(res);
181         return res;
182     }
183     
184     protected void fillStartupProject() {
185         string s = _settings.startupProjectName;
186         if ((!_startupProject || !_startupProject.name.toUTF8.equal(s)) && _projects.length) {
187             if (!s.empty) {
188                 foreach(p; _projects) {
189                     if (p.name.toUTF8.equal(s)) {
190                         _startupProject = p;
191                     }
192                 }
193             }
194             if (!_startupProject) {
195                 startupProject = _projects[0];
196             }
197         }
198     }
199 
200     /// tries to find source file in one of projects, returns found project source file item, or null if not found
201     ProjectSourceFile findSourceFileItem(string filename, bool fullFileName=true, dstring projectName = null) {
202         foreach (Project p; _projects) {
203             if (projectName && p.name != projectName)
204                 continue;
205             ProjectSourceFile res = p.findSourceFileItem(filename, fullFileName);
206             if (res)
207                 return res;
208         }
209         return null;
210     }
211 
212     /// find project in workspace by filename
213     Project findProject(string filename) {
214         foreach (Project p; _projects) {
215             if (p.filename.equal(filename))
216                 return p;
217         }
218         return null;
219     }
220 
221     /// find project in workspace by filename
222     Project findProjectByName(string name) {
223         foreach (Project p; _projects) {
224             if (p.name.toUTF8.equal(name))
225                 return p;
226         }
227         return null;
228     }
229 
230     Project findProjectInWorkspace(Project p) {
231         foreach(existing; _projects)
232             if (existing is p || existing.filename == p.filename)
233                 return existing;
234         return null;
235     }
236 
237     Project findProjectInWorkspace(string projectFilename) {
238         foreach(existing; _projects)
239             if (existing.filename == projectFilename)
240                 return existing;
241         return null;
242     }
243 
244     void addProject(Project p) {
245         if (findProjectInWorkspace(p))
246             return;
247         Log.d("addProject ", p.filename);
248         _projects ~= p;
249         p.workspace = this;
250         fillStartupProject();
251     }
252 
253     Project removeProject(int index) {
254         if (index < 0 || index > _projects.length)
255             return null;
256         Project res = _projects[index];
257         for (int j = index; j + 1 < _projects.length; j++)
258             _projects[j] = _projects[j + 1];
259         _projects.length = _projects.length - 1;
260         return res;
261     }
262 
263     bool isDependencyProjectUsed(string filename) {
264         foreach(p; _projects)
265             if (!p.isDependency && p.findDependencyProject(filename))
266                 return true;
267         return false;
268     }
269 
270     void cleanupUnusedDependencies() {
271         for (int i = cast(int)_projects.length - 1; i >= 0; i--) {
272             if (_projects[i].isDependency) {
273                 if (!isDependencyProjectUsed(_projects[i].filename))
274                     removeProject(i);
275             }
276         }
277     }
278 
279     bool addDependencyProject(Project p) {
280         if (findProjectInWorkspace(p))
281             return false;
282         addProject(p);
283         return true;
284     }
285 
286     string absoluteToRelativePath(string path) {
287         return toForwardSlashSeparator(relativePath(path, _dir));
288     }
289 
290     override bool save(string fname = null) {
291         if (fname.length > 0)
292             filename = fname;
293         if (_filename.empty) // no file name specified
294             return false;
295         _settings.save(_filename ~ WORKSPACE_SETTINGS_EXTENSION);
296         // If name is null, then compose it from projects
297         // If description is null, then compose it from project's descriptions
298         immutable auto nf = _name.empty;
299         immutable auto df = _description.empty;
300         if (nf || df)
301         {
302             _name = nf ? "" : _name;
303             _description = df ? "" : _description;
304             foreach (Project p; _projects) {
305                if (p.isDependency)
306                     continue; // don't add dependency
307                 if (nf)
308                     _name ~= p.name ~ ",";
309                 if (df)
310                     _description ~= p.description ~ " / ";
311             }
312             if (nf && !_name.empty) // cut off last comma
313                 _name = _name[ 0 .. $ - 1 ];
314             if (df && !_description.empty) // cut off last delimiter
315                 _description = _description[ 0 .. $ - 3 ]; 
316         }
317         _workspaceFile.setString("name", toUTF8(_name));
318         _workspaceFile.setString("description", toUTF8(_description));
319         Log.d("workspace name: ", _name);
320         Log.d("workspace description: ", _description);
321         Setting projects = _workspaceFile.objectByPath("projects", true);
322         projects.clear(SettingType.OBJECT);
323         foreach (Project p; _projects) {
324             if (p.isDependency)
325                 continue; // don't save dependency
326             string pname = toUTF8(p.name);
327             string ppath = absoluteToRelativePath(p.filename);
328             projects[pname] = ppath;
329         }
330         if (!_workspaceFile.save(_filename, true)) {
331             Log.e("Cannot save workspace file");
332             return false;
333         }
334         return true;
335     }
336 
337     override bool load(string fname = null) {
338         if (fname.length > 0)
339             filename = fname;
340         if (!exists(_filename) || !isFile(_filename))  {
341             return false;
342         }
343         Log.d("Reading workspace from file ", _filename);
344         if (!_workspaceFile.load(_filename)) {
345             Log.e("Cannot read workspace file");
346             return false;
347         }
348         _settings.load(filename ~ WORKSPACE_SETTINGS_EXTENSION);
349         _name = toUTF32(_workspaceFile["name"].str);
350         _description = toUTF32(_workspaceFile["description"].str);
351         Log.d("workspace name: ", _name);
352         Log.d("workspace description: ", _description);
353         if (_name.empty()) {
354             Log.e("empty workspace name");
355             return false;
356         }
357         auto originalStartupProjectName = _settings.startupProjectName;
358         Setting projects = _workspaceFile.objectByPath("projects", true);
359         foreach(string key, Setting value; projects) {
360             string path = value.str;
361             Log.d("project: ", key, " path:", path);
362             if (!isAbsolute(path))
363                 path = buildNormalizedPath(_dir, path); //, "dub.json"
364             if (findProjectInWorkspace(path))
365                 continue;
366             Project project = new Project(this, path);
367             _projects ~= project;
368             project.load();
369         }
370         _settings.startupProjectName = originalStartupProjectName;
371         fillStartupProject();
372         return true;
373     }
374     void close() {
375     }
376 
377     void refresh() {
378         foreach (Project p; _projects) {
379             p.refresh();
380         }
381     }
382 }
383 
384 /// global workspace
385 __gshared Workspace currentWorkspace;