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