aboutsummaryrefslogtreecommitdiff
path: root/tools
diff options
context:
space:
mode:
authortim-zh <tim.zhlobich@gmail.com>2016-11-13 22:20:06 +0300
committertim-zh <tim.zhlobich@gmail.com>2016-11-13 22:20:06 +0300
commit2ef3cc9b722da0d0b61966871ba0a0b24e0d7739 (patch)
tree2be94eca584894c9ee4066923d86cb971541d079 /tools
parentfd849d293448d55c6bcb6f8440f44838b51fc860 (diff)
downloadcbt-2ef3cc9b722da0d0b61966871ba0a0b24e0d7739.tar.gz
cbt-2ef3cc9b722da0d0b61966871ba0a0b24e0d7739.tar.bz2
cbt-2ef3cc9b722da0d0b61966871ba0a0b24e0d7739.zip
example browser
Diffstat (limited to 'tools')
-rw-r--r--tools/gui/resources/web/definitions.js223
-rw-r--r--tools/gui/resources/web/index.html52
-rw-r--r--tools/gui/resources/web/main.js150
-rw-r--r--tools/gui/resources/web/styles.css75
-rw-r--r--tools/gui/src/JettyServer.scala4
-rw-r--r--tools/gui/src/Main.scala63
6 files changed, 441 insertions, 126 deletions
diff --git a/tools/gui/resources/web/definitions.js b/tools/gui/resources/web/definitions.js
new file mode 100644
index 0000000..d1313d4
--- /dev/null
+++ b/tools/gui/resources/web/definitions.js
@@ -0,0 +1,223 @@
+function ajax(url, data, method) {
+ let f = method == "post" ? $.post : $.get;
+ return f("/api" + url, data).fail(e =>
+ Notifications.showFail(e)
+ );
+}
+
+function getFlags() {
+ return "readme/" + $("#readme-flag")[0].checked
+ + " dotty/" + $("#dotty-flag")[0].checked
+ + " uberJar/" + $("#uberJar-flag")[0].checked
+ + " wartremover/" + $("#wartremover-flag")[0].checked;
+}
+
+let ProjectLocation = {
+ _base: "",
+
+ init: function (container, nameInput) {
+ this._container = container;
+ nameInput.keyup(function () {
+ ProjectLocation.updateName(this.value);
+ });
+ ajax("/cwd").done(path => {
+ ProjectLocation._container.append(path[0]);
+ ProjectLocation._base = ProjectLocation._container.html();
+ });
+ },
+
+ updateName: function (x) {
+ this._container.html(this._base + (x ? "/" + x : ""));
+ }
+};
+
+let Popup = {
+ init: function (container, table) {
+ this._container = container;
+ this._table = table;
+ document.body.onkeydown = function (event) {
+ if (event.keyCode == 27)
+ Popup.hide();
+ };
+ },
+
+ show: function (contents) {
+ this._container.show();
+ this._table.html(contents);
+ },
+
+ hide: function () {
+ this._container.hide();
+ this._table.html("");
+ }
+};
+
+let Notifications = {
+ init: function () {
+ Notification.requestPermission();
+ },
+
+ show: function (text, title) {
+ new Notification(title || "", {body: text});
+ },
+
+ showFail: function (e) {
+ let head = e.status == 0 ? "Error" : e.status + " " + e.statusText;
+ let body = e.status == 0 ? "No response from UI server." : e.responseText;
+ this.show(body, head);
+ }
+};
+
+let Dependencies = {
+ _list: [],
+
+ init: function (searchBtn, queryInput, dependenciesNode) {
+ this._queryInput = queryInput;
+ queryInput.keyup(function handleSearchInput(event) {
+ if (event.keyCode == 13)
+ Dependencies.search();
+ });
+ this._dependenciesNode = dependenciesNode;
+ },
+
+ serialize: function () {
+ return this._list.length == 0 ? "" :
+ this._list.map(l => l.group + "/" + l.artifact + "/" + l.version).reduce((a, b) => a + " " + b);
+ },
+
+ search: function () {
+ let query = this._queryInput.val();
+ if (query) {
+ ajax("/dependency", {query: query}).done(data => {
+ let entries = data.response.docs.map(x => ({
+ group: x.g,
+ artifact: x.a
+ }));
+ Popup.show(Dependencies._makeRowsFrom(entries, Dependencies.selectDependency));
+ });
+ document.activeElement.blur();
+ }
+ },
+
+ selectDependency: function (selected) {
+ ajax("/dependency/version", {group: selected.group, artifact: selected.artifact}).done(data => {
+ let versions = data.response.docs.map(x => ({version: x.v}));
+ Popup.show(Dependencies._makeRowsFrom(versions, function (version) {
+ Dependencies.selectDependencyVersion(selected.group, selected.artifact, version.version);
+ }));
+ });
+ },
+
+ selectDependencyVersion: function (group, artifact, version) {
+ let dependency = {group: group, artifact: artifact, version: version};
+ this._list.push(dependency);
+ let scalaName = artifact.match(/^(.+)_\d+\.\d+$/);
+ let name = scalaName && scalaName[1] ? scalaName[1] : artifact;
+ let depDiv = $("<div class='entry removable'>" + name + " " + version + "</div>");
+ depDiv.click(function () {
+ Dependencies.removeDependency(dependency, depDiv);
+ });
+ this._dependenciesNode.append(depDiv);
+ Popup.hide();
+ },
+
+ removeDependency: function (d, div) {
+ this._list.splice(this._list.indexOf(d), 1);
+ div.remove();
+ },
+
+ _makeRowsFrom: function (results, rowAction) {
+ if (results.length == 0) {
+ return [];
+ } else {
+ let rows = [];
+ let row = $("<tr></tr>");
+ rows.push(row);
+ let fields = [];
+ for (let field in results[0])
+ if (results[0].hasOwnProperty(field)) {
+ fields.push(field);
+ $("<td>" + field + "</td>").appendTo(row);
+ }
+ results.forEach(result => {
+ let row = $("<tr></tr>");
+ row.click(function () {
+ rowAction(result);
+ });
+ rows.push(row);
+ fields.forEach(field => $("<td>" + result[field] + "</td>").appendTo(row));
+ });
+ return rows;
+ }
+ }
+};
+
+let Examples = {
+ fetchExamples: () => {
+ let examplesContainer = $("#examples");
+ examplesContainer.html("");
+ ajax("/examples").done(data => {
+ data.forEach(name => {
+ var example = $("<div class='link-btn'>" + name + "</div>");
+ example.click(() => Examples.selectExample(name));
+ example.appendTo(examplesContainer);
+ });
+ });
+ },
+
+ selectExample: name => {
+ $("#examples-title").hide();
+ let selectedExample = $("#selected-example");
+ selectedExample.html(name);
+ selectedExample.show();
+ ProjectLocation.updateName(name);
+ $("#examples").hide();
+ $("#example-browser").show();
+ $("#copy-project-btn").show();
+
+ Examples._fetchExampleFiles(name);
+ },
+
+ unselectExample: () => {
+ $("#examples-title").show();
+ $("#selected-example").hide();
+ ProjectLocation.updateName("");
+ $("#examples").show();
+ $("#example-browser").hide();
+ $("#copy-project-btn").hide();
+ $("#code-browser").hide();
+ },
+
+ _fetchExampleFiles: name => {
+ let fileBrowser = $("#file-browser");
+ fileBrowser.html("");
+ ajax("/example/files", {name: name}).done(data => {
+ Examples._appendTree(data, fileBrowser, 0);
+ });
+ },
+
+ selectedFileNode: null,
+
+ _appendTree: function (node, parent, dirDepth) {
+ let div = $("<div class='browser-node'>" + node.name + "</div>");
+ div.appendTo(parent);
+ if (dirDepth % 2 == 0)
+ div.addClass("even-node");
+ if (node.children) {
+ node.children.forEach(x => Examples._appendTree(x, div, dirDepth + 1));
+ } else {
+ div.addClass("file-node");
+ div.click(() => {
+ if (Examples.selectedFileNode)
+ Examples.selectedFileNode.removeClass("selected-node");
+ Examples.selectedFileNode = div;
+ Examples.selectedFileNode.addClass("selected-node");
+ ajax("/example/file", {path: node.path}).done(data => {
+ var codeBrowser = $("#code-browser");
+ codeBrowser.show();
+ codeBrowser.html(data);
+ });
+ });
+ }
+ }
+};
diff --git a/tools/gui/resources/web/index.html b/tools/gui/resources/web/index.html
index 9ad45a5..100c65b 100644
--- a/tools/gui/resources/web/index.html
+++ b/tools/gui/resources/web/index.html
@@ -10,33 +10,57 @@
</head>
<body>
<div id="popup">
- <div onclick="hidePopup()">×</div>
+ <div class="stroke" onclick="Popup.hide()">×</div>
<table id="popup-table"></table>
</div>
<div class="container">
<h1>CBT bootstrap</h1>
- <div id="cwd" class="entry"><span>current dir:</span> </div>
- <div class="entry"><input type="text" id="name" placeholder="project name"></div>
- <div class="entry"><input type="text" id="package" placeholder="default package"></div>
+ <button class="small-btn" onclick="setFlowCreate()" id="flow-create-btn">create from scratch</button>
+ or
+ <button class="small-btn" onclick="setFlowCopy()" id="flow-copy-btn">copy an example</button>
<hr>
- <div class="entry"><input type="text" id="query" placeholder="add maven dependency" onkeyup="handleSearchInput(event)"></div>
- <button class="small-btn" onclick="search()">search</button>
- <div id="dependencies"></div>
- <hr>
+ <div id="cwd" class="entry"><span>project location:</span> </div>
- <input type="checkbox" id="readme-flag"><label for="readme-flag">readme.md</label>
- <input type="checkbox" id="dotty-flag"><label for="dotty-flag">dotty</label>
- <input type="checkbox" id="uberJar-flag"><label for="uberJar-flag">uber jar</label>
- <input type="checkbox" id="wartremover-flag"><label for="wartremover-flag">wartremover</label>
- <hr>
+ <div id="flow-create">
+ <div class="entry"><input type="text" id="name" placeholder="project name"></div>
+ <div class="entry"><input type="text" id="package" placeholder="default package"></div>
+ <hr>
+
+ <div class="entry"><input type="text" id="query" placeholder="add maven dependency"></div>
+ <button class="small-btn" onclick="Dependencies.search()" id="search-btn">search</button>
+ <div id="dependencies"></div>
+ <hr>
+
+ <input type="checkbox" id="readme-flag"><label for="readme-flag">readme.md</label>
+ <input type="checkbox" id="dotty-flag"><label for="dotty-flag">dotty</label>
+ <input type="checkbox" id="uberJar-flag"><label for="uberJar-flag">uber jar</label>
+ <input type="checkbox" id="wartremover-flag"><label for="wartremover-flag">wartremover</label>
+ <hr>
+
+ <button id="create-project-btn" onclick="createProject()">create project</button>
+ </div>
- <button id="create-project" onclick="createProject()">create project</button>
+ <div id="flow-copy">
+ <span class="entry">
+ <span id="examples-title">select an example:</span>
+ <span class="removable" id="selected-example" onclick="Examples.unselectExample()"></span>
+ </span>
+ <hr>
+ <div id="examples"></div>
+ <div id="example-browser">
+ <div id="file-browser"></div>
+ <pre id="code-browser"><code></code></pre>
+ </div>
+ <hr>
+ <button id="copy-project-btn" onclick="copyProject()">copy example</button>
+ </div>
</div>
<script src="jquery-3.1.1.min.js"></script>
+<script src="definitions.js"></script>
<script src="main.js"></script>
</body>
</html>
diff --git a/tools/gui/resources/web/main.js b/tools/gui/resources/web/main.js
index b4fe38b..139795a 100644
--- a/tools/gui/resources/web/main.js
+++ b/tools/gui/resources/web/main.js
@@ -1,129 +1,73 @@
-Notification.requestPermission();
+Notifications.init();
-$.get("/api/cwd").done(cwd => {
- $("#cwd").append(cwd[0]);
-}).fail(e => notifyFail(e));
+Popup.init($("#popup"), $("#popup-table"));
-document.body.onkeydown = function (event) {
- if (event.keyCode == 27)
- hidePopup();
-};
+Dependencies.init($("#search-btn"), $("#query"), $("#dependencies"));
-$("#create-project")[0].disabled = false;
+ProjectLocation.init($("#cwd"), $("#name"));
-let dependencies = [];
+["#create-project-btn", "#copy-project-btn", "#flow-create-btn", "#flow-copy-btn"].forEach(id => $(id)[0].disabled = false);
-function getFlags() {
- return "readme/" + $("#readme-flag")[0].checked
- + " dotty/" + $("#dotty-flag")[0].checked
- + " uberJar/" + $("#uberJar-flag")[0].checked
- + " wartremover/" + $("#wartremover-flag")[0].checked;
+$("#flow-copy-btn")[0].style.width = $("#flow-create-btn")[0].offsetWidth + "px";
+
+function setFlowCreate() {
+ $("#flow-create").show();
+ $("#flow-copy").hide();
+
+ let createBtn = $("#flow-create-btn");
+ let copyBtn = $("#flow-copy-btn");
+ createBtn.blur();
+ createBtn[0].disabled = true;
+ copyBtn[0].disabled = false;
+
+ ProjectLocation.updateName($("#name").val());
}
function createProject() {
- let button = $("#create-project")[0];
+ let button = $("#create-project-btn")[0];
let buttonText = button.innerHTML;
button.innerHTML = "...";
button.blur();
button.disabled = true;
- $.post("/api/project", {
+ ajax("/project/new", {
name: $("#name").val(),
pack: $("#package").val(),
- dependencies: dependencies.length == 0 ? "" :
- dependencies.map(l => l.group + "/" + l.artifact + "/" + l.version).reduce((a, b) => a + " " + b),
+ dependencies: Dependencies.serialize(),
flags: getFlags()
- }).done(() => {
- notify("Done.");
- }).fail(e =>
- notifyFail(e)
- ).always(() => {
+ }, "post").done(() => {
+ Notifications.show("Done.");
+ }).always(() => {
button.innerHTML = buttonText;
button.disabled = false;
});
}
-function handleSearchInput(event) {
- if (event.keyCode == 13)
- search();
-}
-function search() {
- let query = $("#query").val();
- if (query) {
- $.get("/api/dependency", { query: query }).done(data => {
- let entries = data.response.docs.map(x => ({
- group: x.g,
- artifact: x.a
- }));
- showPopup(makeRowsFrom(entries, selectDependency));
- }).fail(e => notifyFail(e));
- document.activeElement.blur();
- }
-}
-function selectDependency(selected) {
- $.get("/api/dependency/version", { group: selected.group, artifact: selected.artifact }).done(data => {
- let versions = data.response.docs.map(x => ({ version: x.v }));
- $("#popup-table").html(makeRowsFrom(versions, function (version) {
- selectDependencyVersion(selected.group, selected.artifact, version.version);
- }));
- }).fail(e => notifyFail(e));
-}
-function selectDependencyVersion(group, artifact, version) {
- let dependency = { group: group, artifact: artifact, version: version };
- dependencies.push(dependency);
- let scalaName = artifact.match(/^(.+)_\d+\.\d+$/);
- let name = scalaName && scalaName[1] ? scalaName[1] : artifact;
- var depDiv = $("<div class='entry removable'>" + name + " " + version + "</div>");
- depDiv.click(function () {
- removeDependency(dependency, depDiv);
- });
- $("#dependencies").append(depDiv);
- hidePopup();
-}
-function removeDependency(d, div) {
- dependencies.splice(dependencies.indexOf(d), 1);
- div.remove();
-}
+function setFlowCopy() {
+ $("#flow-copy").show();
+ $("#flow-create").hide();
-function showPopup(contents) {
- $("#popup").show();
- $("#popup-table").html(contents);
-}
-function hidePopup() {
- $("#popup").hide();
- $("#popup-table").html("");
-}
+ let createBtn = $("#flow-create-btn");
+ let copyBtn = $("#flow-copy-btn");
+ copyBtn.blur();
+ copyBtn[0].disabled = true;
+ createBtn[0].disabled = false;
-function makeRowsFrom(results, rowAction) {
- if (results.length == 0) {
- return [];
- } else {
- let rows = [];
- let row = $("<tr></tr>");
- rows.push(row);
- let fields = [];
- for (let field in results[0])
- if (results[0].hasOwnProperty(field)) {
- fields.push(field);
- $("<td>" + field + "</td>").appendTo(row);
- }
- results.forEach(result => {
- let row = $("<tr></tr>");
- row.click(function () {
- rowAction(result);
- });
- rows.push(row);
- fields.forEach(field => $("<td>" + result[field] + "</td>").appendTo(row));
- });
- return rows;
- }
-}
+ Examples.unselectExample();
-function notify(text, title) {
- new Notification(title || "", { body: text });
+ Examples.fetchExamples();
}
-function notifyFail(e) {
- let head = e.status == 0 ? "Error" : e.status + " " + e.statusText;
- let body = e.status == 0 ? "No response from UI server." : e.responseText;
- notify(body, head);
+function copyProject() {
+ let name = $("#selected-example").html();
+ let button = $("#copy-project-btn")[0];
+ let buttonText = button.innerHTML;
+ button.innerHTML = "...";
+ button.blur();
+ button.disabled = true;
+ ajax("/project/copy", {name: name}, "post").done(() => {
+ Notifications.show("Done.");
+ }).always(() => {
+ button.innerHTML = buttonText;
+ button.disabled = false;
+ });
}
diff --git a/tools/gui/resources/web/styles.css b/tools/gui/resources/web/styles.css
index 6aa45e9..759dd06 100644
--- a/tools/gui/resources/web/styles.css
+++ b/tools/gui/resources/web/styles.css
@@ -30,12 +30,16 @@ hr {
border-bottom: transparent solid 1px;
}
+pre {
+ margin: 0 0 1em 0;
+}
+
button, .small-btn {
background: #dc322f;
color: #fff;
font-size: 1em;
padding: 0.2em;
- border: transparent solid 0.1em;
+ border: none;
border-radius: 1em;
text-transform: uppercase;
cursor: pointer;
@@ -47,9 +51,14 @@ button, .small-btn {
padding: 0 1em;
}
+.link-btn:hover {
+ color: #dc322f;
+ cursor: pointer;
+}
+
button:hover {
- background: #073642;
- border: #fff solid 0.1em;
+ background: #fff;
+ color: #002B36;
}
button:active {
@@ -57,7 +66,8 @@ button:active {
}
button:disabled {
- background: transparent;
+ background: #073642;
+ color: #fff;
cursor: default;
}
@@ -66,6 +76,7 @@ input[type="text"] {
color: #fff;
border: none;
width: 33%;
+ min-width: 15em;
font-size: 1em;
}
@@ -115,6 +126,10 @@ input[type="checkbox"]:checked + label:after {
border-color: #dc322f;
}
+.stroke {
+ text-shadow: 0 1px 0 #002B36, 0 -1px 0 #002B36, 1px 0 0 #002B36, -1px 0 0 #002B36;
+}
+
.container {
margin: 0 8%;
text-align: center;
@@ -138,10 +153,11 @@ button, .entry {
left: 0;
right: 0;
margin: 0 auto;
+ border: #002B36 solid 0.5em;
background: #073642;
width: 84%;
max-height: 80%;
- overflow-y: scroll;
+ overflow-y: auto;
text-align: center;
display: none;
z-index: 2;
@@ -194,3 +210,52 @@ button, .entry {
#popup tr:first-child:hover {
color: #002B36;
}
+
+#flow-create, #flow-copy {
+ display: none;
+}
+
+#example-browser {
+ width: 100%;
+ float: left;
+}
+
+#file-browser {
+ overflow-x: auto;
+ padding-right: 0.4em;
+ float: left;
+ max-width: 30%;
+}
+
+#code-browser {
+ background: #073642;
+ font-size: 1.2rem;
+ text-align: left;
+ padding: 0.4em;
+ overflow-x: auto;
+ max-height: 100%;
+}
+
+.browser-node {
+ font-size: 1.2rem;
+ text-align: left;
+ line-height: 1.2em;
+ background: #002B36;
+ padding: 0.2em 0 0 0.2em;
+}
+
+.browser-node > div {
+ margin-left: 2em;
+}
+
+.file-node {
+ cursor: pointer;
+}
+
+.file-node:hover, .selected-node {
+ color: #dc322f;
+}
+
+.even-node {
+ background: #073642;
+}
diff --git a/tools/gui/src/JettyServer.scala b/tools/gui/src/JettyServer.scala
index d6024c2..a64c6bc 100644
--- a/tools/gui/src/JettyServer.scala
+++ b/tools/gui/src/JettyServer.scala
@@ -27,7 +27,7 @@ abstract class JettyServer(port: Int, staticFilesUrl: String) {
response.setContentType("application/json")
response.setCharacterEncoding("UTF-8")
- route(request.getMethod, target, request.getParameter) match {
+ route(request.getMethod, target, request.getParameter, response.setContentType) match {
case Success(result) =>
response.getWriter.write(result)
case Failure(e: MalformedURLException) =>
@@ -63,5 +63,5 @@ abstract class JettyServer(port: Int, staticFilesUrl: String) {
println("UI server stopped.")
}
- def route(method: String, path: String, param: String => String): Try[String]
+ def route(method: String, path: String, param: String => String, setContentType: String => Unit): Try[String]
}
diff --git a/tools/gui/src/Main.scala b/tools/gui/src/Main.scala
index d7a9f7d..7bb299c 100644
--- a/tools/gui/src/Main.scala
+++ b/tools/gui/src/Main.scala
@@ -1,5 +1,7 @@
import java.io.{File, IOException}
import java.net.MalformedURLException
+import java.nio.file.attribute.BasicFileAttributes
+import java.nio.file.{FileVisitResult, Files, Path, SimpleFileVisitor}
import scala.io.Source
import scala.util.{Failure, Success, Try}
@@ -21,10 +23,13 @@ object Main {
def launchUi(projectDirectory: File, scalaMajorVersion: String): Unit = {
val staticBase = new File(cbt_home / "tools" / "gui" / "resources" / "web").toURI.toURL.toExternalForm
val server = new JettyServer(uiPort, staticBase) {
- override def route(method: String, path: String, param: String => String) = (method, path) match {
+ override def route(method: String,
+ path: String,
+ param: String => String,
+ setContentType: String => Unit) = (method, path) match {
case ("GET", "/cwd") =>
Success(s"""["$projectDirectory"]""")
- case ("POST", "/project") =>
+ case ("POST", "/project/new") =>
val name = param("name")
val defaultPackage = param("pack")
val dependencies = param("dependencies")
@@ -33,6 +38,14 @@ object Main {
new ProjectBuilder(name, defaultPackage, dependencies, flags, projectDirectory, scalaMajorVersion).build()
Success("[]")
}
+ case ("POST", "/project/copy") =>
+ val name = param("name")
+ val source = new File(cbt_home / "examples" / name)
+ val target = new File(projectDirectory.getAbsolutePath / name)
+ handleIoException {
+ new FileCopier(source, target).copy()
+ Success("[]")
+ }
case ("GET", "/dependency") =>
val query = param("query")
handleIoException(handleMavenBadResponse(searchDependency(query)))
@@ -40,6 +53,28 @@ object Main {
val group = param("group")
val artifact = param("artifact")
handleIoException(handleMavenBadResponse(searchDependencyVersion(group, artifact)))
+ case ("GET", "/examples") =>
+ handleIoException {
+ val names = new File(cbt_home / "examples").listFiles().filter(_.isDirectory).sortBy(_.getName)
+ .map('"' + _.getName + '"').mkString(",")
+ Success(s"[$names]")
+ }
+ case ("GET", "/example/files") =>
+ val name = param("name")
+ handleIoException {
+ val dir = new File(cbt_home / "examples" / name)
+ if (dir.exists())
+ Success(serializeTree(dir))
+ else
+ Failure(new IllegalArgumentException(s"Incorrect example name: $name"))
+ }
+ case ("GET", "/example/file") =>
+ setContentType("text/plain")
+ val path = param("path")
+ handleIoException {
+ val file = new File(path)
+ Success(Source.fromFile(file).mkString)
+ }
case _ =>
Failure(new MalformedURLException(s"Incorrect path: $path"))
}
@@ -82,4 +117,28 @@ object Main {
Failure(new Exception(s"Bad response from $maven_host: $result"))
}
+ private def serializeTree(file: File): String = {
+ val data = if (file.isDirectory)
+ s""","children":[${file.listFiles().sortBy(_.getName).map(serializeTree).mkString(",")}]"""
+ else
+ ""
+ s"""{"name":"${file.getName}","path":"${file.getAbsolutePath}"$data}"""
+ }
+
+ private class FileCopier(source: File, target: File) extends SimpleFileVisitor[Path] {
+
+ def copy() = Files.walkFileTree(source.toPath, this)
+
+ override def preVisitDirectory(dir: Path, attrs: BasicFileAttributes) = {
+ Files.createDirectories(target.toPath.resolve(source.toPath.relativize(dir)))
+ FileVisitResult.CONTINUE
+ }
+
+ override def visitFile(file: Path, attrs: BasicFileAttributes) = {
+ Files.copy(file, target.toPath.resolve(source.toPath.relativize(file)))
+ FileVisitResult.CONTINUE
+ }
+
+ }
+
}