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