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;