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     /// setups currrent project configuration by name
101     void setStartupProjectConfiguration(string conf)
102     {
103         if(_startupProject && conf in _startupProject.configurations) {
104             _projectConfiguration = _startupProject.configurations[conf];
105         }
106     }
107 
108     private void updateBreakpointFiles(Breakpoint[] breakpoints) {
109         foreach(bp; breakpoints) {
110             Project project = findProjectByName(bp.projectName);
111             if (project)
112                 bp.fullFilePath = project.relativeToAbsolutePath(bp.projectFilePath);
113         }
114     }
115 
116     private void updateBookmarkFiles(EditorBookmark[] bookmarks) {
117         foreach(bp; bookmarks) {
118             Project project = findProjectByName(bp.projectName);
119             if (project)
120                 bp.fullFilePath = project.relativeToAbsolutePath(bp.projectFilePath);
121         }
122     }
123 
124     Breakpoint[] getSourceFileBreakpoints(ProjectSourceFile file) {
125         Breakpoint[] res = _settings.getProjectBreakpoints(toUTF8(file.project.name), file.projectFilePath);
126         updateBreakpointFiles(res);
127         return res;
128     }
129     
130     void setSourceFileBreakpoints(ProjectSourceFile file, Breakpoint[] breakpoints) {
131         _settings.setProjectBreakpoints(toUTF8(file.project.name), file.projectFilePath, breakpoints);
132     }
133 
134     EditorBookmark[] getSourceFileBookmarks(ProjectSourceFile file) {
135         EditorBookmark[] res = _settings.getProjectBookmarks(toUTF8(file.project.name), file.projectFilePath);
136         updateBookmarkFiles(res);
137         return res;
138     }
139     
140     void setSourceFileBookmarks(ProjectSourceFile file, EditorBookmark[] bookmarks) {
141         _settings.setProjectBookmarks(toUTF8(file.project.name), file.projectFilePath, bookmarks);
142     }
143 
144     /// returns all workspace breakpoints
145     Breakpoint[] getBreakpoints() {
146         Breakpoint[] res = _settings.getBreakpoints();
147         updateBreakpointFiles(res);
148         return res;
149     }
150     
151     protected void fillStartupProject() {
152         string s = _settings.startupProjectName;
153         if ((!_startupProject || !_startupProject.name.toUTF8.equal(s)) && _projects.length) {
154             if (!s.empty) {
155                 foreach(p; _projects) {
156                     if (p.name.toUTF8.equal(s)) {
157                         _startupProject = p;
158                     }
159                 }
160             }
161             if (!_startupProject) {
162                 startupProject = _projects[0];
163             }
164         }
165     }
166 
167     /// tries to find source file in one of projects, returns found project source file item, or null if not found
168     ProjectSourceFile findSourceFileItem(string filename, bool fullFileName=true) {
169         foreach (Project p; _projects) {
170             ProjectSourceFile res = p.findSourceFileItem(filename, fullFileName);
171             if (res)
172                 return res;
173         }
174         return null;
175     }
176 
177     /// find project in workspace by filename
178     Project findProject(string filename) {
179         foreach (Project p; _projects) {
180             if (p.filename.equal(filename))
181                 return p;
182         }
183         return null;
184     }
185 
186     /// find project in workspace by filename
187     Project findProjectByName(string name) {
188         foreach (Project p; _projects) {
189             if (p.name.toUTF8.equal(name))
190                 return p;
191         }
192         return null;
193     }
194 
195     void addProject(Project p) {
196         _projects ~= p;
197         p.workspace = this;
198         fillStartupProject();
199     }
200 
201     Project removeProject(int index) {
202         if (index < 0 || index > _projects.length)
203             return null;
204         Project res = _projects[index];
205         for (int j = index; j + 1 < _projects.length; j++)
206             _projects[j] = _projects[j + 1];
207         return res;
208     }
209 
210     bool isDependencyProjectUsed(string filename) {
211         foreach(p; _projects)
212             if (!p.isDependency && p.findDependencyProject(filename))
213                 return true;
214         return false;
215     }
216 
217     void cleanupUnusedDependencies() {
218         for (int i = cast(int)_projects.length - 1; i >= 0; i--) {
219             if (_projects[i].isDependency) {
220                 if (!isDependencyProjectUsed(_projects[i].filename))
221                     removeProject(i);
222             }
223         }
224     }
225 
226     bool addDependencyProject(Project p) {
227         for (int i = 0; i < _projects.length; i++) {
228             if (_projects[i].filename.equal(p.filename)) {
229                 _projects[i] = p;
230                 return false;
231             }
232         }
233         addProject(p);
234         return true;
235     }
236 
237     string absoluteToRelativePath(string path) {
238         return toForwardSlashSeparator(relativePath(path, _dir));
239     }
240 
241     override bool save(string fname = null) {
242         if (fname.length > 0)
243             filename = fname;
244         if (_filename.empty) // no file name specified
245             return false;
246         _settings.save(_filename ~ WORKSPACE_SETTINGS_EXTENSION);
247         // If name is null, then compose it from projects
248         // If description is null, then compose it from project's descriptions
249         immutable auto nf = _name.empty;
250         immutable auto df = _description.empty;
251         if (nf || df)
252         {
253             _name = nf ? "" : _name;
254             _description = df ? "" : _description;
255             foreach (Project p; _projects) {
256                if (p.isDependency)
257                     continue; // don't add dependency
258                 if (nf)
259                     _name ~= p.name ~ ",";
260                 if (df)
261                     _description ~= p.description ~ " / ";
262             }
263             if (!_name.empty) // cut off last comma
264                 _name = _name[ 0 .. $ - 1 ];
265             if (!_description.empty) // cut off last delimiter
266                 _description = _description[ 0 .. $ - 3 ]; 
267         }
268         _workspaceFile.setString("name", toUTF8(_name));
269         _workspaceFile.setString("description", toUTF8(_description));
270         Log.d("workspace name: ", _name);
271         Log.d("workspace description: ", _description);
272         Setting projects = _workspaceFile.objectByPath("projects", true);
273         projects.clear(SettingType.OBJECT);
274         foreach (Project p; _projects) {
275             if (p.isDependency)
276                 continue; // don't save dependency
277             string pname = toUTF8(p.name);
278             string ppath = absoluteToRelativePath(p.filename);
279             projects[pname] = ppath;
280         }
281         if (!_workspaceFile.save(_filename, true)) {
282             Log.e("Cannot save workspace file");
283             return false;
284         }
285         return true;
286     }
287 
288     override bool load(string fname = null) {
289         if (fname.length > 0)
290             filename = fname;
291         if (!exists(_filename) || !isFile(_filename))  {
292             return false;
293         }
294         Log.d("Reading workspace from file ", _filename);
295         if (!_workspaceFile.load(_filename)) {
296             Log.e("Cannot read workspace file");
297             return false;
298         }
299         _settings.load(filename ~ WORKSPACE_SETTINGS_EXTENSION);
300         _name = toUTF32(_workspaceFile["name"].str);
301         _description = toUTF32(_workspaceFile["description"].str);
302         Log.d("workspace name: ", _name);
303         Log.d("workspace description: ", _description);
304         if (_name.empty()) {
305             Log.e("empty workspace name");
306             return false;
307         }
308         auto originalStartupProjectName = _settings.startupProjectName;
309         Setting projects = _workspaceFile.objectByPath("projects", true);
310         foreach(string key, Setting value; projects) {
311             string path = value.str;
312             Log.d("project: ", key, " path:", path);
313             if (!isAbsolute(path))
314                 path = buildNormalizedPath(_dir, path); //, "dub.json"
315             Project project = new Project(this, path);
316             _projects ~= project;
317             project.load();
318         }
319         _settings.startupProjectName = originalStartupProjectName;
320         fillStartupProject();
321         return true;
322     }
323     void close() {
324     }
325 
326     void refresh() {
327         foreach (Project p; _projects) {
328             p.refresh();
329         }
330     }
331 }
332 
333 /// global workspace
334 __gshared Workspace currentWorkspace;