<commit_before>enyo.kind({
	name: "HermesFileTree",
	kind: "FittableRows",
	events: {
		onFileClick: "",
		onFolderClick: "",
		onFileDblClick: "",
		onProjectFound: "",
		onConfirm: "",
		onSelect: "",
		onDeselect: "",
		onNewFileConfirm: "",
		onDeleteConfirm: "",
		onRenameConfirm: "",
		onNewFolderConfirm: "",
		onCopyFileConfirm: ""

	},
	handlers: {
		onNodeDblClick: "nodeDblClick"
	},
	published: {
		serverName: ""
	},
	components: [
		{kind: "onyx.Toolbar", classes: "onyx-menu-toolbar ares_harmonia_toolBar", components: [
			{name: "newFolder", kind: "onyx.TooltipDecorator", components: [
				{kind: "onyx.IconButton", src: "$harmonia/images/folder_new.png", onclick: "newFolderClick"},
				{kind: "onyx.Tooltip", content: "New Folder..."}
			]},
			{name: "reloadAll", kind: "onyx.TooltipDecorator", components: [
				{kind: "onyx.IconButton", src: "$harmonia/images/folder_reload.png", onclick: "reloadClick"},
				{kind: "onyx.Tooltip", content: "reload ..."}
			]},
			{name: "newFile", kind: "onyx.TooltipDecorator", components: [
				{kind: "onyx.IconButton", src: "$harmonia/images/document_new.png", onclick: "newFileClick"},
				{kind: "onyx.Tooltip", content: "New File..."}
			]},
			{name: "renameFile", kind: "onyx.TooltipDecorator", components: [
				{kind: "onyx.IconButton", src: "$harmonia/images/document_edit.png", onclick: "renameClick"},
				{kind: "onyx.Tooltip", content: "Rename..."}
			]},
			{name: "copyFile", kind: "onyx.TooltipDecorator", components: [
				{kind: "onyx.IconButton", src: "$harmonia/images/copy.png", onclick: "copyClick"},
				{kind: "onyx.Tooltip", content: "Copy..."}
			]},
			{name: "deleteFile", kind: "onyx.TooltipDecorator", components: [
				{kind: "onyx.IconButton", src: "$harmonia/images/document_delete.png", onclick: "deleteClick"},
				{kind: "onyx.Tooltip", content: "Delete..."}
			]}
		]},

		{kind: "Scroller", fit: true, components: [
			{name: "serverNode", kind: "Node", classes: "enyo-unselectable", showing: false, file: {id: "", name: "server", isServer: true, service: null}, content: "server", icon: "$service/images/antenna.png", expandable: true, expanded: true, onExpand: "nodeExpand", onNodeTap: "nodeTap"}
		]},

		// track selection of nodes. here, selection Key is file or folderId. Selection value is the node object
		{kind: "Selection", onSelect: "select", onDeselect: "deselect"},

		{name: "service", kind: "FileSystemService"},
		{name: "errorPopup", kind: "Ares.ErrorPopup", msg: "Service returned an error"},
		{name: "nameFilePopup", kind: "NamePopup", type: "file", fileName:"", placeHolder: "File Name", onCancel: "newFileCancel", onConfirm: "newFileConfirm"},
		{name: "nameFolderPopup", kind: "NamePopup", type: "folder", fileName: "", placeHolder: "Folder Name", onCancel: "newFolderCancel", onConfirm: "newFolderConfirm"},
		{name: "nameCopyPopup", kind: "NamePopup", title: "Name for copy of", fileName: "Copy of foo.js", onCancel: "copyFileCancel", onConfirm: "copyFileConfirm"},
		{name: "deletePopup", kind: "DeletePopup", onCancel: "deleteCancel", onConfirm: "deleteConfirm"},
		{name: "renamePopup", kind: "RenamePopup", title: "New name for ", fileName: "foo.js", onCancel: "renameCancel", onConfirm: "renameConfirm"}
	],
	// warning: this variable duplicates an information otherwise stored in this.$.selection
	// BUT, retrieving it through this.$.selection.getSelected is not handy as this function
	// return an object (hash) which needs to be scanned to retrieve the selected value
	selectedFile: null,
	selectedNode: null,
	debug: false,
	create: function() {
		this.inherited(arguments);
	},
	connectService: function(inService) {
		this.projectUrlReady = false; // Reset the project information
		this.clear() ;
		this.$.service.connect(inService);
		return this ;
	},
	/**
	 * @param {Object} inFsService a FileSystemService implementation, as listed in ProviderList
	 */
	setConfig: function(inProjectData) {
		if (this.debug) this.log("config:", inProjectData);
		this.projectData = inProjectData;

		var serverNode = this.$.serverNode;
		var nodeName = inProjectData.getName();
		var folderId = inProjectData.getFolderId();
		var service = inProjectData.getService();
		serverNode.hide();

		// connects to a service that provides access to a
		// (possibly remote & always asynchronous) file system
		this.connectService(service);

		// Get extra info such as project URL
		var req = this.$.service.propfind(folderId, 0);
		req.response(this, function(inSender, inValue) {
			var projectUrl = service.getConfig().origin + service.getConfig().pathname + "/file" + inValue.path;
			this.projectData.setProjectUrl(projectUrl);
			this.projectUrlReady = true;
			if (this.$.service.isOk()) {
				serverNode.setContent(nodeName);
				serverNode.file = {id: folderId, name: nodeName, isDir: true, isServer:true, path: nodeName, service: null};
				serverNode.setExpanded(true);
				serverNode.effectExpanded();
				this.refreshFileTree();
				serverNode.render() ;
			} else {
				req.fail("Service NOK");
				this.projectData.setProjectUrl("");
			}
		});
		req.error(this, function(inSender, inError) {
			this.projectData.setProjectUrl("");
			throw new Error(inError + " from " + inConfig.filesystem);
		});

		return this ;
	},
	hideFileOpButtons: function() {
		this.$.newFolder.hide();
		this.$.newFile.hide();
		this.$.reloadAll.hide();
		this.$.renameFile.hide();
		this.$.copyFile.hide();
		this.$.deleteFile.hide();
		return this ;
	},
	showNewFolderButton: function() {
		this.$.newFolder.show();
		return this ;
	},
	showFileOpButtons: function() {
		this.$.newFolder.show();
		this.$.reloadAll.show();
		this.$.newFile.show();
		this.$.renameFile.show();
		this.$.copyFile.show();
		this.$.deleteFile.show();
		return this ;
	},
	clear: function() {
		var server = this.$.serverNode;
		if (this.debug) this.log("clearing serverNode") ;
		enyo.forEach(
			this.getNodeFiles(server) ,
			function(n){
				n.destroy();
			}
		) ;
		server.hide();
		return this ;
	},
	reset: function() {
		this.$.serverNode.hide();
		if (this.$.service.isOk()) {
			if (this.debug) this.log("reseting serverNode") ;
			this.updateNodes(this.$.serverNode);
		}
		return this ;
	},
	//
	// probably should be in a Tree kind
	nodeTap: function(inSender, inEvent) {
		if (this.debug) this.log(inSender, "=>", inEvent);
		var node = inEvent.originator;
		this.$.selection.select(node.file.id, node);
		if (!node.file.isDir) {
			this.doFileClick({file: node.file});
		} else {
			this.doFolderClick({file: node.file});
		}
		// handled here (don't bubble)
		return true;
	},
	nodeDblClick: function(inSender, inEvent) {
		if (this.debug) this.log(inSender, "=>", inEvent);
		var node = inEvent.originator;
		// projectUrl in this.projectData is set asynchonously.  Do not try to
		// open anything before it is available.  Also do not
		// try to open top-level root & folders.
		if (!node.file.isDir && !node.file.isServer && this.projectUrlReady) {
			this.doFileDblClick({file: node.file, projectData: this.projectData});
		}
		// handled here (don't bubble)
		return true;
	},
	select: function(inSender, inEvent) {
		if (this.debug) this.log(inSender, "=>", inEvent);
		this.selectedNode=inEvent.data;
		this.selectedFile=inEvent.data.file;
		inEvent.data.file.service = this.$.service;
		inEvent.data.$.caption.applyStyle("background-color", "lightblue");
		this.doSelect({file: this.selectedFile});
		// handled here (don't bubble)
		return true;
	},
	deselect: function(inSender, inEvent) {
		if (this.debug) this.log(inSender, "=>", inEvent);
		if (inEvent.data && inEvent.data.$.caption) {
			inEvent.data.$.caption.applyStyle("background-color", null);
		}
		this.doDeselect({file: this.selectedFile});
		this.selectedFile=null;
		// handled here (don't bubble)
		return true;
	},
	// Note: this function does not recurse
	updateNodes: function(inNode) {
		this.startLoading(inNode);
		if (this.debug) this.log(inNode) ;
		return this.$.service.listFiles(inNode.file.id)
			.response(this, function(inSender, inFiles) {
				var sortedFiles = inFiles.sort(this.fileNameSort) ;
				if (inFiles && !this.$.serverNode.showing) {
					this.$.serverNode.show();
				}
				
				if (this.debug) this.log("updating node content of " + inNode.name ) ;
				this.updateNodeContent(inNode, sortedFiles);
				inNode.render() ;
			})
			.response(this, function() {
				this.stopLoading(inNode);
			})
			.error(this, function() {
				this.stopLoading(inNode);
			})
			;
	},
	startLoading: function(inNode) {
		inNode.$.extra.setContent('&nbsp;<img src="' + enyo.path.rewrite("$service/images/busy.gif") + '"/>');
	},
	stopLoading: function(inNode) {
		inNode.$.extra.setContent("");
	},
    updateNodeContent: function(inNode, files) {
		var i = 0 , nfiles, rfiles, res, modified = 0, newControl ;
		rfiles = this.filesToNodes(files) ; // with prefix in name
		nfiles = this.getNodeFiles(inNode) ;

		while ( i < nfiles.length || i < rfiles.length) {
			if (this.debug) this.log("considering node " + (nfiles[i] ? nfiles[i].name : '<none>') + ' and file ' + (rfiles[i] ? rfiles[i].name : '<none>') );

			res = i >= nfiles.length ? 1 
			    : i >= rfiles.length ? -1 
			    :                      this.fileNameSort(nfiles[i], rfiles[i]) ; 

			// remember that these file lists are sorted
			switch(res) {
				case -1: // remote file removed
					if (this.debug) this.log(nfiles[i].name + " was removed") ;
					nfiles[i].destroy() ;
				    modified = 1;
					nfiles = this.getNodeFiles(inNode) ;
					// node file list reduced by one, must not increment i
					break ;

				case 0: // no change
					i++ ;
					break ;

				case 1: // file added
				    if (this.debug) this.log(rfiles[i].name + " was added") ;
					newControl = inNode.createComponent( rfiles[i] ) ;
					nfiles = this.getNodeFiles(inNode) ;
					// FIXME: ENYO-1337
					//if (nfiles[i]) {
					//	var justAdded = inNode.controls.pop() ;
					//	inNode.controls.splice(i+4, 0, justAdded);
					//}
				    modified = 1;
					i++ ;
					break ;
			}

		}

		if (modified) {
			inNode.$.client.render();
		}
	},
	compareFiles: function(inFilesA, inFilesB) {
		if (inFilesA.length != inFilesB.length) {
			return false;
		}
		for (var i in inFilesA) {
			var f0 = inFilesA[i], f1 = inFilesB[i];
			if (f0.id !== f1.id) {
				return false;
			}
		}
		return true;
	},
	// Sort files by name, case-insensitively
	// TODO: I18N, and possibly platform-specific sort order
	fileNameSort: function(a, b) {
		var lowA, lowB;
		lowA=a.name.toLowerCase();
		lowB=b.name.toLowerCase();
		// compare lower-case version of name
		if (lowA < lowB) {
			return -1;
		} else if (lowB < lowA) {
			return 1;
		} else {
			// Files that differ only in case are sorted lexicographically
			if (a.name < b.name) {
				return -1;
			} else if (b.name < a.name) {
				return 1;
			} else {
				return 0;
			}
		}
	},
	getNodeFiles: function(inNode) {
		var hasPrefix = function(e){ 
			return (e.name.slice(0,1) === '$') ;
		} ;
		// getComponents only return the graphical items
		return inNode.getControls().filter( hasPrefix ).sort(this.fileNameSort) ;
	},
	filesToNodes: function(inFiles) {
		var nodes = [];
		inFiles.sort(this.fileNameSort); // TODO: Other sort orders
		for (var i=0, f; f=inFiles[i]; i++) {
			nodes.push({
				file: f,
				name: '$' + f.name, // prefix avoids clobberring non-files components like icon
				content: f.name,
				expandable: f.isDir,
				icon: "$service/images/" + (f.isDir ? "folder.png" : "file.png")
			});
		}
		return nodes;
	},
	nodeExpand: function(inSender, inEvent) {
		if (this.debug) this.log(inSender, "=>", inEvent);

		// the originating node is arbitrarily deep
		var node = inEvent.originator;
		if (this.debug) this.log("nodeExpand called while node Expanded is " + node.expanded) ;
		// update icon for expanded state
		if (node.file.isDir) {
			node.setIcon("$service/images/" + (node.expanded ? "folder-open.png" : "folder.png"));
		}
		// handle lazy-load when expanding
		if (node.expanded) {
			this.updateNodes(node).
				response(this, function() {
					node.effectExpanded();
				});
			// tell the event originator that node expansion should be deferred
			inEvent.wait = true;
		}
		// handled here (don't bubble)
		return true;
	},
	copyName: function(inName) {
		return "Copy of "+inName;
	},
	reloadClick: function(inSender, inEvent) {
		if (this.debug) this.log(inSender, "=>", inEvent);
		this.refreshFileTree();
	},

	// Both parameters are optional. node is null for the "top" refresh call. Node is set
	// for inner (async) recursive calls. toSelectId is optional. refresh will select this
	// entry if specified. Nothing is selected otherwise.
	refreshFileTree: function(node, toSelectId) {
		// Warning: this function fires several async events in each recursion. So knowing
		// when the refresh is over is challenging ...
		if (this.debug) this.log(this) ;
		var target = node || this.$.serverNode ;
		if (target.kind !== 'Node') {
			this.showErrorPopup("Internal Error");
			this.error("called with kind " + target.kind + " instead of Node");
			return ;
		}
		if (this.debug) this.log('running refreshFileTree on ' + target.kind + ' with ' +
					 target.controls.length + ' controls with content ' + target.content +
					 ' force select ' + toSelectId );

		this.updateNodes(target).
			response(this, function(inSender, inFiles) {
				// expand node if it has clients (sub-nodes) and is expanded
				target.effectExpanded();

				enyo.forEach(this.getNodeFiles(target), function(f) {
					var c = target.$[f.name] ; // c is a node
					if (this.debug) this.log('running INNER function of refreshFileTree on ' + f.name +
								 ' id ' + c.file.id);
					if ( c.file.id === toSelectId ) {
						if (this.debug) this.log('force select of ' + c.file.id);
						this.$.selection.select(c.file.id, c);
					}
					c.effectExpanded() ;
					if (f.expanded) {
						this.refreshFileTree(c,toSelectId);
					}
				}, this);
			});
	},

	// User Interaction for New File op<commit_msg>Select new folders when created in New Project popup<commit_after>enyo.kind({
	name: "HermesFileTree",
	kind: "FittableRows",
	events: {
		onFileClick: "",
		onFolderClick: "",
		onFileDblClick: "",
		onProjectFound: "",
		onConfirm: "",
		onSelect: "",
		onDeselect: "",
		onNewFileConfirm: "",
		onDeleteConfirm: "",
		onRenameConfirm: "",
		onNewFolderConfirm: "",
		onCopyFileConfirm: ""

	},
	handlers: {
		onNodeDblClick: "nodeDblClick"
	},
	published: {
		serverName: ""
	},
	components: [
		{kind: "onyx.Toolbar", classes: "onyx-menu-toolbar ares_harmonia_toolBar", components: [
			{name: "newFolder", kind: "onyx.TooltipDecorator", components: [
				{kind: "onyx.IconButton", src: "$harmonia/images/folder_new.png", onclick: "newFolderClick"},
				{kind: "onyx.Tooltip", content: "New Folder..."}
			]},
			{name: "reloadAll", kind: "onyx.TooltipDecorator", components: [
				{kind: "onyx.IconButton", src: "$harmonia/images/folder_reload.png", onclick: "reloadClick"},
				{kind: "onyx.Tooltip", content: "reload ..."}
			]},
			{name: "newFile", kind: "onyx.TooltipDecorator", components: [
				{kind: "onyx.IconButton", src: "$harmonia/images/document_new.png", onclick: "newFileClick"},
				{kind: "onyx.Tooltip", content: "New File..."}
			]},
			{name: "renameFile", kind: "onyx.TooltipDecorator", components: [
				{kind: "onyx.IconButton", src: "$harmonia/images/document_edit.png", onclick: "renameClick"},
				{kind: "onyx.Tooltip", content: "Rename..."}
			]},
			{name: "copyFile", kind: "onyx.TooltipDecorator", components: [
				{kind: "onyx.IconButton", src: "$harmonia/images/copy.png", onclick: "copyClick"},
				{kind: "onyx.Tooltip", content: "Copy..."}
			]},
			{name: "deleteFile", kind: "onyx.TooltipDecorator", components: [
				{kind: "onyx.IconButton", src: "$harmonia/images/document_delete.png", onclick: "deleteClick"},
				{kind: "onyx.Tooltip", content: "Delete..."}
			]}
		]},

		{kind: "Scroller", fit: true, components: [
			{name: "serverNode", kind: "Node", classes: "enyo-unselectable", showing: false, file: {id: "", name: "server", isServer: true, service: null}, content: "server", icon: "$service/images/antenna.png", expandable: true, expanded: true, onExpand: "nodeExpand", onNodeTap: "nodeTap"}
		]},

		// track selection of nodes. here, selection Key is file or folderId. Selection value is the node object
		{kind: "Selection", onSelect: "select", onDeselect: "deselect"},

		{name: "service", kind: "FileSystemService"},
		{name: "errorPopup", kind: "Ares.ErrorPopup", msg: "Service returned an error"},
		{name: "nameFilePopup", kind: "NamePopup", type: "file", fileName:"", placeHolder: "File Name", onCancel: "newFileCancel", onConfirm: "newFileConfirm"},
		{name: "nameFolderPopup", kind: "NamePopup", type: "folder", fileName: "", placeHolder: "Folder Name", onCancel: "newFolderCancel", onConfirm: "newFolderConfirm"},
		{name: "nameCopyPopup", kind: "NamePopup", title: "Name for copy of", fileName: "Copy of foo.js", onCancel: "copyFileCancel", onConfirm: "copyFileConfirm"},
		{name: "deletePopup", kind: "DeletePopup", onCancel: "deleteCancel", onConfirm: "deleteConfirm"},
		{name: "renamePopup", kind: "RenamePopup", title: "New name for ", fileName: "foo.js", onCancel: "renameCancel", onConfirm: "renameConfirm"}
	],
	// warning: this variable duplicates an information otherwise stored in this.$.selection
	// BUT, retrieving it through this.$.selection.getSelected is not handy as this function
	// return an object (hash) which needs to be scanned to retrieve the selected value
	selectedFile: null,
	selectedNode: null,
	debug: false,
	create: function() {
		this.inherited(arguments);
	},
	connectService: function(inService) {
		this.projectUrlReady = false; // Reset the project information
		this.clear() ;
		this.$.service.connect(inService);
		return this ;
	},
	/**
	 * @param {Object} inFsService a FileSystemService implementation, as listed in ProviderList
	 */
	setConfig: function(inProjectData) {
		if (this.debug) this.log("config:", inProjectData);
		this.projectData = inProjectData;

		var serverNode = this.$.serverNode;
		var nodeName = inProjectData.getName();
		var folderId = inProjectData.getFolderId();
		var service = inProjectData.getService();
		serverNode.hide();

		// connects to a service that provides access to a
		// (possibly remote & always asynchronous) file system
		this.connectService(service);

		// Get extra info such as project URL
		var req = this.$.service.propfind(folderId, 0);
		req.response(this, function(inSender, inValue) {
			var projectUrl = service.getConfig().origin + service.getConfig().pathname + "/file" + inValue.path;
			this.projectData.setProjectUrl(projectUrl);
			this.projectUrlReady = true;
			if (this.$.service.isOk()) {
				serverNode.setContent(nodeName);
				serverNode.file = {id: folderId, name: nodeName, isDir: true, isServer:true, path: nodeName, service: null};
				serverNode.setExpanded(true);
				serverNode.effectExpanded();
				this.refreshFileTree();
				serverNode.render() ;
			} else {
				req.fail("Service NOK");
				this.projectData.setProjectUrl("");
			}
		});
		req.error(this, function(inSender, inError) {
			this.projectData.setProjectUrl("");
			throw new Error(inError + " from " + inConfig.filesystem);
		});

		return this ;
	},
	hideFileOpButtons: function() {
		this.$.newFolder.hide();
		this.$.newFile.hide();
		this.$.reloadAll.hide();
		this.$.renameFile.hide();
		this.$.copyFile.hide();
		this.$.deleteFile.hide();
		return this ;
	},
	showNewFolderButton: function() {
		this.$.newFolder.show();
		return this ;
	},
	showFileOpButtons: function() {
		this.$.newFolder.show();
		this.$.reloadAll.show();
		this.$.newFile.show();
		this.$.renameFile.show();
		this.$.copyFile.show();
		this.$.deleteFile.show();
		return this ;
	},
	clear: function() {
		var server = this.$.serverNode;
		if (this.debug) this.log("clearing serverNode") ;
		enyo.forEach(
			this.getNodeFiles(server) ,
			function(n){
				n.destroy();
			}
		) ;
		server.hide();
		return this ;
	},
	reset: function() {
		this.$.serverNode.hide();
		if (this.$.service.isOk()) {
			if (this.debug) this.log("reseting serverNode") ;
			this.updateNodes(this.$.serverNode);
		}
		return this ;
	},
	//
	// probably should be in a Tree kind
	nodeTap: function(inSender, inEvent) {
		if (this.debug) this.log(inSender, "=>", inEvent);
		var node = inEvent.originator;
		this.$.selection.select(node.file.id, node);
		if (!node.file.isDir) {
			this.doFileClick({file: node.file});
		} else {
			this.doFolderClick({file: node.file});
		}
		// handled here (don't bubble)
		return true;
	},
	nodeDblClick: function(inSender, inEvent) {
		if (this.debug) this.log(inSender, "=>", inEvent);
		var node = inEvent.originator;
		// projectUrl in this.projectData is set asynchonously.  Do not try to
		// open anything before it is available.  Also do not
		// try to open top-level root & folders.
		if (!node.file.isDir && !node.file.isServer && this.projectUrlReady) {
			this.doFileDblClick({file: node.file, projectData: this.projectData});
		}
		// handled here (don't bubble)
		return true;
	},
	select: function(inSender, inEvent) {
		if (this.debug) this.log(inSender, "=>", inEvent);
		this.selectedNode=inEvent.data;
		this.selectedFile=inEvent.data.file;
		inEvent.data.file.service = this.$.service;
		inEvent.data.$.caption.applyStyle("background-color", "lightblue");
		this.doSelect({file: this.selectedFile});
		// handled here (don't bubble)
		return true;
	},
	deselect: function(inSender, inEvent) {
		if (this.debug) this.log(inSender, "=>", inEvent);
		if (inEvent.data && inEvent.data.$.caption) {
			inEvent.data.$.caption.applyStyle("background-color", null);
		}
		this.doDeselect({file: this.selectedFile});
		this.selectedFile=null;
		// handled here (don't bubble)
		return true;
	},
	// Note: this function does not recurse
	updateNodes: function(inNode) {
		this.startLoading(inNode);
		if (this.debug) this.log(inNode) ;
		return this.$.service.listFiles(inNode.file.id)
			.response(this, function(inSender, inFiles) {
				var sortedFiles = inFiles.sort(this.fileNameSort) ;
				if (inFiles && !this.$.serverNode.showing) {
					this.$.serverNode.show();
				}
				
				if (this.debug) this.log("updating node content of " + inNode.name ) ;
				this.updateNodeContent(inNode, sortedFiles);
				inNode.render() ;
			})
			.response(this, function() {
				this.stopLoading(inNode);
			})
			.error(this, function() {
				this.stopLoading(inNode);
			})
			;
	},
	startLoading: function(inNode) {
		inNode.$.extra.setContent('&nbsp;<img src="' + enyo.path.rewrite("$service/images/busy.gif") + '"/>');
	},
	stopLoading: function(inNode) {
		inNode.$.extra.setContent("");
	},
    updateNodeContent: function(inNode, files) {
		var i = 0 , nfiles, rfiles, res, modified = 0, newControl ;
		rfiles = this.filesToNodes(files) ; // with prefix in name
		nfiles = this.getNodeFiles(inNode) ;

		while ( i < nfiles.length || i < rfiles.length) {
			if (this.debug) this.log("considering node " + (nfiles[i] ? nfiles[i].name : '<none>') + ' and file ' + (rfiles[i] ? rfiles[i].name : '<none>') );

			res = i >= nfiles.length ? 1 
			    : i >= rfiles.length ? -1 
			    :                      this.fileNameSort(nfiles[i], rfiles[i]) ; 

			// remember that these file lists are sorted
			switch(res) {
				case -1: // remote file removed
					if (this.debug) this.log(nfiles[i].name + " was removed") ;
					nfiles[i].destroy() ;
				    modified = 1;
					nfiles = this.getNodeFiles(inNode) ;
					// node file list reduced by one, must not increment i
					break ;

				case 0: // no change
					i++ ;
					break ;

				case 1: // file added
				    if (this.debug) this.log(rfiles[i].name + " was added") ;
					newControl = inNode.createComponent( rfiles[i] ) ;
					nfiles = this.getNodeFiles(inNode) ;
					// FIXME: ENYO-1337
					//if (nfiles[i]) {
					//	var justAdded = inNode.controls.pop() ;
					//	inNode.controls.splice(i+4, 0, justAdded);
					//}
				    modified = 1;
					i++ ;
					break ;
			}

		}

		if (modified) {
			inNode.$.client.render();
		}
	},
	compareFiles: function(inFilesA, inFilesB) {
		if (inFilesA.length != inFilesB.length) {
			return false;
		}
		for (var i in inFilesA) {
			var f0 = inFilesA[i], f1 = inFilesB[i];
			if (f0.id !== f1.id) {
				return false;
			}
		}
		return true;
	},
	// Sort files by name, case-insensitively
	// TODO: I18N, and possibly platform-specific sort order
	fileNameSort: function(a, b) {
		var lowA, lowB;
		lowA=a.name.toLowerCase();
		lowB=b.name.toLowerCase();
		// compare lower-case version of name
		if (lowA < lowB) {
			return -1;
		} else if (lowB < lowA) {
			return 1;
		} else {
			// Files that differ only in case are sorted lexicographically
			if (a.name < b.name) {
				return -1;
			} else if (b.name < a.name) {
				return 1;
			} else {
				return 0;
			}
		}
	},
	getNodeFiles: function(inNode) {
		var hasPrefix = function(e){ 
			return (e.name.slice(0,1) === '$') ;
		} ;
		// getComponents only return the graphical items
		return inNode.getControls().filter( hasPrefix ).sort(this.fileNameSort) ;
	},
	filesToNodes: function(inFiles) {
		var nodes = [];
		inFiles.sort(this.fileNameSort); // TODO: Other sort orders
		for (var i=0, f; f=inFiles[i]; i++) {
			nodes.push({
				file: f,
				name: '$' + f.name, // prefix avoids clobberring non-files components like icon
				content: f.name,
				expandable: f.isDir,
				icon: "$service/images/" + (f.isDir ? "folder.png" : "file.png")
			});
		}
		return nodes;
	},
	nodeExpand: function(inSender, inEvent) {
		if (this.debug) this.log(inSender, "=>", inEvent);

		// the originating node is arbitrarily deep
		var node = inEvent.originator;
		if (this.debug) this.log("nodeExpand called while node Expanded is " + node.expanded) ;
		// update icon for expanded state
		if (node.file.isDir) {
			node.setIcon("$service/images/" + (node.expanded ? "folder-open.png" : "folder.png"));
		}
		// handle lazy-load when expanding
		if (node.expanded) {
			this.updateNodes(node).
				response(this, function() {
					node.effectExpanded();
				});
			// tell the event originator that node expansion should be deferred
			inEvent.wait = true;
		}
		// handled here (don't bubble)
		return true;
	},
	copyName: function(inName) {
		return "Copy of "+inName;
	},
	reloadClick: function(inSender, inEvent) {
		if (this.debug) this.log(inSender, "=>", inEvent);
		this.refreshFileTree();
	},

	// Both parameters are optional. node is null for the "top" refresh call. Node is set
	// for inner (async) recursive calls. toSelectId is optional. refresh will select this
	// entry if specified. Nothing is selected otherwise.
	refreshFileTree: function(node, toSelectId) {
		// Warning: this function fires several async events in each recursion. So knowing
		// when the refresh is over is challenging ...
		if (this.debug) this.log(this) ;
		var target = node || this.$.serverNode ;
		if (target.kind !== 'Node') {
			this.showErrorPopup("Internal Error");
			this.error("called with kind " + target.kind + " instead of Node");
			return ;
		}
		if (this.debug) this.log('running refreshFileTree on ' + target.kind + ' with ' +
					 target.controls.length + ' controls with content ' + target.content +
					 ' force select ' + toSelectId );

		this.updateNodes(target).
			response(this, function(inSender, inFiles) {
				// expand node if it has clients (sub-nodes) and is expanded
				target.effectExpanded();

				enyo.forEach(this.getNodeFiles(target), function(f) {
					var c = target.$[f.name] ; // c is a node
					if (this.debug) this.log('running INNER function of refreshFileTree on ' + f.name +
								 ' id ' + c.file.id);
					if ( c.file.id === toSelectId ) {
						if (this.debug) this.log('force select of ' + c.file.id);
						this.$.selection.select(c.file.id, c);
						this.$.scroller.scrollIntoView(c, true);
						// force a "click" event when the item is selected
						this.doFolderClick({file: c.file});
					}
					c.effectExpanded() ;
					if (f.expanded) {
						this.refreshFileTree(c,toSelectId);
					}
				}, this);
			});
	},

	// User Interaction for New File op
{'fraction_non_alphanumeric': 0.16186731838628554, 'fraction_numerical': 0.0021251726702794602, 'mean_word_length': 3.542149292149292, 'pattern_counts': {'":': 0, '<': 21, '<?xml version=': 0, '>': 25, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '7633329', 'n_tokens_mistral': 10742, 'n_tokens_neox': 9516, 'n_words': 2270}