浏览代码

[filebrowser] Restored SVN support

Clément Espeute 2 月之前
父节点
当前提交
66a9350959
共有 9 个文件被更改,包括 233 次插入36 次删除
  1. 5 0
      bin/res/icons/svg/dot.svg
  2. 28 1
      bin/style.css
  3. 31 1
      bin/style.less
  4. 31 6
      hide/Ide.hx
  5. 9 4
      hide/comp/FancyTree.hx
  6. 77 0
      hide/tools/FileManager.hx
  7. 49 21
      hide/view/FileBrowser.hx
  8. 1 1
      hide/view/FileTree.hx
  9. 2 2
      hide/view/settings/UserSettings.hx

+ 5 - 0
bin/res/icons/svg/dot.svg

@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg width="100%" height="100%" viewBox="0 0 256 256" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
+    <circle cx="128" cy="128" r="128" style="fill:white;"/>
+</svg>

+ 28 - 1
bin/style.css

@@ -4976,13 +4976,13 @@ fancy-flex-fill {
   flex: 1 1;
 }
 fancy-icon {
+  aspect-ratio: 1/1;
   height: 1em;
   display: block;
   mask-size: contain;
   background-color: currentColor;
   mask-mode: luminance;
   mask-repeat: no-repeat;
-  aspect-ratio: 1/1;
   mask-position: center;
 }
 fancy-icon.small {
@@ -5361,6 +5361,33 @@ fancy-tooltip {
   margin: 0;
   overflow: visible;
 }
+.fancy-status-icon {
+  position: relative;
+}
+.fancy-status-icon::after {
+  content: " ";
+  aspect-ratio: 1/1;
+  height: 1em;
+  display: block;
+  mask-size: contain;
+  background-color: currentColor;
+  mask-mode: luminance;
+  mask-repeat: no-repeat;
+  mask-position: center;
+  width: 6px;
+  height: 6px;
+  position: absolute;
+  left: -2px;
+  bottom: 1px;
+}
+.fancy-status-icon.fancy-status-icon-ok::after {
+  mask-image: url("res/icons/svg/dot.svg");
+  color: #87D58E;
+}
+.fancy-status-icon.fancy-status-icon-modified::after {
+  mask-image: url("res/icons/svg/dot.svg");
+  color: #FE383A;
+}
 .lm_tabdropdown_list {
   z-index: 9999 !important;
   background-color: #111111;

+ 31 - 1
bin/style.less

@@ -5959,7 +5959,6 @@ fancy-icon {
 	background-color: currentColor;
 	mask-mode: luminance;
 	mask-repeat: no-repeat;
-	aspect-ratio: 1/1;
 	mask-position: center;
 
 	&.small {
@@ -6438,6 +6437,37 @@ fancy-tooltip {
 	// }
 }
 
+.fancy-status-icon {
+	position: relative;
+
+	&::after {
+		content: " ";
+		aspect-ratio: 1/1;
+		height: 1em;
+		display: block;
+		mask-size: contain;
+		background-color: currentColor;
+		mask-mode: luminance;
+		mask-repeat: no-repeat;
+		mask-position: center;
+		width: 6px;
+		height: 6px;
+		position: absolute;
+		left: -2px;
+		bottom: 1px;
+	}
+
+	&.fancy-status-icon-ok::after {
+		mask-image: url("res/icons/svg/dot.svg");
+		color: #87D58E;
+	}
+
+	&.fancy-status-icon-modified::after {
+		mask-image: url("res/icons/svg/dot.svg");
+		color: #FE383A;
+	}
+}
+
 .lm_tabdropdown_list {
 	z-index: 9999 !important;
 	background-color: #111111;

+ 31 - 6
hide/Ide.hx

@@ -1891,12 +1891,11 @@ class Ide extends hide.tools.IdeData {
 		return js.Browser.window.prompt(text, defaultValue);
 	}
 
-	public function getSVNModifiedFiles() {
+	var delayedSvnStatusCallbacks : Array<(files : Array<String>) -> Void> = null;
+
+	function onSvnStatusFinished(callbacks: Array<(files : Array<String>) -> Void>, error: Dynamic, stdOut: String, stderr: String) {
 		var modifiedFiles : Array<String> = [];
-		if (!isSVNAvailable())
-			throw "SVN not available";
-		var cmd = js.node.ChildProcess.execSync('svn status', { cwd: projectDir });
-		var outputs : Array<String> = '$cmd'.split("\r\n");
+		var outputs : Array<String> = stdOut.split("\r\n");
 		for (o in outputs) {
 			if (o.length == 0)
 				continue;
@@ -1905,7 +1904,33 @@ class Ide extends hide.tools.IdeData {
 			var file = getPath(o.substr(o.indexOf("res/") + 4));
 			modifiedFiles.push(file);
 		}
-		return modifiedFiles;
+		for (callback in callbacks) {
+			callback(modifiedFiles);
+		}
+
+		if (delayedSvnStatusCallbacks != null && delayedSvnStatusCallbacks.length > 0) {
+			var oldCallbacks = delayedSvnStatusCallbacks;
+			delayedSvnStatusCallbacks = [];
+			execSvnCommand(onSvnStatusFinished.bind(oldCallbacks));
+		} else {
+			delayedSvnStatusCallbacks = null;
+		}
+	}
+
+	function execSvnCommand(callback: (error: Dynamic, stdOut: String, stderr: String) -> Void) {
+		js.node.ChildProcess.exec('svn status', { cwd: projectDir }, callback);
+	}
+
+	public function getSVNModifiedFiles(callback: (files : Array<String>) -> Void) : Void{
+		if (!isSVNAvailable())
+			throw "SVN not available";
+
+		if (delayedSvnStatusCallbacks == null) {
+			delayedSvnStatusCallbacks = [];
+			execSvnCommand(onSvnStatusFinished.bind([callback]));
+		} else {
+			hide.tools.Extensions.ArrayExtensions.pushUnique(delayedSvnStatusCallbacks, callback);
+		}
 	}
 
 	public function isSVNAvailable() {

+ 9 - 4
hide/comp/FancyTree.hx

@@ -232,8 +232,13 @@ class FancyTree<TreeItem> extends hide.comp.Component {
 	}
 
 	// Separate definition to onContextMenu to allow .bind()
-	function contextMenuHandler(item: TreeItem, event : js.html.MouseEvent) {
-		onContextMenu(item, event);
+	function contextMenuHandler(data: TreeItemData<TreeItem>, event : js.html.MouseEvent) {
+		if ((data == null || !selection.exists(cast data)) && !event.shiftKey) {
+			clearSelection();
+			if (data != null)
+				setSelection(data, true);
+		}
+		onContextMenu(data?.item, event);
 	}
 
 	/**
@@ -713,7 +718,7 @@ class FancyTree<TreeItem> extends hide.comp.Component {
 			});
 
 			element.onclick = dataClickHandler.bind(data);
-			element.oncontextmenu = contextMenuHandler.bind(data.item);
+			element.oncontextmenu = contextMenuHandler.bind(data);
 			element.ondblclick = doubleClickHander.bind(data);
 
 			data.element = element;
@@ -1009,7 +1014,7 @@ class FancyTree<TreeItem> extends hide.comp.Component {
 	// TODO(ces) : The main release of haxe doesn't support type inference with `|` which make using
 	// queueRefresh with an EnumFlag as an argument cumbersome. Untill then, make multiple queueRefresh calls
 	// with each of the flags you want to set
-	function queueRefresh(?flag: RefreshFlag = null) {
+	public function queueRefresh(?flag: RefreshFlag = null) {
 		if (flag != null) {
 			currentRefreshFlags.set(flag);
 		}

+ 77 - 0
hide/tools/FileManager.hx

@@ -21,6 +21,23 @@ enum FileKind {
 	File;
 }
 
+enum VCSStatus {
+	/**
+		Pending or no vsc available on system
+	**/
+	None;
+
+	/**
+		The file is up to date and not modified
+	**/
+	UpToDate;
+
+	/**
+		The file is modified locally
+	**/
+	Modified;
+}
+
 @:access(hide.tools.FileManager)
 @:allow(hide.tools.FileManager)
 class FileEntry {
@@ -32,6 +49,7 @@ class FileEntry {
 	public var parent: FileEntry;
 	public var iconPath: String;
 	public var disposed: Bool = false;
+	public var vcsStatus: VCSStatus = None;
 	public var ignored: Bool = false;
 
 	var registeredWatcher : hide.tools.FileWatcher.FileWatchEvent = null;
@@ -167,6 +185,9 @@ class FileManager {
 
 	public static var inst(get, default) : FileManager;
 	public var onFileChangeHandlers: Array<(entry: FileEntry) -> Void> = [];
+	public var onVCSStatusUpdateHandlers: Array<() -> Void> = [];
+
+	var svnEnabled = false;
 
 	var windowManager : RenderWindowManager = null;
 
@@ -275,6 +296,49 @@ class FileManager {
 		return roots;
 	}
 
+	function onSVNFileModified(modifiedFiles: Array<String>) {
+		for (file in fileIndex) {
+			file.vcsStatus = UpToDate;
+		}
+
+		for (modifiedFile in modifiedFiles) {
+			var relPath = hide.Ide.inst.getRelPath(modifiedFile);
+			var file = fileIndex.get(relPath);
+
+			while(file != null && file.vcsStatus != Modified) {
+				file.vcsStatus = Modified;
+				file = file.parent;
+			}
+		}
+
+		for (handler in onVCSStatusUpdateHandlers) {
+			handler();
+		}
+	}
+
+	/**
+		Return the path to a temporary file with all the paths in the files array inside
+	**/
+	public function createSVNFileList(files: Array<FileEntry>) : String {
+		var tmpdir = js.node.Os.tmpdir();
+		var name = 'hidefiles${Std.int(hxd.Math.random(100000000))}.txt';
+		var path = tmpdir + "/" + name;
+
+
+		var str = [for(f in files) f.getPath()].join("\n");
+
+		// Encode paths as utf-16 because tortoiseproc want the file encoded that way
+		var bytes = haxe.io.Bytes.alloc(str.length * 2);
+		var pos = 0;
+
+		for (char in 0...str.length) {
+			bytes.setUInt16(pos, str.charCodeAt(char));
+			pos += 2;
+		}
+		sys.io.File.saveBytes(path, bytes);
+		return path;
+	}
+
 	function setupServer() {
 		if (serverSocket != null)
 			throw "Server already exists";
@@ -337,6 +401,8 @@ class FileManager {
 		// kill server when page is reloaded
 		js.Browser.window.addEventListener('beforeunload', () -> { cleanupGenerator(); cleanupServer(); });
 
+		svnEnabled = hide.Ide.inst.isSVNAvailable();
+
 		var exclPatterns : Array<String> = hide.Ide.inst.currentConfig.get("filetree.excludes", []);
 		ignorePatterns = [];
 		for(pat in exclPatterns)
@@ -354,6 +420,8 @@ class FileManager {
 
 		fileRoot = new FileEntry("res", null, Dir);
 		fileRoot.refreshChildren();
+
+		queueRefreshSVN();
 	}
 
 	function fileChangeInternal(entry: FileEntry) {
@@ -367,11 +435,20 @@ class FileManager {
 		if (entry.kind == Dir) {
 			fileEntryRefreshDelay.queue(entry);
 		}
+
+		queueRefreshSVN();
+
 		for (handler in onFileChangeHandlers) {
 			handler(entry);
 		}
 	}
 
+	public function queueRefreshSVN() {
+		if (svnEnabled) {
+			hide.Ide.inst.getSVNModifiedFiles(onSVNFileModified);
+		}
+	}
+
 	public function cloneFile(entry: FileEntry) {
 		var sourcePath = entry.getPath();
 		var nameNewFile = hide.Ide.inst.ask("New filename:", new haxe.io.Path(sourcePath).file);

+ 49 - 21
hide/view/FileBrowser.hx

@@ -330,6 +330,28 @@ class FileBrowser extends hide.ui.View<FileBrowserState> {
 		return !entry.ignored;
 	}
 
+	public function refreshVCS() {
+		fancyTree.queueRefresh(RegenHeader);
+		fancyGallery.queueRefresh(RegenHeader);
+	}
+
+	function getIcon(item : FileEntry) : String {
+		var vcsClass = switch(item.vcsStatus) {
+			case None: "";
+			case UpToDate: Ide.inst.ideConfig.svnShowVersionedFiles ? "fancy-status-icon fancy-status-icon-ok" : "";
+			case Modified: Ide.inst.ideConfig.svnShowModifiedFiles ? "fancy-status-icon fancy-status-icon-modified" : "";
+		};
+
+		if (item.kind == Dir)
+			return '<div class="ico ico-folder ${vcsClass}"></div>';
+		var ext = @:privateAccess hide.view.FileTree.getExtension(item.name);
+		if (ext != null) {
+			if (ext?.options.icon != null) {
+				return '<div class="ico ico-${ext.options.icon} ${vcsClass}" title="${ext.options.name ?? "Unknown"}"></div>';
+			}
+		}
+		return '<div class="ico ico-file ${vcsClass}" title="Unknown"></div>';
+	}
 
 	override function onDisplay() {
 		keys.register("undo", function() undo.undo());
@@ -417,17 +439,7 @@ class FileBrowser extends hide.ui.View<FileBrowserState> {
 		fancyTree.getName = (file: FileEntry) -> return file?.name;
 		fancyTree.getUniqueName = (file: FileEntry) -> file?.getRelPath();
 
-		fancyTree.getIcon = (item : FileEntry) -> {
-			if (item.kind == Dir)
-				return '<div class="ico ico-folder"></div>';
-			var ext = @:privateAccess hide.view.FileTree.getExtension(item.name);
-			if (ext != null) {
-				if (ext?.options.icon != null) {
-					return '<div class="ico ico-${ext.options.icon}" title="${ext.options.name ?? "Unknown"}"></div>';
-				}
-			}
-			return '<div class="ico ico-file" title="Unknown"></div>';
-		}
+		fancyTree.getIcon = getIcon;
 
 		fancyTree.onNameChange = renameHandler;
 
@@ -551,15 +563,7 @@ class FileBrowser extends hide.ui.View<FileBrowserState> {
 			}
 		};
 
-		fancyGallery.getIcon = (item : FileEntry) -> {
-			var ext = @:privateAccess hide.view.FileTree.getExtension(item.name);
-			if (ext != null) {
-				if (ext?.options.icon != null) {
-					return '<div class="ico ico-${ext.options.icon}" title="${ext.options.name ?? "Unknown"}"></div>';
-				}
-			}
-			return null;
-		}
+		fancyGallery.getIcon = getIcon;
 
 		fancyGallery.onDoubleClick = (item: FileEntry) -> {
 			if (item.kind == File) {
@@ -684,6 +688,7 @@ class FileBrowser extends hide.ui.View<FileBrowserState> {
 		syncGallerySearchFullPath();
 
 		FileManager.inst.onFileChangeHandlers.push(onFileChange);
+		FileManager.inst.onVCSStatusUpdateHandlers.push(refreshVCS);
 
 		layout = state.savedLayout ?? Horizontal;
 	}
@@ -1000,9 +1005,32 @@ class FileBrowser extends hide.ui.View<FileBrowserState> {
 					}
 				});
 			}});
-
 		}
 
+		if (ide.isSVNAvailable()) {
+			options.push({ label : "", isSeparator: true });
+			options.push({ label: "SVN Revert", click : function() {
+				var fileList = FileManager.inst.createSVNFileList(getItemAndSelection(item, isGallery));
+				js.node.ChildProcess.exec('cmd.exe /c start "" TortoiseProc.exe /command:revert /pathfile:"$fileList" /deletepathfile', { cwd: ide.getPath(ide.resourceDir) }, (error, stdout, stderr) -> {
+					if (error != null)
+						ide.quickError('Error while trying to revert files : ${error}');
+				});
+			}});
+			options.push({ label: "SVN Log", click : function() {
+				var path = item.getPath();
+				js.node.ChildProcess.exec('cmd.exe /c start "" TortoiseProc.exe /command:log /path:"$path"', { cwd: ide.getPath(ide.resourceDir) }, (error, stdout, stderr) -> {
+					if (error != null)
+						ide.quickError('Error while trying to log file ${path} : ${error}');
+				});
+			}});
+			options.push({ label: "SVN Blame", click : function() {
+				var path = item.getPath();
+				js.node.ChildProcess.exec('cmd.exe /c start "" TortoiseProc.exe /command:blame /path:"$path"', { cwd: ide.getPath(ide.resourceDir) }, (error, stdout, stderr) -> {
+					if (error != null)
+						ide.quickError('Error while trying to blame file ${path} : ${error}');
+				});
+			}});
+		}
 
 		hide.comp.ContextMenu.createFromEvent(event, options);
 	}

+ 1 - 1
hide/view/FileTree.hx

@@ -246,7 +246,7 @@ class FileTree extends FileView {
 			return;
 		}
 
-		modifiedFiles = Ide.inst.getSVNModifiedFiles();
+		modifiedFiles = [];//Ide.inst.getSVNModifiedFiles();
 		if (el == null)
 			el = tree.element;
 

+ 2 - 2
hide/view/settings/UserSettings.hx

@@ -7,8 +7,8 @@ class UserSettings extends Settings {
 		var general = new hide.view.settings.Settings.Categorie("General");
 		general.add("Auto-save prefabs before closing", new Element('<input type="checkbox"/>'), Ide.inst.ideConfig.autoSavePrefab, (v) -> Ide.inst.ideConfig.autoSavePrefab = v);
 		general.add("Use alternate font", new Element('<input type="checkbox"/>'), Ide.inst.ideConfig.useAlternateFont, (v) -> {Ide.inst.ideConfig.useAlternateFont = v; Ide.inst.refreshFont();});
-		general.add("Show versioned files in filetree", new Element('<input type="checkbox"/>'), Ide.inst.ideConfig.svnShowVersionedFiles, (v) -> {Ide.inst.ideConfig.svnShowVersionedFiles = v; Ide.inst.getViews(FileTree)[0].rebuild(); });
-		general.add("Show modified files in filetree", new Element('<input type="checkbox"/>'), Ide.inst.ideConfig.svnShowModifiedFiles, (v) -> {Ide.inst.ideConfig.svnShowModifiedFiles = v; Ide.inst.getViews(FileTree)[0].rebuild(); });
+		general.add("Show versioned files in filetree", new Element('<input type="checkbox"/>'), Ide.inst.ideConfig.svnShowVersionedFiles, (v) -> {Ide.inst.ideConfig.svnShowVersionedFiles = v; for(view in Ide.inst.getViews(FileBrowser)) view.refreshVCS(); });
+		general.add("Show modified files in filetree", new Element('<input type="checkbox"/>'), Ide.inst.ideConfig.svnShowModifiedFiles, (v) -> {Ide.inst.ideConfig.svnShowModifiedFiles = v; for(view in Ide.inst.getViews(FileBrowser)) view.refreshVCS(); });
 
 		categories.push(general);