├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── build.sbt ├── client └── src │ └── main │ └── scala │ └── client │ └── WootClient.scala ├── docs ├── poem.png ├── script.md ├── wootjs.key └── wootjs.pdf ├── project ├── build.properties └── plugins.sbt ├── server └── src │ └── main │ ├── resources │ ├── ace │ │ ├── ace.js │ │ ├── ext-chromevox.js │ │ ├── ext-elastic_tabstops_lite.js │ │ ├── ext-emmet.js │ │ ├── ext-keybinding_menu.js │ │ ├── ext-language_tools.js │ │ ├── ext-modelist.js │ │ ├── ext-old_ie.js │ │ ├── ext-searchbox.js │ │ ├── ext-settings_menu.js │ │ ├── ext-spellcheck.js │ │ ├── ext-split.js │ │ ├── ext-static_highlight.js │ │ ├── ext-statusbar.js │ │ ├── ext-textarea.js │ │ ├── ext-themelist.js │ │ ├── ext-whitespace.js │ │ ├── mode-markdown.js │ │ ├── snippets │ │ │ └── markdown.js │ │ ├── theme-merbivore.js │ │ ├── worker-coffee.js │ │ ├── worker-css.js │ │ ├── worker-javascript.js │ │ ├── worker-json.js │ │ ├── worker-lua.js │ │ ├── worker-php.js │ │ └── worker-xquery.js │ ├── editor.js │ ├── index.html │ ├── jquery-1.10.2.min.js │ ├── logback.xml │ ├── underscore-min.js │ └── ws.js │ └── scala │ ├── main.scala │ ├── static-routes.scala │ └── woot-routes.scala └── wootModel └── shared └── src ├── main └── scala │ └── woot │ └── Woot.scala └── test └── scala └── woot └── WootSpec.scala /.gitignore: -------------------------------------------------------------------------------- 1 | project 2 | target 3 | client/target/ 4 | server/target/ 5 | target/ 6 | woot-model/target/ 7 | .settings 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: scala 2 | 3 | jdk: 4 | - oraclejdk8 5 | 6 | script: 7 | - sbt wootModelJVM/test 8 | 9 | # From http://www.scala-sbt.org/0.13/docs/Travis-CI-with-sbt.html ... 10 | 11 | sudo: false 12 | 13 | cache: 14 | directories: 15 | - $HOME/.sbt 16 | - $HOME/.ivy2 17 | - $HOME/.coursier 18 | 19 | before_cache: 20 | # Tricks to avoid unnecessary cache updates 21 | - find $HOME/.ivy2 -name "ivydata-*.properties" -delete 22 | - find $HOME/.sbt -name "*.lock" -delete 23 | 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/d6y/wootjs.svg?branch=master)](https://travis-ci.org/d6y/wootjs) 2 | 3 | # WOOT with Scala.js 4 | 5 | * Collaborative text editing, using the WOOT algorithm. 6 | * Implemented in Scala, running on both the JVM and a JavaScript interpreter. 7 | 8 | ## For the Impatient 9 | 10 | $ sbt server/run 11 | 12 | Then open _http://127.0.0.1:8080/_ to edit a document. 13 | 14 | Open another browser at the same address, and you'll get the idea of collaboration. 15 | 16 | ## What is WOOT? 17 | 18 | WOOT is a collaborative text editing algorithm, allowing multiple users ("sites") to insert or delete characters (`WChar`) from a shared document (`WString`). The algorithm preserves the intention of users, and ensures that the text converges to the same state for all users. 19 | 20 | Its key properties are simplicity, and avoiding the need for a reliable network or vector clocks (it can be peer-to-peer). 21 | 22 | The key references are: 23 | 24 | * Oster _et al._ (2005) _Real time group editors without Operational transformation_, report paper 5580, INRIA - [PDF](http://www.loria.fr/~oster/pmwiki/pub/papers/OsterRR05a.pdf) 25 | 26 | * Oster _et al._ (2006) _Data Consistency for P2P Collaborative Editing_, CSCW'06 - [PDF](http://hal.archives-ouvertes.fr/docs/00/10/85/23/PDF/OsterCSCW06.pdf) 27 | 28 | WOOT stands for With Out [Operational Transforms](https://en.wikipedia.org/wiki/Operational_transform). 29 | 30 | ## Presentations 31 | 32 | I've spoken about this project at Scala Days 2015: [there's video](https://www.parleys.com/tutorial/towards-browser-server-utopia-scala-js-example-using-crdts) and also [the slides](https://speakerdeck.com/d6y/towards-browser-and-server-utopia-with-scala-dot-js-an-example-using-crdts). 33 | 34 | 35 | ## This Project 36 | 37 | This project contains a Scala implementation of WOOT. It has been compiled to JavaScript using Scala.js. 38 | In other words, this is an example of sharing one implementation (the WOOT model) in both a JavaScript and Scala context. 39 | 40 | WOOT is only the algorithm for text consistency. 41 | You need to bring your own editor and network layer. 42 | 43 | This example includes the [ACE](http://ace.c9.io/) editor, which is wired up to the 44 | Scala.js implementation of WOOT locally within the browser. 45 | Updates are sent over a web socket to a http4s server which also maintains a copy of the model, but on the JVM. 46 | 47 | ![Screen Shot of Editor being Used](docs/poem.png) 48 | 49 | ## Performance 50 | 51 | This is a simple implementation that is slow for bulk actions (e.g., paste and cut). 52 | 53 | To improve performance you will want to: 54 | 55 | - measure what's slow for your scenarios 56 | - batch messages between client and server (maybe) 57 | - optimize the `trim`, `canIntegrate`, and `indexOf` methods. 58 | 59 | I may get round to doing this at some point! 60 | 61 | ## What Happens When You Run the Web Server 62 | 63 | Running the sever code will likely produce: 64 | 65 | ``` 66 | $ sbt "project server" run 67 | [info] Loading global plugins from ... 68 | [info] Loading project definition from wootjs/project/project 69 | [info] Loading project definition from wootjs/project 70 | [info] Set current project to woot 71 | [info] Set current project to woot-server 72 | [info] Fast optimizing wootjs/client/target/scala-2.11/woot-client-fastopt.js 73 | [info] Running Main 74 | 2015-04-08 13:43:52 [run-main-0] INFO WootServer - Starting Http4s-blaze WootServer on '0.0.0.0:8080' 75 | ... 76 | ``` 77 | 78 | Notice that the Scala.js compiler has run on the _woot-client_ project, to convert the client Scala code into JavaScript. This JavaScript, _woot-client-fastopt.js_, is made available on the classpath of the server, so it can be included on the web page. The web page is _server/src/main/resources/index.html_. 79 | 80 | This reflects the structure of the project: 81 | 82 | * _client_ - Scala source code, to be compiled to JavaScript. 83 | * _server_ - Scala source code to run server-side, plus other assets to be served, such as HTML, CSS and plain old JavaScript. 84 | * _wootModel_ - Scala source code shared by both the client and server projects. This is the WOOT algorithm, implemented once, used in the JVM and the JavaScript runtime. 85 | 86 | 87 | ## Exploring the Code 88 | 89 | 1. _server/src/main/resources/index.html_ is the starting point for the client. This folder also contains a trivial websocket JavaScript client (_ws.js_) and the editor bindings (_editor.js_). 90 | 2. _editor.js_ creates a local instance of the "woot client" and kicks off the web socket interactions. 91 | 3. _client/src/main/scala/client/WootClient.scala_ is the exposed interface to the WOOT model. This is Scala compiled to JavaScript. 92 | 4. _server/src/main/scala/main.scala_ is the server that accepts and broadcasts WOOT operations to clients. 93 | 94 | 95 | ## Tests 96 | 97 | The tests for this project are implemented as [ScalaCheck](http://www.scalacheck.org/) properties. 98 | 99 | ### Running the tests in the JVM 100 | 101 | sbt> project wootModelJVM 102 | sbt> coverage 103 | sbt> test 104 | 105 | Then open _wootModel/jvm/target/scala-2.11/scoverage-report/index.html_ 106 | 107 | ## Publishing Woot Model 108 | 109 | * Follow [bintray-sbt publishing instructions](https://github.com/softprops/bintray-sbt#publishing) 110 | * `;wootModelJS/publish; wootModelJVM/publish` 111 | 112 | It will now be available with 113 | 114 | ```scala 115 | resolvers += "" at "http://dl.bintray.com/content//maven", 116 | libraryDependencies += "com.dallaway.richard" %%% "woot-model" % "", 117 | ``` 118 | 119 | ## Reference 120 | 121 | * [Exporting Scala.js APIs to JavaScript](http://www.scala-js.org/doc/export-to-javascript.html) 122 | * [Calling JavaScript from Scala.js](http://www.scala-js.org/doc/calling-javascript.html) 123 | * [Semantics of Scala.js](http://www.scala-js.org/doc/semantics.html) - exceptions to the rule that Scala.js prorgrams behave the same as Scala on the JVM. 124 | * [JavaScript interoperability ](http://www.scala-js.org/doc/js-interoperability.html) - including a list of [opaque types](http://stackoverflow.com/questions/27821841/working-with-opaque-types-char-and-long). 125 | * [µPickle 0.2.8](http://lihaoyi.github.io/upickle/) - the JVM and JavaScript JSON/case class serialization library used in this demo. 126 | * [Depending on Libraries](http://www.scala-js.org/doc/sbt/depending.html) -- both JavaScript and Scala.js. 127 | * [http4s](http://http4s.org/) - the server used in this demo. 128 | 129 | ## Scala.js Learning Path 130 | 131 | If you're new to Scala: 132 | 133 | * [Creative Scala](http://underscore.io/training/courses/creative-scala/) - a free course from Underscore teaching Scala using drawing primitives backed by Scala.js. 134 | 135 | And then... 136 | 137 | * [Scala-js.org Tutorial](http://www.scala-js.org/doc/tutorial.html) 138 | * [Hands-on Scala.js](http://lihaoyi.github.io/hands-on-scala-js/#Hands-onScala.js) 139 | 140 | 141 | # License 142 | 143 | Copyright 2015 Richard Dallaway 144 | 145 | Licensed under the Apache License, Version 2.0 (the "License"); 146 | you may not use this file except in compliance with the License. 147 | You may obtain a copy of the License at 148 | 149 | http://www.apache.org/licenses/LICENSE-2.0 150 | 151 | Unless required by applicable law or agreed to in writing, software 152 | distributed under the License is distributed on an "AS IS" BASIS, 153 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 154 | See the License for the specific language governing permissions and 155 | limitations under the License. 156 | -------------------------------------------------------------------------------- /build.sbt: -------------------------------------------------------------------------------- 1 | lazy val root = project.in(file(".")) 2 | .aggregate(client, server, modelJvm, modelJs) 3 | .dependsOn(client, server, modelJvm, modelJs) 4 | .settings(publish := {}, publishLocal := {}) 5 | 6 | lazy val commonSettings = Seq( 7 | version := "0.1.1", 8 | organization := "com.dallaway.richard", 9 | licenses += "Apache-2.0" -> url("http://www.apache.org/licenses/LICENSE-2.0"), 10 | scalaVersion := "2.11.7", 11 | scalacOptions ++= Seq( 12 | "-deprecation", 13 | "-encoding", "UTF-8", 14 | "-unchecked", 15 | "-feature", 16 | "-language:implicitConversions", 17 | "-language:postfixOps", 18 | "-language:higherKinds", 19 | "-Xlint", 20 | "-Xfatal-warnings", 21 | "-Yno-adapted-args", 22 | "-Ywarn-dead-code", 23 | "-Ywarn-numeric-widen", 24 | "-Ywarn-value-discard", 25 | "-Xfuture" 26 | ) 27 | ) 28 | 29 | val upickleVersion = "0.2.8" 30 | 31 | lazy val client = project.in(file("client")) 32 | .enablePlugins(ScalaJSPlugin) 33 | .settings(commonSettings: _*) 34 | .settings( 35 | name := "woot-client", 36 | testFrameworks += new TestFramework("scalacheck.ScalaCheckFramework"), 37 | javaOptions += "-Xmx2048m", // For tests, to avoid "OutOfMemoryError: Metaspace" 38 | libraryDependencies += "com.lihaoyi" %%% "upickle" % upickleVersion 39 | ) dependsOn(modelJs) 40 | 41 | lazy val http4s = Seq( 42 | resolvers += { 43 | // http4s dependsOn scalaz-stream wich is available at 44 | "Scalaz Bintray Repo" at "http://dl.bintray.com/scalaz/releases" 45 | }, 46 | libraryDependencies ++= { 47 | val http4sVersion = "0.10.0" 48 | Seq( 49 | "org.http4s" %% "http4s-blaze-server" % http4sVersion, 50 | "org.http4s" %% "http4s-dsl" % http4sVersion 51 | ) 52 | } 53 | ) 54 | 55 | lazy val server = project.in(file("server")) 56 | .settings(commonSettings: _*) 57 | .settings(Revolver.settings) 58 | .settings(http4s: _*) 59 | .settings( 60 | name := "woot-server", 61 | libraryDependencies ++= Seq( 62 | "com.lihaoyi" %%% "upickle" % upickleVersion, 63 | "ch.qos.logback" % "logback-classic" % "1.1.3" 64 | ), 65 | resources in Compile ++= { 66 | def andSourceMap(aFile: java.io.File) = Seq( 67 | aFile, 68 | file(aFile.getAbsolutePath + ".map") 69 | ) 70 | andSourceMap((fastOptJS in (client, Compile)).value.data) 71 | }, 72 | testOptions in Test += Tests.Argument(TestFrameworks.ScalaCheck, 73 | "-maxSize", "200", // Longer strings 74 | "-minSuccessfulTests", "250" // More tests 75 | ) 76 | ) dependsOn(modelJvm) 77 | 78 | lazy val wootModel = crossProject 79 | .settings(commonSettings: _*) 80 | .settings( 81 | name := "woot-model", 82 | libraryDependencies += "org.scalacheck" %%% "scalacheck" % "1.12.5" % "test" 83 | ) 84 | .jsSettings( 85 | libraryDependencies += "org.scala-js" %%% "scalajs-dom" % "0.8.0" 86 | ) 87 | 88 | lazy val modelJvm = wootModel.jvm 89 | lazy val modelJs = wootModel.js 90 | -------------------------------------------------------------------------------- /client/src/main/scala/client/WootClient.scala: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import scala.util.{Try,Failure,Success} 4 | import scala.scalajs.js 5 | import js.annotation.JSExport 6 | 7 | import woot._ 8 | import upickle._ 9 | 10 | @JSExport 11 | class WootClient( 12 | onLoad: js.Function1[String, Unit], 13 | onReceive: js.Function3[String,Boolean,Int,Unit]) { 14 | 15 | // This is the client copy of the document 16 | var doc = WString.empty() 17 | 18 | def openDocument(w: WString): Unit = { 19 | println(s"Becoming document: $w") 20 | doc = w 21 | onLoad(doc.text) 22 | } 23 | 24 | // The web socket works in terms of text 25 | type Json = String 26 | 27 | // 28 | // Local operations, producing a WChar to send across the network 29 | // 30 | 31 | @JSExport 32 | def insert(s: String, pos: Int): Json = { 33 | val (op, wstring) = doc.insert(s.head, pos) 34 | doc = wstring 35 | write(op) 36 | } 37 | 38 | @JSExport 39 | def delete(pos: Int): Json = { 40 | val (op, wstring) = doc.delete(pos) 41 | doc = wstring 42 | write(op) 43 | } 44 | 45 | // 46 | // Ingesting a remote operation or document 47 | // 48 | 49 | private[this] def applyOperation(op: Operation): Unit = { 50 | 51 | if (op.from == doc.site) { 52 | // We receive back operations we sent. 53 | // Useful for confirming receipt, but no action required. 54 | println(s"Ignoring echo of our own operation") 55 | } else { 56 | println(s"Applying $op") 57 | val (ops, wstring) = doc.integrate(op) 58 | 59 | // Become the updated document: 60 | doc = wstring 61 | 62 | // Side effects: 63 | ops.foreach { 64 | case InsertOp(ch, _) => onReceive(ch.alpha.toString, true, doc.visibleIndexOf(ch.id)) 65 | case DeleteOp(ch, _) => onReceive(ch.alpha.toString, false, doc.visibleIndexOf(ch.id)) 66 | } 67 | } 68 | } 69 | 70 | @JSExport 71 | def ingest(json: Json): Unit = { 72 | println(s"Ingesting: $json") 73 | 74 | // We can be sent a whole WString or an individual Operation. 75 | // We'll simply try to decode each type: 76 | val result = 77 | Try(read[Operation](json)).map(applyOperation) orElse 78 | Try(read[WString](json)).map(openDocument) 79 | 80 | } 81 | } -------------------------------------------------------------------------------- /docs/poem.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/d6y/wootjs/62b0a81342bb4497cc5fc8be5224153210b7dc9c/docs/poem.png -------------------------------------------------------------------------------- /docs/wootjs.key: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/d6y/wootjs/62b0a81342bb4497cc5fc8be5224153210b7dc9c/docs/wootjs.key -------------------------------------------------------------------------------- /docs/wootjs.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/d6y/wootjs/62b0a81342bb4497cc5fc8be5224153210b7dc9c/docs/wootjs.pdf -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=0.13.8 -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("org.scala-js" % "sbt-scalajs" % "0.6.5") 2 | addSbtPlugin("org.scoverage" % "sbt-scoverage" % "1.2.0") 3 | addSbtPlugin("io.spray" % "sbt-revolver" % "0.8.0") 4 | addSbtPlugin("me.lessis" % "bintray-sbt" % "0.3.0") -------------------------------------------------------------------------------- /server/src/main/resources/ace/ext-chromevox.js: -------------------------------------------------------------------------------- 1 | ace.define('ace/ext/chromevox', ['require', 'exports', 'module' , 'ace/editor', 'ace/config'], function(require, exports, module) { 2 | var cvoxAce = {}; 3 | cvoxAce.SpeechProperty; 4 | cvoxAce.Cursor; 5 | cvoxAce.Token; 6 | cvoxAce.Annotation; 7 | var CONSTANT_PROP = { 8 | 'rate': 0.8, 9 | 'pitch': 0.4, 10 | 'volume': 0.9 11 | }; 12 | var DEFAULT_PROP = { 13 | 'rate': 1, 14 | 'pitch': 0.5, 15 | 'volume': 0.9 16 | }; 17 | var ENTITY_PROP = { 18 | 'rate': 0.8, 19 | 'pitch': 0.8, 20 | 'volume': 0.9 21 | }; 22 | var KEYWORD_PROP = { 23 | 'rate': 0.8, 24 | 'pitch': 0.3, 25 | 'volume': 0.9 26 | }; 27 | var STORAGE_PROP = { 28 | 'rate': 0.8, 29 | 'pitch': 0.7, 30 | 'volume': 0.9 31 | }; 32 | var VARIABLE_PROP = { 33 | 'rate': 0.8, 34 | 'pitch': 0.8, 35 | 'volume': 0.9 36 | }; 37 | var DELETED_PROP = { 38 | 'punctuationEcho': 'none', 39 | 'relativePitch': -0.6 40 | }; 41 | var ERROR_EARCON = 'ALERT_NONMODAL'; 42 | var MODE_SWITCH_EARCON = 'ALERT_MODAL'; 43 | var NO_MATCH_EARCON = 'INVALID_KEYPRESS'; 44 | var INSERT_MODE_STATE = 'insertMode'; 45 | var COMMAND_MODE_STATE = 'start'; 46 | 47 | var REPLACE_LIST = [ 48 | { 49 | substr: ';', 50 | newSubstr: ' semicolon ' 51 | }, 52 | { 53 | substr: ':', 54 | newSubstr: ' colon ' 55 | } 56 | ]; 57 | var Command = { 58 | SPEAK_ANNOT: 'annots', 59 | SPEAK_ALL_ANNOTS: 'all_annots', 60 | TOGGLE_LOCATION: 'toggle_location', 61 | SPEAK_MODE: 'mode', 62 | SPEAK_ROW_COL: 'row_col', 63 | TOGGLE_DISPLACEMENT: 'toggle_displacement', 64 | FOCUS_TEXT: 'focus_text' 65 | }; 66 | var KEY_PREFIX = 'CONTROL + SHIFT '; 67 | cvoxAce.editor = null; 68 | var lastCursor = null; 69 | var annotTable = {}; 70 | var shouldSpeakRowLocation = false; 71 | var shouldSpeakDisplacement = false; 72 | var changed = false; 73 | var vimState = null; 74 | var keyCodeToShortcutMap = {}; 75 | var cmdToShortcutMap = {}; 76 | var getKeyShortcutString = function(keyCode) { 77 | return KEY_PREFIX + String.fromCharCode(keyCode); 78 | }; 79 | var isVimMode = function() { 80 | var keyboardHandler = cvoxAce.editor.keyBinding.getKeyboardHandler(); 81 | return keyboardHandler.$id === 'ace/keyboard/vim'; 82 | }; 83 | var getCurrentToken = function(cursor) { 84 | return cvoxAce.editor.getSession().getTokenAt(cursor.row, cursor.column + 1); 85 | }; 86 | var getCurrentLine = function(cursor) { 87 | return cvoxAce.editor.getSession().getLine(cursor.row); 88 | }; 89 | var onRowChange = function(currCursor) { 90 | if (annotTable[currCursor.row]) { 91 | cvox.Api.playEarcon(ERROR_EARCON); 92 | } 93 | if (shouldSpeakRowLocation) { 94 | cvox.Api.stop(); 95 | speakChar(currCursor); 96 | speakTokenQueue(getCurrentToken(currCursor)); 97 | speakLine(currCursor.row, 1); 98 | } else { 99 | speakLine(currCursor.row, 0); 100 | } 101 | }; 102 | var isWord = function(cursor) { 103 | var line = getCurrentLine(cursor); 104 | var lineSuffix = line.substr(cursor.column - 1); 105 | if (cursor.column === 0) { 106 | lineSuffix = ' ' + line; 107 | } 108 | var firstWordRegExp = /^\W(\w+)/; 109 | var words = firstWordRegExp.exec(lineSuffix); 110 | return words !== null; 111 | }; 112 | var rules = { 113 | 'constant': { 114 | prop: CONSTANT_PROP 115 | }, 116 | 'entity': { 117 | prop: ENTITY_PROP 118 | }, 119 | 'keyword': { 120 | prop: KEYWORD_PROP 121 | }, 122 | 'storage': { 123 | prop: STORAGE_PROP 124 | }, 125 | 'variable': { 126 | prop: VARIABLE_PROP 127 | }, 128 | 'meta': { 129 | prop: DEFAULT_PROP, 130 | replace: [ 131 | { 132 | substr: '', 137 | newSubstr: ' close tag ' 138 | }, 139 | { 140 | substr: '<', 141 | newSubstr: ' tag start ' 142 | }, 143 | { 144 | substr: '>', 145 | newSubstr: ' tag end ' 146 | } 147 | ] 148 | } 149 | }; 150 | var DEFAULT_RULE = { 151 | prop: DEFAULT_RULE 152 | }; 153 | var expand = function(value, replaceRules) { 154 | var newValue = value; 155 | for (var i = 0; i < replaceRules.length; i++) { 156 | var replaceRule = replaceRules[i]; 157 | var regexp = new RegExp(replaceRule.substr, 'g'); 158 | newValue = newValue.replace(regexp, replaceRule.newSubstr); 159 | } 160 | return newValue; 161 | }; 162 | var mergeTokens = function(tokens, start, end) { 163 | var newToken = {}; 164 | newToken.value = ''; 165 | newToken.type = tokens[start].type; 166 | for (var j = start; j < end; j++) { 167 | newToken.value += tokens[j].value; 168 | } 169 | return newToken; 170 | }; 171 | var mergeLikeTokens = function(tokens) { 172 | if (tokens.length <= 1) { 173 | return tokens; 174 | } 175 | var newTokens = []; 176 | var lastLikeIndex = 0; 177 | for (var i = 1; i < tokens.length; i++) { 178 | var lastLikeToken = tokens[lastLikeIndex]; 179 | var currToken = tokens[i]; 180 | if (getTokenRule(lastLikeToken) !== getTokenRule(currToken)) { 181 | newTokens.push(mergeTokens(tokens, lastLikeIndex, i)); 182 | lastLikeIndex = i; 183 | } 184 | } 185 | newTokens.push(mergeTokens(tokens, lastLikeIndex, tokens.length)); 186 | return newTokens; 187 | }; 188 | var isRowWhiteSpace = function(row) { 189 | var line = cvoxAce.editor.getSession().getLine(row); 190 | var whiteSpaceRegexp = /^\s*$/; 191 | return whiteSpaceRegexp.exec(line) !== null; 192 | }; 193 | var speakLine = function(row, queue) { 194 | var tokens = cvoxAce.editor.getSession().getTokens(row); 195 | if (tokens.length === 0 || isRowWhiteSpace(row)) { 196 | cvox.Api.playEarcon('EDITABLE_TEXT'); 197 | return; 198 | } 199 | tokens = mergeLikeTokens(tokens); 200 | var firstToken = tokens[0]; 201 | tokens = tokens.filter(function(token) { 202 | return token !== firstToken; 203 | }); 204 | speakToken_(firstToken, queue); 205 | tokens.forEach(speakTokenQueue); 206 | }; 207 | var speakTokenFlush = function(token) { 208 | speakToken_(token, 0); 209 | }; 210 | var speakTokenQueue = function(token) { 211 | speakToken_(token, 1); 212 | }; 213 | var getTokenRule = function(token) { 214 | if (!token || !token.type) { 215 | return; 216 | } 217 | var split = token.type.split('.'); 218 | if (split.length === 0) { 219 | return; 220 | } 221 | var type = split[0]; 222 | var rule = rules[type]; 223 | if (!rule) { 224 | return DEFAULT_RULE; 225 | } 226 | return rule; 227 | }; 228 | var speakToken_ = function(token, queue) { 229 | var rule = getTokenRule(token); 230 | var value = expand(token.value, REPLACE_LIST); 231 | if (rule.replace) { 232 | value = expand(value, rule.replace); 233 | } 234 | cvox.Api.speak(value, queue, rule.prop); 235 | }; 236 | var speakChar = function(cursor) { 237 | var line = getCurrentLine(cursor); 238 | cvox.Api.speak(line[cursor.column], 1); 239 | }; 240 | var speakDisplacement = function(lastCursor, currCursor) { 241 | var line = getCurrentLine(currCursor); 242 | var displace = line.substring(lastCursor.column, currCursor.column); 243 | displace = displace.replace(/ /g, ' space '); 244 | cvox.Api.speak(displace); 245 | }; 246 | var speakCharOrWordOrLine = function(lastCursor, currCursor) { 247 | if (Math.abs(lastCursor.column - currCursor.column) !== 1) { 248 | var currLineLength = getCurrentLine(currCursor).length; 249 | if (currCursor.column === 0 || currCursor.column === currLineLength) { 250 | speakLine(currCursor.row, 0); 251 | return; 252 | } 253 | if (isWord(currCursor)) { 254 | cvox.Api.stop(); 255 | speakTokenQueue(getCurrentToken(currCursor)); 256 | return; 257 | } 258 | } 259 | speakChar(currCursor); 260 | }; 261 | var onColumnChange = function(lastCursor, currCursor) { 262 | if (!cvoxAce.editor.selection.isEmpty()) { 263 | speakDisplacement(lastCursor, currCursor); 264 | cvox.Api.speak('selected', 1); 265 | } 266 | else if (shouldSpeakDisplacement) { 267 | speakDisplacement(lastCursor, currCursor); 268 | } else { 269 | speakCharOrWordOrLine(lastCursor, currCursor); 270 | } 271 | }; 272 | var onCursorChange = function(evt) { 273 | if (changed) { 274 | changed = false; 275 | return; 276 | } 277 | var currCursor = cvoxAce.editor.selection.getCursor(); 278 | if (currCursor.row !== lastCursor.row) { 279 | onRowChange(currCursor); 280 | } else { 281 | onColumnChange(lastCursor, currCursor); 282 | } 283 | lastCursor = currCursor; 284 | }; 285 | var onSelectionChange = function(evt) { 286 | if (cvoxAce.editor.selection.isEmpty()) { 287 | cvox.Api.speak('unselected'); 288 | } 289 | }; 290 | var onChange = function(evt) { 291 | var data = evt.data; 292 | switch (data.action) { 293 | case 'removeText': 294 | cvox.Api.speak(data.text, 0, DELETED_PROP); 295 | changed = true; 296 | break; 297 | case 'insertText': 298 | cvox.Api.speak(data.text, 0); 299 | changed = true; 300 | break; 301 | } 302 | }; 303 | var isNewAnnotation = function(annot) { 304 | var row = annot.row; 305 | var col = annot.column; 306 | return !annotTable[row] || !annotTable[row][col]; 307 | }; 308 | var populateAnnotations = function(annotations) { 309 | annotTable = {}; 310 | for (var i = 0; i < annotations.length; i++) { 311 | var annotation = annotations[i]; 312 | var row = annotation.row; 313 | var col = annotation.column; 314 | if (!annotTable[row]) { 315 | annotTable[row] = {}; 316 | } 317 | annotTable[row][col] = annotation; 318 | } 319 | }; 320 | var onAnnotationChange = function(evt) { 321 | var annotations = cvoxAce.editor.getSession().getAnnotations(); 322 | var newAnnotations = annotations.filter(isNewAnnotation); 323 | if (newAnnotations.length > 0) { 324 | cvox.Api.playEarcon(ERROR_EARCON); 325 | } 326 | populateAnnotations(annotations); 327 | }; 328 | var speakAnnot = function(annot) { 329 | var annotText = annot.type + ' ' + annot.text + ' on ' + 330 | rowColToString(annot.row, annot.column); 331 | annotText = annotText.replace(';', 'semicolon'); 332 | cvox.Api.speak(annotText, 1); 333 | }; 334 | var speakAnnotsByRow = function(row) { 335 | var annots = annotTable[row]; 336 | for (var col in annots) { 337 | speakAnnot(annots[col]); 338 | } 339 | }; 340 | var rowColToString = function(row, col) { 341 | return 'row ' + (row + 1) + ' column ' + (col + 1); 342 | }; 343 | var speakCurrRowAndCol = function() { 344 | cvox.Api.speak(rowColToString(lastCursor.row, lastCursor.column)); 345 | }; 346 | var speakAllAnnots = function() { 347 | for (var row in annotTable) { 348 | speakAnnotsByRow(row); 349 | } 350 | }; 351 | var speakMode = function() { 352 | if (!isVimMode()) { 353 | return; 354 | } 355 | switch (cvoxAce.editor.keyBinding.$data.state) { 356 | case INSERT_MODE_STATE: 357 | cvox.Api.speak('Insert mode'); 358 | break; 359 | case COMMAND_MODE_STATE: 360 | cvox.Api.speak('Command mode'); 361 | break; 362 | } 363 | }; 364 | var toggleSpeakRowLocation = function() { 365 | shouldSpeakRowLocation = !shouldSpeakRowLocation; 366 | if (shouldSpeakRowLocation) { 367 | cvox.Api.speak('Speak location on row change enabled.'); 368 | } else { 369 | cvox.Api.speak('Speak location on row change disabled.'); 370 | } 371 | }; 372 | var toggleSpeakDisplacement = function() { 373 | shouldSpeakDisplacement = !shouldSpeakDisplacement; 374 | if (shouldSpeakDisplacement) { 375 | cvox.Api.speak('Speak displacement on column changes.'); 376 | } else { 377 | cvox.Api.speak('Speak current character or word on column changes.'); 378 | } 379 | }; 380 | var onKeyDown = function(evt) { 381 | if (evt.ctrlKey && evt.shiftKey) { 382 | var shortcut = keyCodeToShortcutMap[evt.keyCode]; 383 | if (shortcut) { 384 | shortcut.func(); 385 | } 386 | } 387 | }; 388 | var onChangeStatus = function(evt, editor) { 389 | if (!isVimMode()) { 390 | return; 391 | } 392 | var state = editor.keyBinding.$data.state; 393 | if (state === vimState) { 394 | return; 395 | } 396 | switch (state) { 397 | case INSERT_MODE_STATE: 398 | cvox.Api.playEarcon(MODE_SWITCH_EARCON); 399 | cvox.Api.setKeyEcho(true); 400 | break; 401 | case COMMAND_MODE_STATE: 402 | cvox.Api.playEarcon(MODE_SWITCH_EARCON); 403 | cvox.Api.setKeyEcho(false); 404 | break; 405 | } 406 | vimState = state; 407 | }; 408 | var contextMenuHandler = function(evt) { 409 | var cmd = evt.detail['customCommand']; 410 | var shortcut = cmdToShortcutMap[cmd]; 411 | if (shortcut) { 412 | shortcut.func(); 413 | cvoxAce.editor.focus(); 414 | } 415 | }; 416 | var initContextMenu = function() { 417 | var ACTIONS = SHORTCUTS.map(function(shortcut) { 418 | return { 419 | desc: shortcut.desc + getKeyShortcutString(shortcut.keyCode), 420 | cmd: shortcut.cmd 421 | }; 422 | }); 423 | var body = document.querySelector('body'); 424 | body.setAttribute('contextMenuActions', JSON.stringify(ACTIONS)); 425 | body.addEventListener('ATCustomEvent', contextMenuHandler, true); 426 | }; 427 | var onFindSearchbox = function(evt) { 428 | if (evt.match) { 429 | speakLine(lastCursor.row, 0); 430 | } else { 431 | cvox.Api.playEarcon(NO_MATCH_EARCON); 432 | } 433 | }; 434 | var focus = function() { 435 | cvoxAce.editor.focus(); 436 | }; 437 | var SHORTCUTS = [ 438 | { 439 | keyCode: 49, 440 | func: function() { 441 | speakAnnotsByRow(lastCursor.row); 442 | }, 443 | cmd: Command.SPEAK_ANNOT, 444 | desc: 'Speak annotations on line' 445 | }, 446 | { 447 | keyCode: 50, 448 | func: speakAllAnnots, 449 | cmd: Command.SPEAK_ALL_ANNOTS, 450 | desc: 'Speak all annotations' 451 | }, 452 | { 453 | keyCode: 51, 454 | func: speakMode, 455 | cmd: Command.SPEAK_MODE, 456 | desc: 'Speak Vim mode' 457 | }, 458 | { 459 | keyCode: 52, 460 | func: toggleSpeakRowLocation, 461 | cmd: Command.TOGGLE_LOCATION, 462 | desc: 'Toggle speak row location' 463 | }, 464 | { 465 | keyCode: 53, 466 | func: speakCurrRowAndCol, 467 | cmd: Command.SPEAK_ROW_COL, 468 | desc: 'Speak row and column' 469 | }, 470 | { 471 | keyCode: 54, 472 | func: toggleSpeakDisplacement, 473 | cmd: Command.TOGGLE_DISPLACEMENT, 474 | desc: 'Toggle speak displacement' 475 | }, 476 | { 477 | keyCode: 55, 478 | func: focus, 479 | cmd: Command.FOCUS_TEXT, 480 | desc: 'Focus text' 481 | } 482 | ]; 483 | var onFocus = function() { 484 | cvoxAce.editor = editor; 485 | editor.getSession().selection.on('changeCursor', onCursorChange); 486 | editor.getSession().selection.on('changeSelection', onSelectionChange); 487 | editor.getSession().on('change', onChange); 488 | editor.getSession().on('changeAnnotation', onAnnotationChange); 489 | editor.on('changeStatus', onChangeStatus); 490 | editor.on('findSearchBox', onFindSearchbox); 491 | editor.container.addEventListener('keydown', onKeyDown); 492 | 493 | lastCursor = editor.selection.getCursor(); 494 | }; 495 | var init = function(editor) { 496 | onFocus(); 497 | SHORTCUTS.forEach(function(shortcut) { 498 | keyCodeToShortcutMap[shortcut.keyCode] = shortcut; 499 | cmdToShortcutMap[shortcut.cmd] = shortcut; 500 | }); 501 | 502 | editor.on('focus', onFocus); 503 | if (isVimMode()) { 504 | cvox.Api.setKeyEcho(false); 505 | } 506 | initContextMenu(); 507 | }; 508 | function cvoxApiExists() { 509 | return (typeof(cvox) !== 'undefined') && cvox && cvox.Api; 510 | } 511 | var tries = 0; 512 | var MAX_TRIES = 15; 513 | function watchForCvoxLoad(editor) { 514 | if (cvoxApiExists()) { 515 | init(editor); 516 | } else { 517 | tries++; 518 | if (tries >= MAX_TRIES) { 519 | return; 520 | } 521 | window.setTimeout(watchForCvoxLoad, 500, editor); 522 | } 523 | } 524 | 525 | var Editor = require('../editor').Editor; 526 | require('../config').defineOptions(Editor.prototype, 'editor', { 527 | enableChromevoxEnhancements: { 528 | set: function(val) { 529 | if (val) { 530 | watchForCvoxLoad(this); 531 | } 532 | }, 533 | value: true // turn it on by default or check for window.cvox 534 | } 535 | }); 536 | 537 | }); 538 | -------------------------------------------------------------------------------- /server/src/main/resources/ace/ext-elastic_tabstops_lite.js: -------------------------------------------------------------------------------- 1 | /* ***** BEGIN LICENSE BLOCK ***** 2 | * Distributed under the BSD license: 3 | * 4 | * Copyright (c) 2012, Ajax.org B.V. 5 | * All rights reserved. 6 | * 7 | * Redistribution and use in source and binary forms, with or without 8 | * modification, are permitted provided that the following conditions are met: 9 | * * Redistributions of source code must retain the above copyright 10 | * notice, this list of conditions and the following disclaimer. 11 | * * Redistributions in binary form must reproduce the above copyright 12 | * notice, this list of conditions and the following disclaimer in the 13 | * documentation and/or other materials provided with the distribution. 14 | * * Neither the name of Ajax.org B.V. nor the 15 | * names of its contributors may be used to endorse or promote products 16 | * derived from this software without specific prior written permission. 17 | * 18 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 19 | * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | * DISCLAIMED. IN NO EVENT SHALL AJAX.ORG B.V. BE LIABLE FOR ANY 22 | * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 23 | * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 24 | * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 25 | * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | * 29 | * ***** END LICENSE BLOCK ***** */ 30 | 31 | ace.define('ace/ext/elastic_tabstops_lite', ['require', 'exports', 'module' , 'ace/editor', 'ace/config'], function(require, exports, module) { 32 | 33 | 34 | var ElasticTabstopsLite = function(editor) { 35 | this.$editor = editor; 36 | var self = this; 37 | var changedRows = []; 38 | var recordChanges = false; 39 | this.onAfterExec = function() { 40 | recordChanges = false; 41 | self.processRows(changedRows); 42 | changedRows = []; 43 | }; 44 | this.onExec = function() { 45 | recordChanges = true; 46 | }; 47 | this.onChange = function(e) { 48 | var range = e.data.range 49 | if (recordChanges) { 50 | if (changedRows.indexOf(range.start.row) == -1) 51 | changedRows.push(range.start.row); 52 | if (range.end.row != range.start.row) 53 | changedRows.push(range.end.row); 54 | } 55 | }; 56 | }; 57 | 58 | (function() { 59 | this.processRows = function(rows) { 60 | this.$inChange = true; 61 | var checkedRows = []; 62 | 63 | for (var r = 0, rowCount = rows.length; r < rowCount; r++) { 64 | var row = rows[r]; 65 | 66 | if (checkedRows.indexOf(row) > -1) 67 | continue; 68 | 69 | var cellWidthObj = this.$findCellWidthsForBlock(row); 70 | var cellWidths = this.$setBlockCellWidthsToMax(cellWidthObj.cellWidths); 71 | var rowIndex = cellWidthObj.firstRow; 72 | 73 | for (var w = 0, l = cellWidths.length; w < l; w++) { 74 | var widths = cellWidths[w]; 75 | checkedRows.push(rowIndex); 76 | this.$adjustRow(rowIndex, widths); 77 | rowIndex++; 78 | } 79 | } 80 | this.$inChange = false; 81 | }; 82 | 83 | this.$findCellWidthsForBlock = function(row) { 84 | var cellWidths = [], widths; 85 | var rowIter = row; 86 | while (rowIter >= 0) { 87 | widths = this.$cellWidthsForRow(rowIter); 88 | if (widths.length == 0) 89 | break; 90 | 91 | cellWidths.unshift(widths); 92 | rowIter--; 93 | } 94 | var firstRow = rowIter + 1; 95 | rowIter = row; 96 | var numRows = this.$editor.session.getLength(); 97 | 98 | while (rowIter < numRows - 1) { 99 | rowIter++; 100 | 101 | widths = this.$cellWidthsForRow(rowIter); 102 | if (widths.length == 0) 103 | break; 104 | 105 | cellWidths.push(widths); 106 | } 107 | 108 | return { cellWidths: cellWidths, firstRow: firstRow }; 109 | }; 110 | 111 | this.$cellWidthsForRow = function(row) { 112 | var selectionColumns = this.$selectionColumnsForRow(row); 113 | 114 | var tabs = [-1].concat(this.$tabsForRow(row)); 115 | var widths = tabs.map(function(el) { return 0; } ).slice(1); 116 | var line = this.$editor.session.getLine(row); 117 | 118 | for (var i = 0, len = tabs.length - 1; i < len; i++) { 119 | var leftEdge = tabs[i]+1; 120 | var rightEdge = tabs[i+1]; 121 | 122 | var rightmostSelection = this.$rightmostSelectionInCell(selectionColumns, rightEdge); 123 | var cell = line.substring(leftEdge, rightEdge); 124 | widths[i] = Math.max(cell.replace(/\s+$/g,'').length, rightmostSelection - leftEdge); 125 | } 126 | 127 | return widths; 128 | }; 129 | 130 | this.$selectionColumnsForRow = function(row) { 131 | var selections = [], cursor = this.$editor.getCursorPosition(); 132 | if (this.$editor.session.getSelection().isEmpty()) { 133 | if (row == cursor.row) 134 | selections.push(cursor.column); 135 | } 136 | 137 | return selections; 138 | }; 139 | 140 | this.$setBlockCellWidthsToMax = function(cellWidths) { 141 | var startingNewBlock = true, blockStartRow, blockEndRow, maxWidth; 142 | var columnInfo = this.$izip_longest(cellWidths); 143 | 144 | for (var c = 0, l = columnInfo.length; c < l; c++) { 145 | var column = columnInfo[c]; 146 | if (!column.push) { 147 | console.error(column); 148 | continue; 149 | } 150 | column.push(NaN); 151 | 152 | for (var r = 0, s = column.length; r < s; r++) { 153 | var width = column[r]; 154 | if (startingNewBlock) { 155 | blockStartRow = r; 156 | maxWidth = 0; 157 | startingNewBlock = false; 158 | } 159 | if (isNaN(width)) { 160 | blockEndRow = r; 161 | 162 | for (var j = blockStartRow; j < blockEndRow; j++) { 163 | cellWidths[j][c] = maxWidth; 164 | } 165 | startingNewBlock = true; 166 | } 167 | 168 | maxWidth = Math.max(maxWidth, width); 169 | } 170 | } 171 | 172 | return cellWidths; 173 | }; 174 | 175 | this.$rightmostSelectionInCell = function(selectionColumns, cellRightEdge) { 176 | var rightmost = 0; 177 | 178 | if (selectionColumns.length) { 179 | var lengths = []; 180 | for (var s = 0, length = selectionColumns.length; s < length; s++) { 181 | if (selectionColumns[s] <= cellRightEdge) 182 | lengths.push(s); 183 | else 184 | lengths.push(0); 185 | } 186 | rightmost = Math.max.apply(Math, lengths); 187 | } 188 | 189 | return rightmost; 190 | }; 191 | 192 | this.$tabsForRow = function(row) { 193 | var rowTabs = [], line = this.$editor.session.getLine(row), 194 | re = /\t/g, match; 195 | 196 | while ((match = re.exec(line)) != null) { 197 | rowTabs.push(match.index); 198 | } 199 | 200 | return rowTabs; 201 | }; 202 | 203 | this.$adjustRow = function(row, widths) { 204 | var rowTabs = this.$tabsForRow(row); 205 | 206 | if (rowTabs.length == 0) 207 | return; 208 | 209 | var bias = 0, location = -1; 210 | var expandedSet = this.$izip(widths, rowTabs); 211 | 212 | for (var i = 0, l = expandedSet.length; i < l; i++) { 213 | var w = expandedSet[i][0], it = expandedSet[i][1]; 214 | location += 1 + w; 215 | it += bias; 216 | var difference = location - it; 217 | 218 | if (difference == 0) 219 | continue; 220 | 221 | var partialLine = this.$editor.session.getLine(row).substr(0, it ); 222 | var strippedPartialLine = partialLine.replace(/\s*$/g, ""); 223 | var ispaces = partialLine.length - strippedPartialLine.length; 224 | 225 | if (difference > 0) { 226 | this.$editor.session.getDocument().insertInLine({row: row, column: it + 1}, Array(difference + 1).join(" ") + "\t"); 227 | this.$editor.session.getDocument().removeInLine(row, it, it + 1); 228 | 229 | bias += difference; 230 | } 231 | 232 | if (difference < 0 && ispaces >= -difference) { 233 | this.$editor.session.getDocument().removeInLine(row, it + difference, it); 234 | bias += difference; 235 | } 236 | } 237 | }; 238 | this.$izip_longest = function(iterables) { 239 | if (!iterables[0]) 240 | return []; 241 | var longest = iterables[0].length; 242 | var iterablesLength = iterables.length; 243 | 244 | for (var i = 1; i < iterablesLength; i++) { 245 | var iLength = iterables[i].length; 246 | if (iLength > longest) 247 | longest = iLength; 248 | } 249 | 250 | var expandedSet = []; 251 | 252 | for (var l = 0; l < longest; l++) { 253 | var set = []; 254 | for (var i = 0; i < iterablesLength; i++) { 255 | if (iterables[i][l] === "") 256 | set.push(NaN); 257 | else 258 | set.push(iterables[i][l]); 259 | } 260 | 261 | expandedSet.push(set); 262 | } 263 | 264 | 265 | return expandedSet; 266 | }; 267 | this.$izip = function(widths, tabs) { 268 | var size = widths.length >= tabs.length ? tabs.length : widths.length; 269 | 270 | var expandedSet = []; 271 | for (var i = 0; i < size; i++) { 272 | var set = [ widths[i], tabs[i] ]; 273 | expandedSet.push(set); 274 | } 275 | return expandedSet; 276 | }; 277 | 278 | }).call(ElasticTabstopsLite.prototype); 279 | 280 | exports.ElasticTabstopsLite = ElasticTabstopsLite; 281 | 282 | var Editor = require("../editor").Editor; 283 | require("../config").defineOptions(Editor.prototype, "editor", { 284 | useElasticTabstops: { 285 | set: function(val) { 286 | if (val) { 287 | if (!this.elasticTabstops) 288 | this.elasticTabstops = new ElasticTabstopsLite(this); 289 | this.commands.on("afterExec", this.elasticTabstops.onAfterExec); 290 | this.commands.on("exec", this.elasticTabstops.onExec); 291 | this.on("change", this.elasticTabstops.onChange); 292 | } else if (this.elasticTabstops) { 293 | this.commands.removeListener("afterExec", this.elasticTabstops.onAfterExec); 294 | this.commands.removeListener("exec", this.elasticTabstops.onExec); 295 | this.removeListener("change", this.elasticTabstops.onChange); 296 | } 297 | } 298 | } 299 | }); 300 | 301 | }); -------------------------------------------------------------------------------- /server/src/main/resources/ace/ext-keybinding_menu.js: -------------------------------------------------------------------------------- 1 | /* ***** BEGIN LICENSE BLOCK ***** 2 | * Distributed under the BSD license: 3 | * 4 | * Copyright (c) 2013 Matthew Christopher Kastor-Inare III, Atropa Inc. Intl 5 | * All rights reserved. 6 | * 7 | * Contributed to Ajax.org under the BSD license. 8 | * 9 | * Redistribution and use in source and binary forms, with or without 10 | * modification, are permitted provided that the following conditions are met: 11 | * * Redistributions of source code must retain the above copyright 12 | * notice, this list of conditions and the following disclaimer. 13 | * * Redistributions in binary form must reproduce the above copyright 14 | * notice, this list of conditions and the following disclaimer in the 15 | * documentation and/or other materials provided with the distribution. 16 | * * Neither the name of Ajax.org B.V. nor the 17 | * names of its contributors may be used to endorse or promote products 18 | * derived from this software without specific prior written permission. 19 | * 20 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 21 | * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 22 | * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | * DISCLAIMED. IN NO EVENT SHALL AJAX.ORG B.V. BE LIABLE FOR ANY 24 | * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 25 | * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 26 | * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 27 | * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 28 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 29 | * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | * 31 | * ***** END LICENSE BLOCK ***** */ 32 | 33 | ace.define('ace/ext/keybinding_menu', ['require', 'exports', 'module' , 'ace/editor', 'ace/ext/menu_tools/overlay_page', 'ace/ext/menu_tools/get_editor_keyboard_shortcuts'], function(require, exports, module) { 34 | 35 | var Editor = require("ace/editor").Editor; 36 | function showKeyboardShortcuts (editor) { 37 | if(!document.getElementById('kbshortcutmenu')) { 38 | var overlayPage = require('./menu_tools/overlay_page').overlayPage; 39 | var getEditorKeybordShortcuts = require('./menu_tools/get_editor_keyboard_shortcuts').getEditorKeybordShortcuts; 40 | var kb = getEditorKeybordShortcuts(editor); 41 | var el = document.createElement('div'); 42 | var commands = kb.reduce(function(previous, current) { 43 | return previous + '
' 44 | + current.command + ' : ' 45 | + '' + current.key + '
'; 46 | }, ''); 47 | 48 | el.id = 'kbshortcutmenu'; 49 | el.innerHTML = '

Keyboard Shortcuts

' + commands + ''; 50 | overlayPage(editor, el, '0', '0', '0', null); 51 | } 52 | }; 53 | module.exports.init = function(editor) { 54 | Editor.prototype.showKeyboardShortcuts = function() { 55 | showKeyboardShortcuts(this); 56 | }; 57 | editor.commands.addCommands([{ 58 | name: "showKeyboardShortcuts", 59 | bindKey: {win: "Ctrl-Alt-h", mac: "Command-Alt-h"}, 60 | exec: function(editor, line) { 61 | editor.showKeyboardShortcuts(); 62 | } 63 | }]); 64 | }; 65 | 66 | }); 67 | 68 | ace.define('ace/ext/menu_tools/overlay_page', ['require', 'exports', 'module' , 'ace/lib/dom'], function(require, exports, module) { 69 | 70 | var dom = require("../../lib/dom"); 71 | var cssText = "#ace_settingsmenu, #kbshortcutmenu {\ 72 | background-color: #F7F7F7;\ 73 | color: black;\ 74 | box-shadow: -5px 4px 5px rgba(126, 126, 126, 0.55);\ 75 | padding: 1em 0.5em 2em 1em;\ 76 | overflow: auto;\ 77 | position: absolute;\ 78 | margin: 0;\ 79 | bottom: 0;\ 80 | right: 0;\ 81 | top: 0;\ 82 | z-index: 9991;\ 83 | cursor: default;\ 84 | }\ 85 | .ace_dark #ace_settingsmenu, .ace_dark #kbshortcutmenu {\ 86 | box-shadow: -20px 10px 25px rgba(126, 126, 126, 0.25);\ 87 | background-color: rgba(255, 255, 255, 0.6);\ 88 | color: black;\ 89 | }\ 90 | .ace_optionsMenuEntry:hover {\ 91 | background-color: rgba(100, 100, 100, 0.1);\ 92 | -webkit-transition: all 0.5s;\ 93 | transition: all 0.3s\ 94 | }\ 95 | .ace_closeButton {\ 96 | background: rgba(245, 146, 146, 0.5);\ 97 | border: 1px solid #F48A8A;\ 98 | border-radius: 50%;\ 99 | padding: 7px;\ 100 | position: absolute;\ 101 | right: -8px;\ 102 | top: -8px;\ 103 | z-index: 1000;\ 104 | }\ 105 | .ace_closeButton{\ 106 | background: rgba(245, 146, 146, 0.9);\ 107 | }\ 108 | .ace_optionsMenuKey {\ 109 | color: darkslateblue;\ 110 | font-weight: bold;\ 111 | }\ 112 | .ace_optionsMenuCommand {\ 113 | color: darkcyan;\ 114 | font-weight: normal;\ 115 | }"; 116 | dom.importCssString(cssText); 117 | module.exports.overlayPage = function overlayPage(editor, contentElement, top, right, bottom, left) { 118 | top = top ? 'top: ' + top + ';' : ''; 119 | bottom = bottom ? 'bottom: ' + bottom + ';' : ''; 120 | right = right ? 'right: ' + right + ';' : ''; 121 | left = left ? 'left: ' + left + ';' : ''; 122 | 123 | var closer = document.createElement('div'); 124 | var contentContainer = document.createElement('div'); 125 | 126 | function documentEscListener(e) { 127 | if (e.keyCode === 27) { 128 | closer.click(); 129 | } 130 | } 131 | 132 | closer.style.cssText = 'margin: 0; padding: 0; ' + 133 | 'position: fixed; top:0; bottom:0; left:0; right:0;' + 134 | 'z-index: 9990; ' + 135 | 'background-color: rgba(0, 0, 0, 0.3);'; 136 | closer.addEventListener('click', function() { 137 | document.removeEventListener('keydown', documentEscListener); 138 | closer.parentNode.removeChild(closer); 139 | editor.focus(); 140 | closer = null; 141 | }); 142 | document.addEventListener('keydown', documentEscListener); 143 | 144 | contentContainer.style.cssText = top + right + bottom + left; 145 | contentContainer.addEventListener('click', function(e) { 146 | e.stopPropagation(); 147 | }); 148 | 149 | var wrapper = dom.createElement("div"); 150 | wrapper.style.position = "relative"; 151 | 152 | var closeButton = dom.createElement("div"); 153 | closeButton.className = "ace_closeButton"; 154 | closeButton.addEventListener('click', function() { 155 | closer.click(); 156 | }); 157 | 158 | wrapper.appendChild(closeButton); 159 | contentContainer.appendChild(wrapper); 160 | 161 | contentContainer.appendChild(contentElement); 162 | closer.appendChild(contentContainer); 163 | document.body.appendChild(closer); 164 | editor.blur(); 165 | }; 166 | 167 | }); 168 | 169 | ace.define('ace/ext/menu_tools/get_editor_keyboard_shortcuts', ['require', 'exports', 'module' , 'ace/lib/keys'], function(require, exports, module) { 170 | 171 | var keys = require("../../lib/keys"); 172 | module.exports.getEditorKeybordShortcuts = function(editor) { 173 | var KEY_MODS = keys.KEY_MODS; 174 | var keybindings = []; 175 | var commandMap = {}; 176 | editor.keyBinding.$handlers.forEach(function(handler) { 177 | var ckb = handler.commmandKeyBinding; 178 | for (var i in ckb) { 179 | var modifier = parseInt(i); 180 | if (modifier == -1) { 181 | modifier = ""; 182 | } else if(isNaN(modifier)) { 183 | modifier = i; 184 | } else { 185 | modifier = "" + 186 | (modifier & KEY_MODS.command ? "Cmd-" : "") + 187 | (modifier & KEY_MODS.ctrl ? "Ctrl-" : "") + 188 | (modifier & KEY_MODS.alt ? "Alt-" : "") + 189 | (modifier & KEY_MODS.shift ? "Shift-" : ""); 190 | } 191 | for (var key in ckb[i]) { 192 | var command = ckb[i][key] 193 | if (typeof command != "string") 194 | command = command.name 195 | if (commandMap[command]) { 196 | commandMap[command].key += "|" + modifier + key; 197 | } else { 198 | commandMap[command] = {key: modifier+key, command: command}; 199 | keybindings.push(commandMap[command]); 200 | } 201 | } 202 | } 203 | }); 204 | return keybindings; 205 | }; 206 | 207 | }); -------------------------------------------------------------------------------- /server/src/main/resources/ace/ext-modelist.js: -------------------------------------------------------------------------------- 1 | ace.define('ace/ext/modelist', ['require', 'exports', 'module' ], function(require, exports, module) { 2 | 3 | 4 | var modes = []; 5 | function getModeForPath(path) { 6 | var mode = modesByName.text; 7 | var fileName = path.split(/[\/\\]/).pop(); 8 | for (var i = 0; i < modes.length; i++) { 9 | if (modes[i].supportsFile(fileName)) { 10 | mode = modes[i]; 11 | break; 12 | } 13 | } 14 | return mode; 15 | } 16 | 17 | var Mode = function(name, caption, extensions) { 18 | this.name = name; 19 | this.caption = caption; 20 | this.mode = "ace/mode/" + name; 21 | this.extensions = extensions; 22 | if (/\^/.test(extensions)) { 23 | var re = extensions.replace(/\|(\^)?/g, function(a, b){ 24 | return "$|" + (b ? "^" : "^.*\\."); 25 | }) + "$"; 26 | } else { 27 | var re = "^.*\\.(" + extensions + ")$"; 28 | } 29 | 30 | this.extRe = new RegExp(re, "gi"); 31 | }; 32 | 33 | Mode.prototype.supportsFile = function(filename) { 34 | return filename.match(this.extRe); 35 | }; 36 | var supportedModes = { 37 | ABAP: ["abap"], 38 | ADA: ["ada|adb"], 39 | ActionScript:["as"], 40 | AsciiDoc: ["asciidoc"], 41 | Assembly_x86:["asm"], 42 | AutoHotKey: ["ahk"], 43 | BatchFile: ["bat|cmd"], 44 | C9Search: ["c9search_results"], 45 | C_Cpp: ["c|cc|cpp|cxx|h|hh|hpp"], 46 | Clojure: ["clj"], 47 | Cobol: ["^CBL|COB"], 48 | coffee: ["^Cakefile|coffee|cf|cson"], 49 | ColdFusion: ["cfm"], 50 | CSharp: ["cs"], 51 | CSS: ["css"], 52 | Curly: ["curly"], 53 | D: ["d|di"], 54 | Dart: ["dart"], 55 | Diff: ["diff|patch"], 56 | Dot: ["dot"], 57 | Erlang: ["erl|hrl"], 58 | EJS: ["ejs"], 59 | Forth: ["frt|fs|ldr"], 60 | FTL: ["ftl"], 61 | Glsl: ["glsl|frag|vert"], 62 | golang: ["go"], 63 | Groovy: ["groovy"], 64 | HAML: ["haml"], 65 | Haskell: ["hs"], 66 | haXe: ["hx"], 67 | HTML: ["htm|html|xhtml"], 68 | HTML_Ruby: ["erb|rhtml|html.erb"], 69 | Ini: ["Ini|conf"], 70 | Jade: ["jade"], 71 | Java: ["java"], 72 | JavaScript: ["js"], 73 | JSON: ["json"], 74 | JSONiq: ["jq"], 75 | JSP: ["jsp"], 76 | JSX: ["jsx"], 77 | Julia: ["jl"], 78 | LaTeX: ["latex|tex|ltx|bib"], 79 | LESS: ["less"], 80 | Liquid: ["liquid"], 81 | Lisp: ["lisp"], 82 | LiveScript: ["ls"], 83 | LogiQL: ["logic|lql"], 84 | LSL: ["lsl"], 85 | Lua: ["lua"], 86 | LuaPage: ["lp"], 87 | Lucene: ["lucene"], 88 | Makefile: ["^GNUmakefile|^makefile|^Makefile|^OCamlMakefile|make"], 89 | MATLAB: ["matlab"], 90 | Markdown: ["md|markdown"], 91 | MySQL: ["mysql"], 92 | MUSHCode: ["mc|mush"], 93 | Nix: ["nix"], 94 | ObjectiveC: ["m|mm"], 95 | OCaml: ["ml|mli"], 96 | Pascal: ["pas|p"], 97 | Perl: ["pl|pm"], 98 | pgSQL: ["pgsql"], 99 | PHP: ["php|phtml"], 100 | Powershell: ["ps1"], 101 | Prolog: ["plg|prolog"], 102 | Properties: ["properties"], 103 | Protobuf: ["proto"], 104 | Python: ["py"], 105 | R: ["r"], 106 | RDoc: ["Rd"], 107 | RHTML: ["Rhtml"], 108 | Ruby: ["ru|gemspec|rake|rb"], 109 | Rust: ["rs"], 110 | SASS: ["sass"], 111 | SCAD: ["scad"], 112 | Scala: ["scala"], 113 | Scheme: ["scm|rkt"], 114 | SCSS: ["scss"], 115 | SH: ["sh|bash"], 116 | snippets: ["snippets"], 117 | SQL: ["sql"], 118 | Stylus: ["styl|stylus"], 119 | SVG: ["svg"], 120 | Tcl: ["tcl"], 121 | Tex: ["tex"], 122 | Text: ["txt"], 123 | Textile: ["textile"], 124 | Toml: ["toml"], 125 | Twig: ["twig"], 126 | Typescript: ["typescript|ts|str"], 127 | VBScript: ["vbs"], 128 | Velocity: ["vm"], 129 | XML: ["xml|rdf|rss|wsdl|xslt|atom|mathml|mml|xul|xbl"], 130 | XQuery: ["xq"], 131 | YAML: ["yaml"] 132 | }; 133 | 134 | var nameOverrides = { 135 | ObjectiveC: "Objective-C", 136 | CSharp: "C#", 137 | golang: "Go", 138 | C_Cpp: "C/C++", 139 | coffee: "CoffeeScript", 140 | HTML_Ruby: "HTML (Ruby)", 141 | FTL: "FreeMarker" 142 | }; 143 | var modesByName = {}; 144 | for (var name in supportedModes) { 145 | var data = supportedModes[name]; 146 | var displayName = nameOverrides[name] || name; 147 | var filename = name.toLowerCase(); 148 | var mode = new Mode(filename, displayName, data[0]); 149 | modesByName[filename] = mode; 150 | modes.push(mode); 151 | } 152 | 153 | module.exports = { 154 | getModeForPath: getModeForPath, 155 | modes: modes, 156 | modesByName: modesByName 157 | }; 158 | 159 | }); 160 | 161 | -------------------------------------------------------------------------------- /server/src/main/resources/ace/ext-old_ie.js: -------------------------------------------------------------------------------- 1 | /* ***** BEGIN LICENSE BLOCK ***** 2 | * Distributed under the BSD license: 3 | * 4 | * Copyright (c) 2010, Ajax.org B.V. 5 | * All rights reserved. 6 | * 7 | * Redistribution and use in source and binary forms, with or without 8 | * modification, are permitted provided that the following conditions are met: 9 | * * Redistributions of source code must retain the above copyright 10 | * notice, this list of conditions and the following disclaimer. 11 | * * Redistributions in binary form must reproduce the above copyright 12 | * notice, this list of conditions and the following disclaimer in the 13 | * documentation and/or other materials provided with the distribution. 14 | * * Neither the name of Ajax.org B.V. nor the 15 | * names of its contributors may be used to endorse or promote products 16 | * derived from this software without specific prior written permission. 17 | * 18 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 19 | * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | * DISCLAIMED. IN NO EVENT SHALL AJAX.ORG B.V. BE LIABLE FOR ANY 22 | * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 23 | * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 24 | * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 25 | * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | * 29 | * ***** END LICENSE BLOCK ***** */ 30 | 31 | ace.define('ace/ext/old_ie', ['require', 'exports', 'module' , 'ace/lib/useragent', 'ace/tokenizer', 'ace/ext/searchbox'], function(require, exports, module) { 32 | 33 | var MAX_TOKEN_COUNT = 1000; 34 | var useragent = require("../lib/useragent"); 35 | var TokenizerModule = require("../tokenizer"); 36 | 37 | function patch(obj, name, regexp, replacement) { 38 | eval("obj['" + name + "']=" + obj[name].toString().replace( 39 | regexp, replacement 40 | )); 41 | } 42 | 43 | if (useragent.isIE && useragent.isIE < 10 && window.top.document.compatMode === "BackCompat") 44 | useragent.isOldIE = true; 45 | 46 | if (typeof document != "undefined" && !document.documentElement.querySelector) { 47 | useragent.isOldIE = true; 48 | var qs = function(el, selector) { 49 | if (selector.charAt(0) == ".") { 50 | var classNeme = selector.slice(1); 51 | } else { 52 | var m = selector.match(/(\w+)=(\w+)/); 53 | var attr = m && m[1]; 54 | var attrVal = m && m[2]; 55 | } 56 | for (var i = 0; i < el.all.length; i++) { 57 | var ch = el.all[i]; 58 | if (classNeme) { 59 | if (ch.className.indexOf(classNeme) != -1) 60 | return ch; 61 | } else if (attr) { 62 | if (ch.getAttribute(attr) == attrVal) 63 | return ch; 64 | } 65 | } 66 | }; 67 | var sb = require("./searchbox").SearchBox.prototype; 68 | patch( 69 | sb, "$initElements", 70 | /([^\s=]*).querySelector\((".*?")\)/g, 71 | "qs($1, $2)" 72 | ); 73 | } 74 | 75 | var compliantExecNpcg = /()??/.exec("")[1] === undefined; 76 | if (compliantExecNpcg) 77 | return; 78 | var proto = TokenizerModule.Tokenizer.prototype; 79 | TokenizerModule.Tokenizer_orig = TokenizerModule.Tokenizer; 80 | proto.getLineTokens_orig = proto.getLineTokens; 81 | 82 | patch( 83 | TokenizerModule, "Tokenizer", 84 | "ruleRegExps.push(adjustedregex);\n", 85 | function(m) { 86 | return m + '\ 87 | if (state[i].next && RegExp(adjustedregex).test(""))\n\ 88 | rule._qre = RegExp(adjustedregex, "g");\n\ 89 | '; 90 | } 91 | ); 92 | TokenizerModule.Tokenizer.prototype = proto; 93 | patch( 94 | proto, "getLineTokens", 95 | /if \(match\[i \+ 1\] === undefined\)\s*continue;/, 96 | "if (!match[i + 1]) {\n\ 97 | if (value)continue;\n\ 98 | var qre = state[mapping[i]]._qre;\n\ 99 | if (!qre) continue;\n\ 100 | qre.lastIndex = lastIndex;\n\ 101 | if (!qre.exec(line) || qre.lastIndex != lastIndex)\n\ 102 | continue;\n\ 103 | }" 104 | ); 105 | 106 | useragent.isOldIE = true; 107 | 108 | }); 109 | 110 | ace.define('ace/ext/searchbox', ['require', 'exports', 'module' , 'ace/lib/dom', 'ace/lib/lang', 'ace/lib/event', 'ace/keyboard/hash_handler', 'ace/lib/keys'], function(require, exports, module) { 111 | 112 | 113 | var dom = require("../lib/dom"); 114 | var lang = require("../lib/lang"); 115 | var event = require("../lib/event"); 116 | var searchboxCss = "\ 117 | /* ------------------------------------------------------------------------------------------\ 118 | * Editor Search Form\ 119 | * --------------------------------------------------------------------------------------- */\ 120 | .ace_search {\ 121 | background-color: #ddd;\ 122 | border: 1px solid #cbcbcb;\ 123 | border-top: 0 none;\ 124 | max-width: 297px;\ 125 | overflow: hidden;\ 126 | margin: 0;\ 127 | padding: 4px;\ 128 | padding-right: 6px;\ 129 | padding-bottom: 0;\ 130 | position: absolute;\ 131 | top: 0px;\ 132 | z-index: 99;\ 133 | }\ 134 | .ace_search.left {\ 135 | border-left: 0 none;\ 136 | border-radius: 0px 0px 5px 0px;\ 137 | left: 0;\ 138 | }\ 139 | .ace_search.right {\ 140 | border-radius: 0px 0px 0px 5px;\ 141 | border-right: 0 none;\ 142 | right: 0;\ 143 | }\ 144 | .ace_search_form, .ace_replace_form {\ 145 | border-radius: 3px;\ 146 | border: 1px solid #cbcbcb;\ 147 | float: left;\ 148 | margin-bottom: 4px;\ 149 | overflow: hidden;\ 150 | }\ 151 | .ace_search_form.ace_nomatch {\ 152 | outline: 1px solid red;\ 153 | }\ 154 | .ace_search_field {\ 155 | background-color: white;\ 156 | border-right: 1px solid #cbcbcb;\ 157 | border: 0 none;\ 158 | -webkit-box-sizing: border-box;\ 159 | -moz-box-sizing: border-box;\ 160 | box-sizing: border-box;\ 161 | display: block;\ 162 | float: left;\ 163 | height: 22px;\ 164 | outline: 0;\ 165 | padding: 0 7px;\ 166 | width: 214px;\ 167 | margin: 0;\ 168 | }\ 169 | .ace_searchbtn,\ 170 | .ace_replacebtn {\ 171 | background: #fff;\ 172 | border: 0 none;\ 173 | border-left: 1px solid #dcdcdc;\ 174 | cursor: pointer;\ 175 | display: block;\ 176 | float: left;\ 177 | height: 22px;\ 178 | margin: 0;\ 179 | padding: 0;\ 180 | position: relative;\ 181 | }\ 182 | .ace_searchbtn:last-child,\ 183 | .ace_replacebtn:last-child {\ 184 | border-top-right-radius: 3px;\ 185 | border-bottom-right-radius: 3px;\ 186 | }\ 187 | .ace_searchbtn:disabled {\ 188 | background: none;\ 189 | cursor: default;\ 190 | }\ 191 | .ace_searchbtn {\ 192 | background-position: 50% 50%;\ 193 | background-repeat: no-repeat;\ 194 | width: 27px;\ 195 | }\ 196 | .ace_searchbtn.prev {\ 197 | background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAFCAYAAAB4ka1VAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAADFJREFUeNpiSU1NZUAC/6E0I0yACYskCpsJiySKIiY0SUZk40FyTEgCjGgKwTRAgAEAQJUIPCE+qfkAAAAASUVORK5CYII=); \ 198 | }\ 199 | .ace_searchbtn.next {\ 200 | background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAFCAYAAAB4ka1VAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAADRJREFUeNpiTE1NZQCC/0DMyIAKwGJMUAYDEo3M/s+EpvM/mkKwCQxYjIeLMaELoLMBAgwAU7UJObTKsvAAAAAASUVORK5CYII=); \ 201 | }\ 202 | .ace_searchbtn_close {\ 203 | background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAcCAYAAABRVo5BAAAAZ0lEQVR42u2SUQrAMAhDvazn8OjZBilCkYVVxiis8H4CT0VrAJb4WHT3C5xU2a2IQZXJjiQIRMdkEoJ5Q2yMqpfDIo+XY4k6h+YXOyKqTIj5REaxloNAd0xiKmAtsTHqW8sR2W5f7gCu5nWFUpVjZwAAAABJRU5ErkJggg==) no-repeat 50% 0;\ 204 | border-radius: 50%;\ 205 | border: 0 none;\ 206 | color: #656565;\ 207 | cursor: pointer;\ 208 | display: block;\ 209 | float: right;\ 210 | font-family: Arial;\ 211 | font-size: 16px;\ 212 | height: 14px;\ 213 | line-height: 16px;\ 214 | margin: 5px 1px 9px 5px;\ 215 | padding: 0;\ 216 | text-align: center;\ 217 | width: 14px;\ 218 | }\ 219 | .ace_searchbtn_close:hover {\ 220 | background-color: #656565;\ 221 | background-position: 50% 100%;\ 222 | color: white;\ 223 | }\ 224 | .ace_replacebtn.prev {\ 225 | width: 54px\ 226 | }\ 227 | .ace_replacebtn.next {\ 228 | width: 27px\ 229 | }\ 230 | .ace_button {\ 231 | margin-left: 2px;\ 232 | cursor: pointer;\ 233 | -webkit-user-select: none;\ 234 | -moz-user-select: none;\ 235 | -o-user-select: none;\ 236 | -ms-user-select: none;\ 237 | user-select: none;\ 238 | overflow: hidden;\ 239 | opacity: 0.7;\ 240 | border: 1px solid rgba(100,100,100,0.23);\ 241 | padding: 1px;\ 242 | -moz-box-sizing: border-box;\ 243 | box-sizing: border-box;\ 244 | color: black;\ 245 | }\ 246 | .ace_button:hover {\ 247 | background-color: #eee;\ 248 | opacity:1;\ 249 | }\ 250 | .ace_button:active {\ 251 | background-color: #ddd;\ 252 | }\ 253 | .ace_button.checked {\ 254 | border-color: #3399ff;\ 255 | opacity:1;\ 256 | }\ 257 | .ace_search_options{\ 258 | margin-bottom: 3px;\ 259 | text-align: right;\ 260 | -webkit-user-select: none;\ 261 | -moz-user-select: none;\ 262 | -o-user-select: none;\ 263 | -ms-user-select: none;\ 264 | user-select: none;\ 265 | }"; 266 | var HashHandler = require("../keyboard/hash_handler").HashHandler; 267 | var keyUtil = require("../lib/keys"); 268 | 269 | dom.importCssString(searchboxCss, "ace_searchbox"); 270 | 271 | var html = ''.replace(/>\s+/g, ">"); 289 | 290 | var SearchBox = function(editor, range, showReplaceForm) { 291 | var div = dom.createElement("div"); 292 | div.innerHTML = html; 293 | this.element = div.firstChild; 294 | 295 | this.$init(); 296 | this.setEditor(editor); 297 | }; 298 | 299 | (function() { 300 | this.setEditor = function(editor) { 301 | editor.searchBox = this; 302 | editor.container.appendChild(this.element); 303 | this.editor = editor; 304 | }; 305 | 306 | this.$initElements = function(sb) { 307 | this.searchBox = sb.querySelector(".ace_search_form"); 308 | this.replaceBox = sb.querySelector(".ace_replace_form"); 309 | this.searchOptions = sb.querySelector(".ace_search_options"); 310 | this.regExpOption = sb.querySelector("[action=toggleRegexpMode]"); 311 | this.caseSensitiveOption = sb.querySelector("[action=toggleCaseSensitive]"); 312 | this.wholeWordOption = sb.querySelector("[action=toggleWholeWords]"); 313 | this.searchInput = this.searchBox.querySelector(".ace_search_field"); 314 | this.replaceInput = this.replaceBox.querySelector(".ace_search_field"); 315 | }; 316 | 317 | this.$init = function() { 318 | var sb = this.element; 319 | 320 | this.$initElements(sb); 321 | 322 | var _this = this; 323 | event.addListener(sb, "mousedown", function(e) { 324 | setTimeout(function(){ 325 | _this.activeInput.focus(); 326 | }, 0); 327 | event.stopPropagation(e); 328 | }); 329 | event.addListener(sb, "click", function(e) { 330 | var t = e.target || e.srcElement; 331 | var action = t.getAttribute("action"); 332 | if (action && _this[action]) 333 | _this[action](); 334 | else if (_this.$searchBarKb.commands[action]) 335 | _this.$searchBarKb.commands[action].exec(_this); 336 | event.stopPropagation(e); 337 | }); 338 | 339 | event.addCommandKeyListener(sb, function(e, hashId, keyCode) { 340 | var keyString = keyUtil.keyCodeToString(keyCode); 341 | var command = _this.$searchBarKb.findKeyCommand(hashId, keyString); 342 | if (command && command.exec) { 343 | command.exec(_this); 344 | event.stopEvent(e); 345 | } 346 | }); 347 | 348 | this.$onChange = lang.delayedCall(function() { 349 | _this.find(false, false); 350 | }); 351 | 352 | event.addListener(this.searchInput, "input", function() { 353 | _this.$onChange.schedule(20); 354 | }); 355 | event.addListener(this.searchInput, "focus", function() { 356 | _this.activeInput = _this.searchInput; 357 | _this.searchInput.value && _this.highlight(); 358 | }); 359 | event.addListener(this.replaceInput, "focus", function() { 360 | _this.activeInput = _this.replaceInput; 361 | _this.searchInput.value && _this.highlight(); 362 | }); 363 | }; 364 | this.$closeSearchBarKb = new HashHandler([{ 365 | bindKey: "Esc", 366 | name: "closeSearchBar", 367 | exec: function(editor) { 368 | editor.searchBox.hide(); 369 | } 370 | }]); 371 | this.$searchBarKb = new HashHandler(); 372 | this.$searchBarKb.bindKeys({ 373 | "Ctrl-f|Command-f|Ctrl-H|Command-Option-F": function(sb) { 374 | var isReplace = sb.isReplace = !sb.isReplace; 375 | sb.replaceBox.style.display = isReplace ? "" : "none"; 376 | sb[isReplace ? "replaceInput" : "searchInput"].focus(); 377 | }, 378 | "Ctrl-G|Command-G": function(sb) { 379 | sb.findNext(); 380 | }, 381 | "Ctrl-Shift-G|Command-Shift-G": function(sb) { 382 | sb.findPrev(); 383 | }, 384 | "esc": function(sb) { 385 | setTimeout(function() { sb.hide();}); 386 | }, 387 | "Return": function(sb) { 388 | if (sb.activeInput == sb.replaceInput) 389 | sb.replace(); 390 | sb.findNext(); 391 | }, 392 | "Shift-Return": function(sb) { 393 | if (sb.activeInput == sb.replaceInput) 394 | sb.replace(); 395 | sb.findPrev(); 396 | }, 397 | "Tab": function(sb) { 398 | (sb.activeInput == sb.replaceInput ? sb.searchInput : sb.replaceInput).focus(); 399 | } 400 | }); 401 | 402 | this.$searchBarKb.addCommands([{ 403 | name: "toggleRegexpMode", 404 | bindKey: {win: "Alt-R|Alt-/", mac: "Ctrl-Alt-R|Ctrl-Alt-/"}, 405 | exec: function(sb) { 406 | sb.regExpOption.checked = !sb.regExpOption.checked; 407 | sb.$syncOptions(); 408 | } 409 | }, { 410 | name: "toggleCaseSensitive", 411 | bindKey: {win: "Alt-C|Alt-I", mac: "Ctrl-Alt-R|Ctrl-Alt-I"}, 412 | exec: function(sb) { 413 | sb.caseSensitiveOption.checked = !sb.caseSensitiveOption.checked; 414 | sb.$syncOptions(); 415 | } 416 | }, { 417 | name: "toggleWholeWords", 418 | bindKey: {win: "Alt-B|Alt-W", mac: "Ctrl-Alt-B|Ctrl-Alt-W"}, 419 | exec: function(sb) { 420 | sb.wholeWordOption.checked = !sb.wholeWordOption.checked; 421 | sb.$syncOptions(); 422 | } 423 | }]); 424 | 425 | this.$syncOptions = function() { 426 | dom.setCssClass(this.regExpOption, "checked", this.regExpOption.checked); 427 | dom.setCssClass(this.wholeWordOption, "checked", this.wholeWordOption.checked); 428 | dom.setCssClass(this.caseSensitiveOption, "checked", this.caseSensitiveOption.checked); 429 | this.find(false, false); 430 | }; 431 | 432 | this.highlight = function(re) { 433 | this.editor.session.highlight(re || this.editor.$search.$options.re); 434 | this.editor.renderer.updateBackMarkers() 435 | }; 436 | this.find = function(skipCurrent, backwards) { 437 | var range = this.editor.find(this.searchInput.value, { 438 | skipCurrent: skipCurrent, 439 | backwards: backwards, 440 | wrap: true, 441 | regExp: this.regExpOption.checked, 442 | caseSensitive: this.caseSensitiveOption.checked, 443 | wholeWord: this.wholeWordOption.checked 444 | }); 445 | var noMatch = !range && this.searchInput.value; 446 | dom.setCssClass(this.searchBox, "ace_nomatch", noMatch); 447 | this.editor._emit("findSearchBox", { match: !noMatch }); 448 | this.highlight(); 449 | }; 450 | this.findNext = function() { 451 | this.find(true, false); 452 | }; 453 | this.findPrev = function() { 454 | this.find(true, true); 455 | }; 456 | this.replace = function() { 457 | if (!this.editor.getReadOnly()) 458 | this.editor.replace(this.replaceInput.value); 459 | }; 460 | this.replaceAndFindNext = function() { 461 | if (!this.editor.getReadOnly()) { 462 | this.editor.replace(this.replaceInput.value); 463 | this.findNext() 464 | } 465 | }; 466 | this.replaceAll = function() { 467 | if (!this.editor.getReadOnly()) 468 | this.editor.replaceAll(this.replaceInput.value); 469 | }; 470 | 471 | this.hide = function() { 472 | this.element.style.display = "none"; 473 | this.editor.keyBinding.removeKeyboardHandler(this.$closeSearchBarKb); 474 | this.editor.focus(); 475 | }; 476 | this.show = function(value, isReplace) { 477 | this.element.style.display = ""; 478 | this.replaceBox.style.display = isReplace ? "" : "none"; 479 | 480 | this.isReplace = isReplace; 481 | 482 | if (value) 483 | this.searchInput.value = value; 484 | this.searchInput.focus(); 485 | this.searchInput.select(); 486 | 487 | this.editor.keyBinding.addKeyboardHandler(this.$closeSearchBarKb); 488 | }; 489 | 490 | }).call(SearchBox.prototype); 491 | 492 | exports.SearchBox = SearchBox; 493 | 494 | exports.Search = function(editor, isReplace) { 495 | var sb = editor.searchBox || new SearchBox(editor); 496 | sb.show(editor.session.getTextRange(), isReplace); 497 | }; 498 | 499 | }); 500 | -------------------------------------------------------------------------------- /server/src/main/resources/ace/ext-searchbox.js: -------------------------------------------------------------------------------- 1 | /* ***** BEGIN LICENSE BLOCK ***** 2 | * Distributed under the BSD license: 3 | * 4 | * Copyright (c) 2010, Ajax.org B.V. 5 | * All rights reserved. 6 | * 7 | * Redistribution and use in source and binary forms, with or without 8 | * modification, are permitted provided that the following conditions are met: 9 | * * Redistributions of source code must retain the above copyright 10 | * notice, this list of conditions and the following disclaimer. 11 | * * Redistributions in binary form must reproduce the above copyright 12 | * notice, this list of conditions and the following disclaimer in the 13 | * documentation and/or other materials provided with the distribution. 14 | * * Neither the name of Ajax.org B.V. nor the 15 | * names of its contributors may be used to endorse or promote products 16 | * derived from this software without specific prior written permission. 17 | * 18 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 19 | * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | * DISCLAIMED. IN NO EVENT SHALL AJAX.ORG B.V. BE LIABLE FOR ANY 22 | * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 23 | * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 24 | * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 25 | * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | * 29 | * ***** END LICENSE BLOCK ***** */ 30 | 31 | ace.define('ace/ext/searchbox', ['require', 'exports', 'module' , 'ace/lib/dom', 'ace/lib/lang', 'ace/lib/event', 'ace/keyboard/hash_handler', 'ace/lib/keys'], function(require, exports, module) { 32 | 33 | 34 | var dom = require("../lib/dom"); 35 | var lang = require("../lib/lang"); 36 | var event = require("../lib/event"); 37 | var searchboxCss = "\ 38 | /* ------------------------------------------------------------------------------------------\ 39 | * Editor Search Form\ 40 | * --------------------------------------------------------------------------------------- */\ 41 | .ace_search {\ 42 | background-color: #ddd;\ 43 | border: 1px solid #cbcbcb;\ 44 | border-top: 0 none;\ 45 | max-width: 297px;\ 46 | overflow: hidden;\ 47 | margin: 0;\ 48 | padding: 4px;\ 49 | padding-right: 6px;\ 50 | padding-bottom: 0;\ 51 | position: absolute;\ 52 | top: 0px;\ 53 | z-index: 99;\ 54 | }\ 55 | .ace_search.left {\ 56 | border-left: 0 none;\ 57 | border-radius: 0px 0px 5px 0px;\ 58 | left: 0;\ 59 | }\ 60 | .ace_search.right {\ 61 | border-radius: 0px 0px 0px 5px;\ 62 | border-right: 0 none;\ 63 | right: 0;\ 64 | }\ 65 | .ace_search_form, .ace_replace_form {\ 66 | border-radius: 3px;\ 67 | border: 1px solid #cbcbcb;\ 68 | float: left;\ 69 | margin-bottom: 4px;\ 70 | overflow: hidden;\ 71 | }\ 72 | .ace_search_form.ace_nomatch {\ 73 | outline: 1px solid red;\ 74 | }\ 75 | .ace_search_field {\ 76 | background-color: white;\ 77 | border-right: 1px solid #cbcbcb;\ 78 | border: 0 none;\ 79 | -webkit-box-sizing: border-box;\ 80 | -moz-box-sizing: border-box;\ 81 | box-sizing: border-box;\ 82 | display: block;\ 83 | float: left;\ 84 | height: 22px;\ 85 | outline: 0;\ 86 | padding: 0 7px;\ 87 | width: 214px;\ 88 | margin: 0;\ 89 | }\ 90 | .ace_searchbtn,\ 91 | .ace_replacebtn {\ 92 | background: #fff;\ 93 | border: 0 none;\ 94 | border-left: 1px solid #dcdcdc;\ 95 | cursor: pointer;\ 96 | display: block;\ 97 | float: left;\ 98 | height: 22px;\ 99 | margin: 0;\ 100 | padding: 0;\ 101 | position: relative;\ 102 | }\ 103 | .ace_searchbtn:last-child,\ 104 | .ace_replacebtn:last-child {\ 105 | border-top-right-radius: 3px;\ 106 | border-bottom-right-radius: 3px;\ 107 | }\ 108 | .ace_searchbtn:disabled {\ 109 | background: none;\ 110 | cursor: default;\ 111 | }\ 112 | .ace_searchbtn {\ 113 | background-position: 50% 50%;\ 114 | background-repeat: no-repeat;\ 115 | width: 27px;\ 116 | }\ 117 | .ace_searchbtn.prev {\ 118 | background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAFCAYAAAB4ka1VAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAADFJREFUeNpiSU1NZUAC/6E0I0yACYskCpsJiySKIiY0SUZk40FyTEgCjGgKwTRAgAEAQJUIPCE+qfkAAAAASUVORK5CYII=); \ 119 | }\ 120 | .ace_searchbtn.next {\ 121 | background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAFCAYAAAB4ka1VAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAADRJREFUeNpiTE1NZQCC/0DMyIAKwGJMUAYDEo3M/s+EpvM/mkKwCQxYjIeLMaELoLMBAgwAU7UJObTKsvAAAAAASUVORK5CYII=); \ 122 | }\ 123 | .ace_searchbtn_close {\ 124 | background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAcCAYAAABRVo5BAAAAZ0lEQVR42u2SUQrAMAhDvazn8OjZBilCkYVVxiis8H4CT0VrAJb4WHT3C5xU2a2IQZXJjiQIRMdkEoJ5Q2yMqpfDIo+XY4k6h+YXOyKqTIj5REaxloNAd0xiKmAtsTHqW8sR2W5f7gCu5nWFUpVjZwAAAABJRU5ErkJggg==) no-repeat 50% 0;\ 125 | border-radius: 50%;\ 126 | border: 0 none;\ 127 | color: #656565;\ 128 | cursor: pointer;\ 129 | display: block;\ 130 | float: right;\ 131 | font-family: Arial;\ 132 | font-size: 16px;\ 133 | height: 14px;\ 134 | line-height: 16px;\ 135 | margin: 5px 1px 9px 5px;\ 136 | padding: 0;\ 137 | text-align: center;\ 138 | width: 14px;\ 139 | }\ 140 | .ace_searchbtn_close:hover {\ 141 | background-color: #656565;\ 142 | background-position: 50% 100%;\ 143 | color: white;\ 144 | }\ 145 | .ace_replacebtn.prev {\ 146 | width: 54px\ 147 | }\ 148 | .ace_replacebtn.next {\ 149 | width: 27px\ 150 | }\ 151 | .ace_button {\ 152 | margin-left: 2px;\ 153 | cursor: pointer;\ 154 | -webkit-user-select: none;\ 155 | -moz-user-select: none;\ 156 | -o-user-select: none;\ 157 | -ms-user-select: none;\ 158 | user-select: none;\ 159 | overflow: hidden;\ 160 | opacity: 0.7;\ 161 | border: 1px solid rgba(100,100,100,0.23);\ 162 | padding: 1px;\ 163 | -moz-box-sizing: border-box;\ 164 | box-sizing: border-box;\ 165 | color: black;\ 166 | }\ 167 | .ace_button:hover {\ 168 | background-color: #eee;\ 169 | opacity:1;\ 170 | }\ 171 | .ace_button:active {\ 172 | background-color: #ddd;\ 173 | }\ 174 | .ace_button.checked {\ 175 | border-color: #3399ff;\ 176 | opacity:1;\ 177 | }\ 178 | .ace_search_options{\ 179 | margin-bottom: 3px;\ 180 | text-align: right;\ 181 | -webkit-user-select: none;\ 182 | -moz-user-select: none;\ 183 | -o-user-select: none;\ 184 | -ms-user-select: none;\ 185 | user-select: none;\ 186 | }"; 187 | var HashHandler = require("../keyboard/hash_handler").HashHandler; 188 | var keyUtil = require("../lib/keys"); 189 | 190 | dom.importCssString(searchboxCss, "ace_searchbox"); 191 | 192 | var html = ''.replace(/>\s+/g, ">"); 210 | 211 | var SearchBox = function(editor, range, showReplaceForm) { 212 | var div = dom.createElement("div"); 213 | div.innerHTML = html; 214 | this.element = div.firstChild; 215 | 216 | this.$init(); 217 | this.setEditor(editor); 218 | }; 219 | 220 | (function() { 221 | this.setEditor = function(editor) { 222 | editor.searchBox = this; 223 | editor.container.appendChild(this.element); 224 | this.editor = editor; 225 | }; 226 | 227 | this.$initElements = function(sb) { 228 | this.searchBox = sb.querySelector(".ace_search_form"); 229 | this.replaceBox = sb.querySelector(".ace_replace_form"); 230 | this.searchOptions = sb.querySelector(".ace_search_options"); 231 | this.regExpOption = sb.querySelector("[action=toggleRegexpMode]"); 232 | this.caseSensitiveOption = sb.querySelector("[action=toggleCaseSensitive]"); 233 | this.wholeWordOption = sb.querySelector("[action=toggleWholeWords]"); 234 | this.searchInput = this.searchBox.querySelector(".ace_search_field"); 235 | this.replaceInput = this.replaceBox.querySelector(".ace_search_field"); 236 | }; 237 | 238 | this.$init = function() { 239 | var sb = this.element; 240 | 241 | this.$initElements(sb); 242 | 243 | var _this = this; 244 | event.addListener(sb, "mousedown", function(e) { 245 | setTimeout(function(){ 246 | _this.activeInput.focus(); 247 | }, 0); 248 | event.stopPropagation(e); 249 | }); 250 | event.addListener(sb, "click", function(e) { 251 | var t = e.target || e.srcElement; 252 | var action = t.getAttribute("action"); 253 | if (action && _this[action]) 254 | _this[action](); 255 | else if (_this.$searchBarKb.commands[action]) 256 | _this.$searchBarKb.commands[action].exec(_this); 257 | event.stopPropagation(e); 258 | }); 259 | 260 | event.addCommandKeyListener(sb, function(e, hashId, keyCode) { 261 | var keyString = keyUtil.keyCodeToString(keyCode); 262 | var command = _this.$searchBarKb.findKeyCommand(hashId, keyString); 263 | if (command && command.exec) { 264 | command.exec(_this); 265 | event.stopEvent(e); 266 | } 267 | }); 268 | 269 | this.$onChange = lang.delayedCall(function() { 270 | _this.find(false, false); 271 | }); 272 | 273 | event.addListener(this.searchInput, "input", function() { 274 | _this.$onChange.schedule(20); 275 | }); 276 | event.addListener(this.searchInput, "focus", function() { 277 | _this.activeInput = _this.searchInput; 278 | _this.searchInput.value && _this.highlight(); 279 | }); 280 | event.addListener(this.replaceInput, "focus", function() { 281 | _this.activeInput = _this.replaceInput; 282 | _this.searchInput.value && _this.highlight(); 283 | }); 284 | }; 285 | this.$closeSearchBarKb = new HashHandler([{ 286 | bindKey: "Esc", 287 | name: "closeSearchBar", 288 | exec: function(editor) { 289 | editor.searchBox.hide(); 290 | } 291 | }]); 292 | this.$searchBarKb = new HashHandler(); 293 | this.$searchBarKb.bindKeys({ 294 | "Ctrl-f|Command-f|Ctrl-H|Command-Option-F": function(sb) { 295 | var isReplace = sb.isReplace = !sb.isReplace; 296 | sb.replaceBox.style.display = isReplace ? "" : "none"; 297 | sb[isReplace ? "replaceInput" : "searchInput"].focus(); 298 | }, 299 | "Ctrl-G|Command-G": function(sb) { 300 | sb.findNext(); 301 | }, 302 | "Ctrl-Shift-G|Command-Shift-G": function(sb) { 303 | sb.findPrev(); 304 | }, 305 | "esc": function(sb) { 306 | setTimeout(function() { sb.hide();}); 307 | }, 308 | "Return": function(sb) { 309 | if (sb.activeInput == sb.replaceInput) 310 | sb.replace(); 311 | sb.findNext(); 312 | }, 313 | "Shift-Return": function(sb) { 314 | if (sb.activeInput == sb.replaceInput) 315 | sb.replace(); 316 | sb.findPrev(); 317 | }, 318 | "Tab": function(sb) { 319 | (sb.activeInput == sb.replaceInput ? sb.searchInput : sb.replaceInput).focus(); 320 | } 321 | }); 322 | 323 | this.$searchBarKb.addCommands([{ 324 | name: "toggleRegexpMode", 325 | bindKey: {win: "Alt-R|Alt-/", mac: "Ctrl-Alt-R|Ctrl-Alt-/"}, 326 | exec: function(sb) { 327 | sb.regExpOption.checked = !sb.regExpOption.checked; 328 | sb.$syncOptions(); 329 | } 330 | }, { 331 | name: "toggleCaseSensitive", 332 | bindKey: {win: "Alt-C|Alt-I", mac: "Ctrl-Alt-R|Ctrl-Alt-I"}, 333 | exec: function(sb) { 334 | sb.caseSensitiveOption.checked = !sb.caseSensitiveOption.checked; 335 | sb.$syncOptions(); 336 | } 337 | }, { 338 | name: "toggleWholeWords", 339 | bindKey: {win: "Alt-B|Alt-W", mac: "Ctrl-Alt-B|Ctrl-Alt-W"}, 340 | exec: function(sb) { 341 | sb.wholeWordOption.checked = !sb.wholeWordOption.checked; 342 | sb.$syncOptions(); 343 | } 344 | }]); 345 | 346 | this.$syncOptions = function() { 347 | dom.setCssClass(this.regExpOption, "checked", this.regExpOption.checked); 348 | dom.setCssClass(this.wholeWordOption, "checked", this.wholeWordOption.checked); 349 | dom.setCssClass(this.caseSensitiveOption, "checked", this.caseSensitiveOption.checked); 350 | this.find(false, false); 351 | }; 352 | 353 | this.highlight = function(re) { 354 | this.editor.session.highlight(re || this.editor.$search.$options.re); 355 | this.editor.renderer.updateBackMarkers() 356 | }; 357 | this.find = function(skipCurrent, backwards) { 358 | var range = this.editor.find(this.searchInput.value, { 359 | skipCurrent: skipCurrent, 360 | backwards: backwards, 361 | wrap: true, 362 | regExp: this.regExpOption.checked, 363 | caseSensitive: this.caseSensitiveOption.checked, 364 | wholeWord: this.wholeWordOption.checked 365 | }); 366 | var noMatch = !range && this.searchInput.value; 367 | dom.setCssClass(this.searchBox, "ace_nomatch", noMatch); 368 | this.editor._emit("findSearchBox", { match: !noMatch }); 369 | this.highlight(); 370 | }; 371 | this.findNext = function() { 372 | this.find(true, false); 373 | }; 374 | this.findPrev = function() { 375 | this.find(true, true); 376 | }; 377 | this.replace = function() { 378 | if (!this.editor.getReadOnly()) 379 | this.editor.replace(this.replaceInput.value); 380 | }; 381 | this.replaceAndFindNext = function() { 382 | if (!this.editor.getReadOnly()) { 383 | this.editor.replace(this.replaceInput.value); 384 | this.findNext() 385 | } 386 | }; 387 | this.replaceAll = function() { 388 | if (!this.editor.getReadOnly()) 389 | this.editor.replaceAll(this.replaceInput.value); 390 | }; 391 | 392 | this.hide = function() { 393 | this.element.style.display = "none"; 394 | this.editor.keyBinding.removeKeyboardHandler(this.$closeSearchBarKb); 395 | this.editor.focus(); 396 | }; 397 | this.show = function(value, isReplace) { 398 | this.element.style.display = ""; 399 | this.replaceBox.style.display = isReplace ? "" : "none"; 400 | 401 | this.isReplace = isReplace; 402 | 403 | if (value) 404 | this.searchInput.value = value; 405 | this.searchInput.focus(); 406 | this.searchInput.select(); 407 | 408 | this.editor.keyBinding.addKeyboardHandler(this.$closeSearchBarKb); 409 | }; 410 | 411 | }).call(SearchBox.prototype); 412 | 413 | exports.SearchBox = SearchBox; 414 | 415 | exports.Search = function(editor, isReplace) { 416 | var sb = editor.searchBox || new SearchBox(editor); 417 | sb.show(editor.session.getTextRange(), isReplace); 418 | }; 419 | 420 | }); 421 | -------------------------------------------------------------------------------- /server/src/main/resources/ace/ext-settings_menu.js: -------------------------------------------------------------------------------- 1 | /* ***** BEGIN LICENSE BLOCK ***** 2 | * Distributed under the BSD license: 3 | * 4 | * Copyright (c) 2013 Matthew Christopher Kastor-Inare III, Atropa Inc. Intl 5 | * All rights reserved. 6 | * 7 | * Contributed to Ajax.org under the BSD license. 8 | * 9 | * Redistribution and use in source and binary forms, with or without 10 | * modification, are permitted provided that the following conditions are met: 11 | * * Redistributions of source code must retain the above copyright 12 | * notice, this list of conditions and the following disclaimer. 13 | * * Redistributions in binary form must reproduce the above copyright 14 | * notice, this list of conditions and the following disclaimer in the 15 | * documentation and/or other materials provided with the distribution. 16 | * * Neither the name of Ajax.org B.V. nor the 17 | * names of its contributors may be used to endorse or promote products 18 | * derived from this software without specific prior written permission. 19 | * 20 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 21 | * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 22 | * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | * DISCLAIMED. IN NO EVENT SHALL AJAX.ORG B.V. BE LIABLE FOR ANY 24 | * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 25 | * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 26 | * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 27 | * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 28 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 29 | * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | * 31 | * ***** END LICENSE BLOCK ***** */ 32 | 33 | ace.define('ace/ext/settings_menu', ['require', 'exports', 'module' , 'ace/ext/menu_tools/generate_settings_menu', 'ace/ext/menu_tools/overlay_page', 'ace/editor'], function(require, exports, module) { 34 | 35 | var generateSettingsMenu = require('./menu_tools/generate_settings_menu').generateSettingsMenu; 36 | var overlayPage = require('./menu_tools/overlay_page').overlayPage; 37 | function showSettingsMenu(editor) { 38 | var sm = document.getElementById('ace_settingsmenu'); 39 | if (!sm) 40 | overlayPage(editor, generateSettingsMenu(editor), '0', '0', '0'); 41 | } 42 | module.exports.init = function(editor) { 43 | var Editor = require("ace/editor").Editor; 44 | Editor.prototype.showSettingsMenu = function() { 45 | showSettingsMenu(this); 46 | }; 47 | }; 48 | }); 49 | 50 | ace.define('ace/ext/menu_tools/generate_settings_menu', ['require', 'exports', 'module' , 'ace/ext/menu_tools/element_generator', 'ace/ext/menu_tools/add_editor_menu_options', 'ace/ext/menu_tools/get_set_functions'], function(require, exports, module) { 51 | 52 | var egen = require('./element_generator'); 53 | var addEditorMenuOptions = require('./add_editor_menu_options').addEditorMenuOptions; 54 | var getSetFunctions = require('./get_set_functions').getSetFunctions; 55 | module.exports.generateSettingsMenu = function generateSettingsMenu (editor) { 56 | var elements = []; 57 | function cleanupElementsList() { 58 | elements.sort(function(a, b) { 59 | var x = a.getAttribute('contains'); 60 | var y = b.getAttribute('contains'); 61 | return x.localeCompare(y); 62 | }); 63 | } 64 | function wrapElements() { 65 | var topmenu = document.createElement('div'); 66 | topmenu.setAttribute('id', 'ace_settingsmenu'); 67 | elements.forEach(function(element) { 68 | topmenu.appendChild(element); 69 | }); 70 | return topmenu; 71 | } 72 | function createNewEntry(obj, clss, item, val) { 73 | var el; 74 | var div = document.createElement('div'); 75 | div.setAttribute('contains', item); 76 | div.setAttribute('class', 'ace_optionsMenuEntry'); 77 | div.setAttribute('style', 'clear: both;'); 78 | 79 | div.appendChild(egen.createLabel( 80 | item.replace(/^set/, '').replace(/([A-Z])/g, ' $1').trim(), 81 | item 82 | )); 83 | 84 | if (Array.isArray(val)) { 85 | el = egen.createSelection(item, val, clss); 86 | el.addEventListener('change', function(e) { 87 | try{ 88 | editor.menuOptions[e.target.id].forEach(function(x) { 89 | if(x.textContent !== e.target.textContent) { 90 | delete x.selected; 91 | } 92 | }); 93 | obj[e.target.id](e.target.value); 94 | } catch (err) { 95 | throw new Error(err); 96 | } 97 | }); 98 | } else if(typeof val === 'boolean') { 99 | el = egen.createCheckbox(item, val, clss); 100 | el.addEventListener('change', function(e) { 101 | try{ 102 | obj[e.target.id](!!e.target.checked); 103 | } catch (err) { 104 | throw new Error(err); 105 | } 106 | }); 107 | } else { 108 | el = egen.createInput(item, val, clss); 109 | el.addEventListener('change', function(e) { 110 | try{ 111 | if(e.target.value === 'true') { 112 | obj[e.target.id](true); 113 | } else if(e.target.value === 'false') { 114 | obj[e.target.id](false); 115 | } else { 116 | obj[e.target.id](e.target.value); 117 | } 118 | } catch (err) { 119 | throw new Error(err); 120 | } 121 | }); 122 | } 123 | el.style.cssText = 'float:right;'; 124 | div.appendChild(el); 125 | return div; 126 | } 127 | function makeDropdown(item, esr, clss, fn) { 128 | var val = editor.menuOptions[item]; 129 | var currentVal = esr[fn](); 130 | if (typeof currentVal == 'object') 131 | currentVal = currentVal.$id; 132 | val.forEach(function(valuex) { 133 | if (valuex.value === currentVal) 134 | valuex.selected = 'selected'; 135 | }); 136 | return createNewEntry(esr, clss, item, val); 137 | } 138 | function handleSet(setObj) { 139 | var item = setObj.functionName; 140 | var esr = setObj.parentObj; 141 | var clss = setObj.parentName; 142 | var val; 143 | var fn = item.replace(/^set/, 'get'); 144 | if(editor.menuOptions[item] !== undefined) { 145 | elements.push(makeDropdown(item, esr, clss, fn)); 146 | } else if(typeof esr[fn] === 'function') { 147 | try { 148 | val = esr[fn](); 149 | if(typeof val === 'object') { 150 | val = val.$id; 151 | } 152 | elements.push( 153 | createNewEntry(esr, clss, item, val) 154 | ); 155 | } catch (e) { 156 | } 157 | } 158 | } 159 | addEditorMenuOptions(editor); 160 | getSetFunctions(editor).forEach(function(setObj) { 161 | handleSet(setObj); 162 | }); 163 | cleanupElementsList(); 164 | return wrapElements(); 165 | }; 166 | 167 | }); 168 | 169 | ace.define('ace/ext/menu_tools/element_generator', ['require', 'exports', 'module' ], function(require, exports, module) { 170 | module.exports.createOption = function createOption (obj) { 171 | var attribute; 172 | var el = document.createElement('option'); 173 | for(attribute in obj) { 174 | if(obj.hasOwnProperty(attribute)) { 175 | if(attribute === 'selected') { 176 | el.setAttribute(attribute, obj[attribute]); 177 | } else { 178 | el[attribute] = obj[attribute]; 179 | } 180 | } 181 | } 182 | return el; 183 | }; 184 | module.exports.createCheckbox = function createCheckbox (id, checked, clss) { 185 | var el = document.createElement('input'); 186 | el.setAttribute('type', 'checkbox'); 187 | el.setAttribute('id', id); 188 | el.setAttribute('name', id); 189 | el.setAttribute('value', checked); 190 | el.setAttribute('class', clss); 191 | if(checked) { 192 | el.setAttribute('checked', 'checked'); 193 | } 194 | return el; 195 | }; 196 | module.exports.createInput = function createInput (id, value, clss) { 197 | var el = document.createElement('input'); 198 | el.setAttribute('type', 'text'); 199 | el.setAttribute('id', id); 200 | el.setAttribute('name', id); 201 | el.setAttribute('value', value); 202 | el.setAttribute('class', clss); 203 | return el; 204 | }; 205 | module.exports.createLabel = function createLabel (text, labelFor) { 206 | var el = document.createElement('label'); 207 | el.setAttribute('for', labelFor); 208 | el.textContent = text; 209 | return el; 210 | }; 211 | module.exports.createSelection = function createSelection (id, values, clss) { 212 | var el = document.createElement('select'); 213 | el.setAttribute('id', id); 214 | el.setAttribute('name', id); 215 | el.setAttribute('class', clss); 216 | values.forEach(function(item) { 217 | el.appendChild(module.exports.createOption(item)); 218 | }); 219 | return el; 220 | }; 221 | 222 | }); 223 | 224 | ace.define('ace/ext/menu_tools/add_editor_menu_options', ['require', 'exports', 'module' , 'ace/ext/modelist', 'ace/ext/themelist'], function(require, exports, module) { 225 | module.exports.addEditorMenuOptions = function addEditorMenuOptions (editor) { 226 | var modelist = require('../modelist'); 227 | var themelist = require('../themelist'); 228 | editor.menuOptions = { 229 | "setNewLineMode" : [{ 230 | "textContent" : "unix", 231 | "value" : "unix" 232 | }, { 233 | "textContent" : "windows", 234 | "value" : "windows" 235 | }, { 236 | "textContent" : "auto", 237 | "value" : "auto" 238 | }], 239 | "setTheme" : [], 240 | "setMode" : [], 241 | "setKeyboardHandler": [{ 242 | "textContent" : "ace", 243 | "value" : "" 244 | }, { 245 | "textContent" : "vim", 246 | "value" : "ace/keyboard/vim" 247 | }, { 248 | "textContent" : "emacs", 249 | "value" : "ace/keyboard/emacs" 250 | }] 251 | }; 252 | 253 | editor.menuOptions.setTheme = themelist.themes.map(function(theme) { 254 | return { 255 | 'textContent' : theme.desc, 256 | 'value' : theme.theme 257 | }; 258 | }); 259 | 260 | editor.menuOptions.setMode = modelist.modes.map(function(mode) { 261 | return { 262 | 'textContent' : mode.name, 263 | 'value' : mode.mode 264 | }; 265 | }); 266 | }; 267 | 268 | 269 | });ace.define('ace/ext/modelist', ['require', 'exports', 'module' ], function(require, exports, module) { 270 | 271 | 272 | var modes = []; 273 | function getModeForPath(path) { 274 | var mode = modesByName.text; 275 | var fileName = path.split(/[\/\\]/).pop(); 276 | for (var i = 0; i < modes.length; i++) { 277 | if (modes[i].supportsFile(fileName)) { 278 | mode = modes[i]; 279 | break; 280 | } 281 | } 282 | return mode; 283 | } 284 | 285 | var Mode = function(name, caption, extensions) { 286 | this.name = name; 287 | this.caption = caption; 288 | this.mode = "ace/mode/" + name; 289 | this.extensions = extensions; 290 | if (/\^/.test(extensions)) { 291 | var re = extensions.replace(/\|(\^)?/g, function(a, b){ 292 | return "$|" + (b ? "^" : "^.*\\."); 293 | }) + "$"; 294 | } else { 295 | var re = "^.*\\.(" + extensions + ")$"; 296 | } 297 | 298 | this.extRe = new RegExp(re, "gi"); 299 | }; 300 | 301 | Mode.prototype.supportsFile = function(filename) { 302 | return filename.match(this.extRe); 303 | }; 304 | var supportedModes = { 305 | ABAP: ["abap"], 306 | ADA: ["ada|adb"], 307 | ActionScript:["as"], 308 | AsciiDoc: ["asciidoc"], 309 | Assembly_x86:["asm"], 310 | AutoHotKey: ["ahk"], 311 | BatchFile: ["bat|cmd"], 312 | C9Search: ["c9search_results"], 313 | C_Cpp: ["c|cc|cpp|cxx|h|hh|hpp"], 314 | Clojure: ["clj"], 315 | Cobol: ["^CBL|COB"], 316 | coffee: ["^Cakefile|coffee|cf|cson"], 317 | ColdFusion: ["cfm"], 318 | CSharp: ["cs"], 319 | CSS: ["css"], 320 | Curly: ["curly"], 321 | D: ["d|di"], 322 | Dart: ["dart"], 323 | Diff: ["diff|patch"], 324 | Dot: ["dot"], 325 | Erlang: ["erl|hrl"], 326 | EJS: ["ejs"], 327 | Forth: ["frt|fs|ldr"], 328 | FTL: ["ftl"], 329 | Glsl: ["glsl|frag|vert"], 330 | golang: ["go"], 331 | Groovy: ["groovy"], 332 | HAML: ["haml"], 333 | Haskell: ["hs"], 334 | haXe: ["hx"], 335 | HTML: ["htm|html|xhtml"], 336 | HTML_Ruby: ["erb|rhtml|html.erb"], 337 | Ini: ["Ini|conf"], 338 | Jade: ["jade"], 339 | Java: ["java"], 340 | JavaScript: ["js"], 341 | JSON: ["json"], 342 | JSONiq: ["jq"], 343 | JSP: ["jsp"], 344 | JSX: ["jsx"], 345 | Julia: ["jl"], 346 | LaTeX: ["latex|tex|ltx|bib"], 347 | LESS: ["less"], 348 | Liquid: ["liquid"], 349 | Lisp: ["lisp"], 350 | LiveScript: ["ls"], 351 | LogiQL: ["logic|lql"], 352 | LSL: ["lsl"], 353 | Lua: ["lua"], 354 | LuaPage: ["lp"], 355 | Lucene: ["lucene"], 356 | Makefile: ["^GNUmakefile|^makefile|^Makefile|^OCamlMakefile|make"], 357 | MATLAB: ["matlab"], 358 | Markdown: ["md|markdown"], 359 | MySQL: ["mysql"], 360 | MUSHCode: ["mc|mush"], 361 | Nix: ["nix"], 362 | ObjectiveC: ["m|mm"], 363 | OCaml: ["ml|mli"], 364 | Pascal: ["pas|p"], 365 | Perl: ["pl|pm"], 366 | pgSQL: ["pgsql"], 367 | PHP: ["php|phtml"], 368 | Powershell: ["ps1"], 369 | Prolog: ["plg|prolog"], 370 | Properties: ["properties"], 371 | Protobuf: ["proto"], 372 | Python: ["py"], 373 | R: ["r"], 374 | RDoc: ["Rd"], 375 | RHTML: ["Rhtml"], 376 | Ruby: ["ru|gemspec|rake|rb"], 377 | Rust: ["rs"], 378 | SASS: ["sass"], 379 | SCAD: ["scad"], 380 | Scala: ["scala"], 381 | Scheme: ["scm|rkt"], 382 | SCSS: ["scss"], 383 | SH: ["sh|bash"], 384 | snippets: ["snippets"], 385 | SQL: ["sql"], 386 | Stylus: ["styl|stylus"], 387 | SVG: ["svg"], 388 | Tcl: ["tcl"], 389 | Tex: ["tex"], 390 | Text: ["txt"], 391 | Textile: ["textile"], 392 | Toml: ["toml"], 393 | Twig: ["twig"], 394 | Typescript: ["typescript|ts|str"], 395 | VBScript: ["vbs"], 396 | Velocity: ["vm"], 397 | XML: ["xml|rdf|rss|wsdl|xslt|atom|mathml|mml|xul|xbl"], 398 | XQuery: ["xq"], 399 | YAML: ["yaml"] 400 | }; 401 | 402 | var nameOverrides = { 403 | ObjectiveC: "Objective-C", 404 | CSharp: "C#", 405 | golang: "Go", 406 | C_Cpp: "C/C++", 407 | coffee: "CoffeeScript", 408 | HTML_Ruby: "HTML (Ruby)", 409 | FTL: "FreeMarker" 410 | }; 411 | var modesByName = {}; 412 | for (var name in supportedModes) { 413 | var data = supportedModes[name]; 414 | var displayName = nameOverrides[name] || name; 415 | var filename = name.toLowerCase(); 416 | var mode = new Mode(filename, displayName, data[0]); 417 | modesByName[filename] = mode; 418 | modes.push(mode); 419 | } 420 | 421 | module.exports = { 422 | getModeForPath: getModeForPath, 423 | modes: modes, 424 | modesByName: modesByName 425 | }; 426 | 427 | }); 428 | 429 | ace.define('ace/ext/themelist', ['require', 'exports', 'module' , 'ace/ext/themelist_utils/themes'], function(require, exports, module) { 430 | module.exports.themes = require('ace/ext/themelist_utils/themes').themes; 431 | module.exports.ThemeDescription = function(name) { 432 | this.name = name; 433 | this.desc = name.split('_' 434 | ).map( 435 | function(namePart) { 436 | return namePart[0].toUpperCase() + namePart.slice(1); 437 | } 438 | ).join(' '); 439 | this.theme = "ace/theme/" + name; 440 | }; 441 | 442 | module.exports.themesByName = {}; 443 | 444 | module.exports.themes = module.exports.themes.map(function(name) { 445 | module.exports.themesByName[name] = new module.exports.ThemeDescription(name); 446 | return module.exports.themesByName[name]; 447 | }); 448 | 449 | }); 450 | 451 | ace.define('ace/ext/themelist_utils/themes', ['require', 'exports', 'module' ], function(require, exports, module) { 452 | 453 | module.exports.themes = [ 454 | "ambiance", 455 | "chaos", 456 | "chrome", 457 | "clouds", 458 | "clouds_midnight", 459 | "cobalt", 460 | "crimson_editor", 461 | "dawn", 462 | "dreamweaver", 463 | "eclipse", 464 | "github", 465 | "idle_fingers", 466 | "kr_theme", 467 | "merbivore", 468 | "merbivore_soft", 469 | "mono_industrial", 470 | "monokai", 471 | "pastel_on_dark", 472 | "solarized_dark", 473 | "solarized_light", 474 | "terminal", 475 | "textmate", 476 | "tomorrow", 477 | "tomorrow_night", 478 | "tomorrow_night_blue", 479 | "tomorrow_night_bright", 480 | "tomorrow_night_eighties", 481 | "twilight", 482 | "vibrant_ink", 483 | "xcode" 484 | ]; 485 | 486 | }); 487 | 488 | ace.define('ace/ext/menu_tools/get_set_functions', ['require', 'exports', 'module' ], function(require, exports, module) { 489 | module.exports.getSetFunctions = function getSetFunctions (editor) { 490 | var out = []; 491 | var my = { 492 | 'editor' : editor, 493 | 'session' : editor.session, 494 | 'renderer' : editor.renderer 495 | }; 496 | var opts = []; 497 | var skip = [ 498 | 'setOption', 499 | 'setUndoManager', 500 | 'setDocument', 501 | 'setValue', 502 | 'setBreakpoints', 503 | 'setScrollTop', 504 | 'setScrollLeft', 505 | 'setSelectionStyle', 506 | 'setWrapLimitRange' 507 | ]; 508 | ['renderer', 'session', 'editor'].forEach(function(esra) { 509 | var esr = my[esra]; 510 | var clss = esra; 511 | for(var fn in esr) { 512 | if(skip.indexOf(fn) === -1) { 513 | if(/^set/.test(fn) && opts.indexOf(fn) === -1) { 514 | opts.push(fn); 515 | out.push({ 516 | 'functionName' : fn, 517 | 'parentObj' : esr, 518 | 'parentName' : clss 519 | }); 520 | } 521 | } 522 | } 523 | }); 524 | return out; 525 | }; 526 | 527 | }); 528 | 529 | ace.define('ace/ext/menu_tools/overlay_page', ['require', 'exports', 'module' , 'ace/lib/dom'], function(require, exports, module) { 530 | 531 | var dom = require("../../lib/dom"); 532 | var cssText = "#ace_settingsmenu, #kbshortcutmenu {\ 533 | background-color: #F7F7F7;\ 534 | color: black;\ 535 | box-shadow: -5px 4px 5px rgba(126, 126, 126, 0.55);\ 536 | padding: 1em 0.5em 2em 1em;\ 537 | overflow: auto;\ 538 | position: absolute;\ 539 | margin: 0;\ 540 | bottom: 0;\ 541 | right: 0;\ 542 | top: 0;\ 543 | z-index: 9991;\ 544 | cursor: default;\ 545 | }\ 546 | .ace_dark #ace_settingsmenu, .ace_dark #kbshortcutmenu {\ 547 | box-shadow: -20px 10px 25px rgba(126, 126, 126, 0.25);\ 548 | background-color: rgba(255, 255, 255, 0.6);\ 549 | color: black;\ 550 | }\ 551 | .ace_optionsMenuEntry:hover {\ 552 | background-color: rgba(100, 100, 100, 0.1);\ 553 | -webkit-transition: all 0.5s;\ 554 | transition: all 0.3s\ 555 | }\ 556 | .ace_closeButton {\ 557 | background: rgba(245, 146, 146, 0.5);\ 558 | border: 1px solid #F48A8A;\ 559 | border-radius: 50%;\ 560 | padding: 7px;\ 561 | position: absolute;\ 562 | right: -8px;\ 563 | top: -8px;\ 564 | z-index: 1000;\ 565 | }\ 566 | .ace_closeButton{\ 567 | background: rgba(245, 146, 146, 0.9);\ 568 | }\ 569 | .ace_optionsMenuKey {\ 570 | color: darkslateblue;\ 571 | font-weight: bold;\ 572 | }\ 573 | .ace_optionsMenuCommand {\ 574 | color: darkcyan;\ 575 | font-weight: normal;\ 576 | }"; 577 | dom.importCssString(cssText); 578 | module.exports.overlayPage = function overlayPage(editor, contentElement, top, right, bottom, left) { 579 | top = top ? 'top: ' + top + ';' : ''; 580 | bottom = bottom ? 'bottom: ' + bottom + ';' : ''; 581 | right = right ? 'right: ' + right + ';' : ''; 582 | left = left ? 'left: ' + left + ';' : ''; 583 | 584 | var closer = document.createElement('div'); 585 | var contentContainer = document.createElement('div'); 586 | 587 | function documentEscListener(e) { 588 | if (e.keyCode === 27) { 589 | closer.click(); 590 | } 591 | } 592 | 593 | closer.style.cssText = 'margin: 0; padding: 0; ' + 594 | 'position: fixed; top:0; bottom:0; left:0; right:0;' + 595 | 'z-index: 9990; ' + 596 | 'background-color: rgba(0, 0, 0, 0.3);'; 597 | closer.addEventListener('click', function() { 598 | document.removeEventListener('keydown', documentEscListener); 599 | closer.parentNode.removeChild(closer); 600 | editor.focus(); 601 | closer = null; 602 | }); 603 | document.addEventListener('keydown', documentEscListener); 604 | 605 | contentContainer.style.cssText = top + right + bottom + left; 606 | contentContainer.addEventListener('click', function(e) { 607 | e.stopPropagation(); 608 | }); 609 | 610 | var wrapper = dom.createElement("div"); 611 | wrapper.style.position = "relative"; 612 | 613 | var closeButton = dom.createElement("div"); 614 | closeButton.className = "ace_closeButton"; 615 | closeButton.addEventListener('click', function() { 616 | closer.click(); 617 | }); 618 | 619 | wrapper.appendChild(closeButton); 620 | contentContainer.appendChild(wrapper); 621 | 622 | contentContainer.appendChild(contentElement); 623 | closer.appendChild(contentContainer); 624 | document.body.appendChild(closer); 625 | editor.blur(); 626 | }; 627 | 628 | }); -------------------------------------------------------------------------------- /server/src/main/resources/ace/ext-spellcheck.js: -------------------------------------------------------------------------------- 1 | ace.define('ace/ext/spellcheck', ['require', 'exports', 'module' , 'ace/lib/event', 'ace/editor', 'ace/config'], function(require, exports, module) { 2 | 3 | var event = require("../lib/event"); 4 | 5 | exports.contextMenuHandler = function(e){ 6 | var host = e.target; 7 | var text = host.textInput.getElement(); 8 | if (!host.selection.isEmpty()) 9 | return; 10 | var c = host.getCursorPosition(); 11 | var r = host.session.getWordRange(c.row, c.column); 12 | var w = host.session.getTextRange(r); 13 | 14 | host.session.tokenRe.lastIndex = 0; 15 | if (!host.session.tokenRe.test(w)) 16 | return; 17 | var PLACEHOLDER = "\x01\x01"; 18 | var value = w + " " + PLACEHOLDER; 19 | text.value = value; 20 | text.setSelectionRange(w.length + 1, w.length + 1); 21 | text.setSelectionRange(0, 0); 22 | 23 | var afterKeydown = false; 24 | event.addListener(text, "keydown", function onKeydown() { 25 | event.removeListener(text, "keydown", onKeydown); 26 | afterKeydown = true; 27 | }); 28 | 29 | host.textInput.setInputHandler(function(newVal) { 30 | console.log(newVal , value, text.selectionStart, text.selectionEnd) 31 | if (newVal == value) 32 | return ''; 33 | if (newVal.lastIndexOf(value, 0) === 0) 34 | return newVal.slice(value.length); 35 | if (newVal.substr(text.selectionEnd) == value) 36 | return newVal.slice(0, -value.length); 37 | if (newVal.slice(-2) == PLACEHOLDER) { 38 | var val = newVal.slice(0, -2); 39 | if (val.slice(-1) == " ") { 40 | if (afterKeydown) 41 | return val.substring(0, text.selectionEnd); 42 | val = val.slice(0, -1); 43 | host.session.replace(r, val); 44 | return ""; 45 | } 46 | } 47 | 48 | return newVal; 49 | }); 50 | }; 51 | var Editor = require("../editor").Editor; 52 | require("../config").defineOptions(Editor.prototype, "editor", { 53 | spellcheck: { 54 | set: function(val) { 55 | var text = this.textInput.getElement(); 56 | text.spellcheck = !!val; 57 | if (!val) 58 | this.removeListener("nativecontextmenu", exports.contextMenuHandler); 59 | else 60 | this.on("nativecontextmenu", exports.contextMenuHandler); 61 | }, 62 | value: true 63 | } 64 | }); 65 | 66 | }); 67 | 68 | -------------------------------------------------------------------------------- /server/src/main/resources/ace/ext-split.js: -------------------------------------------------------------------------------- 1 | /* ***** BEGIN LICENSE BLOCK ***** 2 | * Distributed under the BSD license: 3 | * 4 | * Copyright (c) 2010, Ajax.org B.V. 5 | * All rights reserved. 6 | * 7 | * Redistribution and use in source and binary forms, with or without 8 | * modification, are permitted provided that the following conditions are met: 9 | * * Redistributions of source code must retain the above copyright 10 | * notice, this list of conditions and the following disclaimer. 11 | * * Redistributions in binary form must reproduce the above copyright 12 | * notice, this list of conditions and the following disclaimer in the 13 | * documentation and/or other materials provided with the distribution. 14 | * * Neither the name of Ajax.org B.V. nor the 15 | * names of its contributors may be used to endorse or promote products 16 | * derived from this software without specific prior written permission. 17 | * 18 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 19 | * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | * DISCLAIMED. IN NO EVENT SHALL AJAX.ORG B.V. BE LIABLE FOR ANY 22 | * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 23 | * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 24 | * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 25 | * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | * 29 | * ***** END LICENSE BLOCK ***** */ 30 | 31 | ace.define('ace/ext/split', ['require', 'exports', 'module' , 'ace/split'], function(require, exports, module) { 32 | module.exports = require("../split"); 33 | 34 | }); 35 | 36 | ace.define('ace/split', ['require', 'exports', 'module' , 'ace/lib/oop', 'ace/lib/lang', 'ace/lib/event_emitter', 'ace/editor', 'ace/virtual_renderer', 'ace/edit_session'], function(require, exports, module) { 37 | 38 | 39 | var oop = require("./lib/oop"); 40 | var lang = require("./lib/lang"); 41 | var EventEmitter = require("./lib/event_emitter").EventEmitter; 42 | 43 | var Editor = require("./editor").Editor; 44 | var Renderer = require("./virtual_renderer").VirtualRenderer; 45 | var EditSession = require("./edit_session").EditSession; 46 | 47 | 48 | var Split = function(container, theme, splits) { 49 | this.BELOW = 1; 50 | this.BESIDE = 0; 51 | 52 | this.$container = container; 53 | this.$theme = theme; 54 | this.$splits = 0; 55 | this.$editorCSS = ""; 56 | this.$editors = []; 57 | this.$orientation = this.BESIDE; 58 | 59 | this.setSplits(splits || 1); 60 | this.$cEditor = this.$editors[0]; 61 | 62 | 63 | this.on("focus", function(editor) { 64 | this.$cEditor = editor; 65 | }.bind(this)); 66 | }; 67 | 68 | (function(){ 69 | 70 | oop.implement(this, EventEmitter); 71 | 72 | this.$createEditor = function() { 73 | var el = document.createElement("div"); 74 | el.className = this.$editorCSS; 75 | el.style.cssText = "position: absolute; top:0px; bottom:0px"; 76 | this.$container.appendChild(el); 77 | var editor = new Editor(new Renderer(el, this.$theme)); 78 | 79 | editor.on("focus", function() { 80 | this._emit("focus", editor); 81 | }.bind(this)); 82 | 83 | this.$editors.push(editor); 84 | editor.setFontSize(this.$fontSize); 85 | return editor; 86 | }; 87 | 88 | this.setSplits = function(splits) { 89 | var editor; 90 | if (splits < 1) { 91 | throw "The number of splits have to be > 0!"; 92 | } 93 | 94 | if (splits == this.$splits) { 95 | return; 96 | } else if (splits > this.$splits) { 97 | while (this.$splits < this.$editors.length && this.$splits < splits) { 98 | editor = this.$editors[this.$splits]; 99 | this.$container.appendChild(editor.container); 100 | editor.setFontSize(this.$fontSize); 101 | this.$splits ++; 102 | } 103 | while (this.$splits < splits) { 104 | this.$createEditor(); 105 | this.$splits ++; 106 | } 107 | } else { 108 | while (this.$splits > splits) { 109 | editor = this.$editors[this.$splits - 1]; 110 | this.$container.removeChild(editor.container); 111 | this.$splits --; 112 | } 113 | } 114 | this.resize(); 115 | }; 116 | this.getSplits = function() { 117 | return this.$splits; 118 | }; 119 | this.getEditor = function(idx) { 120 | return this.$editors[idx]; 121 | }; 122 | this.getCurrentEditor = function() { 123 | return this.$cEditor; 124 | }; 125 | this.focus = function() { 126 | this.$cEditor.focus(); 127 | }; 128 | this.blur = function() { 129 | this.$cEditor.blur(); 130 | }; 131 | this.setTheme = function(theme) { 132 | this.$editors.forEach(function(editor) { 133 | editor.setTheme(theme); 134 | }); 135 | }; 136 | this.setKeyboardHandler = function(keybinding) { 137 | this.$editors.forEach(function(editor) { 138 | editor.setKeyboardHandler(keybinding); 139 | }); 140 | }; 141 | this.forEach = function(callback, scope) { 142 | this.$editors.forEach(callback, scope); 143 | }; 144 | 145 | 146 | this.$fontSize = ""; 147 | this.setFontSize = function(size) { 148 | this.$fontSize = size; 149 | this.forEach(function(editor) { 150 | editor.setFontSize(size); 151 | }); 152 | }; 153 | 154 | this.$cloneSession = function(session) { 155 | var s = new EditSession(session.getDocument(), session.getMode()); 156 | 157 | var undoManager = session.getUndoManager(); 158 | if (undoManager) { 159 | var undoManagerProxy = new UndoManagerProxy(undoManager, s); 160 | s.setUndoManager(undoManagerProxy); 161 | } 162 | s.$informUndoManager = lang.delayedCall(function() { s.$deltas = []; }); 163 | s.setTabSize(session.getTabSize()); 164 | s.setUseSoftTabs(session.getUseSoftTabs()); 165 | s.setOverwrite(session.getOverwrite()); 166 | s.setBreakpoints(session.getBreakpoints()); 167 | s.setUseWrapMode(session.getUseWrapMode()); 168 | s.setUseWorker(session.getUseWorker()); 169 | s.setWrapLimitRange(session.$wrapLimitRange.min, 170 | session.$wrapLimitRange.max); 171 | s.$foldData = session.$cloneFoldData(); 172 | 173 | return s; 174 | }; 175 | this.setSession = function(session, idx) { 176 | var editor; 177 | if (idx == null) { 178 | editor = this.$cEditor; 179 | } else { 180 | editor = this.$editors[idx]; 181 | } 182 | var isUsed = this.$editors.some(function(editor) { 183 | return editor.session === session; 184 | }); 185 | 186 | if (isUsed) { 187 | session = this.$cloneSession(session); 188 | } 189 | editor.setSession(session); 190 | return session; 191 | }; 192 | this.getOrientation = function() { 193 | return this.$orientation; 194 | }; 195 | this.setOrientation = function(orientation) { 196 | if (this.$orientation == orientation) { 197 | return; 198 | } 199 | this.$orientation = orientation; 200 | this.resize(); 201 | }; 202 | this.resize = function() { 203 | var width = this.$container.clientWidth; 204 | var height = this.$container.clientHeight; 205 | var editor; 206 | 207 | if (this.$orientation == this.BESIDE) { 208 | var editorWidth = width / this.$splits; 209 | for (var i = 0; i < this.$splits; i++) { 210 | editor = this.$editors[i]; 211 | editor.container.style.width = editorWidth + "px"; 212 | editor.container.style.top = "0px"; 213 | editor.container.style.left = i * editorWidth + "px"; 214 | editor.container.style.height = height + "px"; 215 | editor.resize(); 216 | } 217 | } else { 218 | var editorHeight = height / this.$splits; 219 | for (var i = 0; i < this.$splits; i++) { 220 | editor = this.$editors[i]; 221 | editor.container.style.width = width + "px"; 222 | editor.container.style.top = i * editorHeight + "px"; 223 | editor.container.style.left = "0px"; 224 | editor.container.style.height = editorHeight + "px"; 225 | editor.resize(); 226 | } 227 | } 228 | }; 229 | 230 | }).call(Split.prototype); 231 | 232 | 233 | function UndoManagerProxy(undoManager, session) { 234 | this.$u = undoManager; 235 | this.$doc = session; 236 | } 237 | 238 | (function() { 239 | this.execute = function(options) { 240 | this.$u.execute(options); 241 | }; 242 | 243 | this.undo = function() { 244 | var selectionRange = this.$u.undo(true); 245 | if (selectionRange) { 246 | this.$doc.selection.setSelectionRange(selectionRange); 247 | } 248 | }; 249 | 250 | this.redo = function() { 251 | var selectionRange = this.$u.redo(true); 252 | if (selectionRange) { 253 | this.$doc.selection.setSelectionRange(selectionRange); 254 | } 255 | }; 256 | 257 | this.reset = function() { 258 | this.$u.reset(); 259 | }; 260 | 261 | this.hasUndo = function() { 262 | return this.$u.hasUndo(); 263 | }; 264 | 265 | this.hasRedo = function() { 266 | return this.$u.hasRedo(); 267 | }; 268 | }).call(UndoManagerProxy.prototype); 269 | 270 | exports.Split = Split; 271 | }); 272 | -------------------------------------------------------------------------------- /server/src/main/resources/ace/ext-static_highlight.js: -------------------------------------------------------------------------------- 1 | /* ***** BEGIN LICENSE BLOCK ***** 2 | * Distributed under the BSD license: 3 | * 4 | * Copyright (c) 2010, Ajax.org B.V. 5 | * All rights reserved. 6 | * 7 | * Redistribution and use in source and binary forms, with or without 8 | * modification, are permitted provided that the following conditions are met: 9 | * * Redistributions of source code must retain the above copyright 10 | * notice, this list of conditions and the following disclaimer. 11 | * * Redistributions in binary form must reproduce the above copyright 12 | * notice, this list of conditions and the following disclaimer in the 13 | * documentation and/or other materials provided with the distribution. 14 | * * Neither the name of Ajax.org B.V. nor the 15 | * names of its contributors may be used to endorse or promote products 16 | * derived from this software without specific prior written permission. 17 | * 18 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 19 | * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | * DISCLAIMED. IN NO EVENT SHALL AJAX.ORG B.V. BE LIABLE FOR ANY 22 | * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 23 | * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 24 | * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 25 | * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | * 29 | * ***** END LICENSE BLOCK ***** */ 30 | 31 | ace.define('ace/ext/static_highlight', ['require', 'exports', 'module' , 'ace/edit_session', 'ace/layer/text', 'ace/config'], function(require, exports, module) { 32 | 33 | 34 | var EditSession = require("../edit_session").EditSession; 35 | var TextLayer = require("../layer/text").Text; 36 | var baseStyles = ".ace_static_highlight {\ 37 | font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', 'source-code-pro', 'Droid Sans Mono', monospace;\ 38 | font-size: 12px;\ 39 | }\ 40 | .ace_static_highlight .ace_gutter {\ 41 | width: 25px !important;\ 42 | display: block;\ 43 | float: left;\ 44 | text-align: right;\ 45 | padding: 0 3px 0 0;\ 46 | margin-right: 3px;\ 47 | position: static !important;\ 48 | }\ 49 | .ace_static_highlight .ace_line { clear: both; }\ 50 | .ace_static_highlight .ace_gutter-cell {\ 51 | -moz-user-select: -moz-none;\ 52 | -khtml-user-select: none;\ 53 | -webkit-user-select: none;\ 54 | user-select: none;\ 55 | }"; 56 | var config = require("../config"); 57 | 58 | exports.render = function(input, mode, theme, lineStart, disableGutter, callback) { 59 | var waiting = 0; 60 | var modeCache = EditSession.prototype.$modes; 61 | if (typeof theme == "string") { 62 | waiting++; 63 | config.loadModule(['theme', theme], function(m) { 64 | theme = m; 65 | --waiting || done(); 66 | }); 67 | } 68 | 69 | if (typeof mode == "string") { 70 | waiting++; 71 | config.loadModule(['mode', mode], function(m) { 72 | if (!modeCache[mode]) modeCache[mode] = new m.Mode(); 73 | mode = modeCache[mode]; 74 | --waiting || done(); 75 | }); 76 | } 77 | function done() { 78 | var result = exports.renderSync(input, mode, theme, lineStart, disableGutter); 79 | return callback ? callback(result) : result; 80 | } 81 | return waiting || done(); 82 | }; 83 | 84 | exports.renderSync = function(input, mode, theme, lineStart, disableGutter) { 85 | lineStart = parseInt(lineStart || 1, 10); 86 | 87 | var session = new EditSession(""); 88 | session.setUseWorker(false); 89 | session.setMode(mode); 90 | 91 | var textLayer = new TextLayer(document.createElement("div")); 92 | textLayer.setSession(session); 93 | textLayer.config = { 94 | characterWidth: 10, 95 | lineHeight: 20 96 | }; 97 | 98 | session.setValue(input); 99 | 100 | var stringBuilder = []; 101 | var length = session.getLength(); 102 | 103 | for(var ix = 0; ix < length; ix++) { 104 | stringBuilder.push("
"); 105 | if (!disableGutter) 106 | stringBuilder.push("" + (ix + lineStart) + ""); 107 | textLayer.$renderLine(stringBuilder, ix, true, false); 108 | stringBuilder.push("
"); 109 | } 110 | var html = "
" + 111 | "
" + 112 | stringBuilder.join("") + 113 | "
" + 114 | "
"; 115 | 116 | textLayer.destroy(); 117 | 118 | return { 119 | css: baseStyles + theme.cssText, 120 | html: html 121 | }; 122 | }; 123 | 124 | }); 125 | -------------------------------------------------------------------------------- /server/src/main/resources/ace/ext-statusbar.js: -------------------------------------------------------------------------------- 1 | ace.define('ace/ext/statusbar', ['require', 'exports', 'module' , 'ace/lib/dom', 'ace/lib/lang'], function(require, exports, module) { 2 | var dom = require("ace/lib/dom"); 3 | var lang = require("ace/lib/lang"); 4 | 5 | var StatusBar = function(editor, parentNode) { 6 | this.element = dom.createElement("div"); 7 | this.element.className = "ace_status-indicator"; 8 | this.element.style.cssText = "display: inline-block;"; 9 | parentNode.appendChild(this.element); 10 | 11 | var statusUpdate = lang.delayedCall(function(){ 12 | this.updateStatus(editor) 13 | }.bind(this)); 14 | editor.on("changeStatus", function() { 15 | statusUpdate.schedule(100); 16 | }); 17 | editor.on("changeSelection", function() { 18 | statusUpdate.schedule(100); 19 | }); 20 | }; 21 | 22 | (function(){ 23 | this.updateStatus = function(editor) { 24 | var status = []; 25 | function add(str, separator) { 26 | str && status.push(str, separator || "|"); 27 | } 28 | 29 | if (editor.$vimModeHandler) 30 | add(editor.$vimModeHandler.getStatusText()); 31 | else if (editor.commands.recording) 32 | add("REC"); 33 | 34 | var c = editor.selection.lead; 35 | add(c.row + ":" + c.column, " "); 36 | if (!editor.selection.isEmpty()) { 37 | var r = editor.getSelectionRange(); 38 | add("(" + (r.end.row - r.start.row) + ":" +(r.end.column - r.start.column) + ")"); 39 | } 40 | status.pop(); 41 | this.element.textContent = status.join(""); 42 | }; 43 | }).call(StatusBar.prototype); 44 | 45 | exports.StatusBar = StatusBar; 46 | 47 | }); -------------------------------------------------------------------------------- /server/src/main/resources/ace/ext-textarea.js: -------------------------------------------------------------------------------- 1 | /* ***** BEGIN LICENSE BLOCK ***** 2 | * Distributed under the BSD license: 3 | * 4 | * Copyright (c) 2010, Ajax.org B.V. 5 | * All rights reserved. 6 | * 7 | * Redistribution and use in source and binary forms, with or without 8 | * modification, are permitted provided that the following conditions are met: 9 | * * Redistributions of source code must retain the above copyright 10 | * notice, this list of conditions and the following disclaimer. 11 | * * Redistributions in binary form must reproduce the above copyright 12 | * notice, this list of conditions and the following disclaimer in the 13 | * documentation and/or other materials provided with the distribution. 14 | * * Neither the name of Ajax.org B.V. nor the 15 | * names of its contributors may be used to endorse or promote products 16 | * derived from this software without specific prior written permission. 17 | * 18 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 19 | * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | * DISCLAIMED. IN NO EVENT SHALL AJAX.ORG B.V. BE LIABLE FOR ANY 22 | * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 23 | * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 24 | * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 25 | * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | * 29 | * ***** END LICENSE BLOCK ***** */ 30 | 31 | ace.define('ace/ext/textarea', ['require', 'exports', 'module' , 'ace/lib/event', 'ace/lib/useragent', 'ace/lib/net', 'ace/ace', 'ace/theme/textmate', 'ace/mode/text'], function(require, exports, module) { 32 | 33 | 34 | var event = require("../lib/event"); 35 | var UA = require("../lib/useragent"); 36 | var net = require("../lib/net"); 37 | var ace = require("../ace"); 38 | 39 | require("../theme/textmate"); 40 | 41 | module.exports = exports = ace; 42 | var getCSSProperty = function(element, container, property) { 43 | var ret = element.style[property]; 44 | 45 | if (!ret) { 46 | if (window.getComputedStyle) { 47 | ret = window.getComputedStyle(element, '').getPropertyValue(property); 48 | } else { 49 | ret = element.currentStyle[property]; 50 | } 51 | } 52 | 53 | if (!ret || ret == 'auto' || ret == 'intrinsic') { 54 | ret = container.style[property]; 55 | } 56 | return ret; 57 | }; 58 | 59 | function applyStyles(elm, styles) { 60 | for (var style in styles) { 61 | elm.style[style] = styles[style]; 62 | } 63 | } 64 | 65 | function setupContainer(element, getValue) { 66 | if (element.type != 'textarea') { 67 | throw "Textarea required!"; 68 | } 69 | 70 | var parentNode = element.parentNode; 71 | var container = document.createElement('div'); 72 | var resizeEvent = function() { 73 | var style = 'position:relative;'; 74 | [ 75 | 'margin-top', 'margin-left', 'margin-right', 'margin-bottom' 76 | ].forEach(function(item) { 77 | style += item + ':' + 78 | getCSSProperty(element, container, item) + ';'; 79 | }); 80 | var width = getCSSProperty(element, container, 'width') || (element.clientWidth + "px"); 81 | var height = getCSSProperty(element, container, 'height') || (element.clientHeight + "px"); 82 | style += 'height:' + height + ';width:' + width + ';'; 83 | style += 'display:inline-block;'; 84 | container.setAttribute('style', style); 85 | }; 86 | event.addListener(window, 'resize', resizeEvent); 87 | resizeEvent(); 88 | parentNode.insertBefore(container, element.nextSibling); 89 | while (parentNode !== document) { 90 | if (parentNode.tagName.toUpperCase() === 'FORM') { 91 | var oldSumit = parentNode.onsubmit; 92 | parentNode.onsubmit = function(evt) { 93 | element.value = getValue(); 94 | if (oldSumit) { 95 | oldSumit.call(this, evt); 96 | } 97 | }; 98 | break; 99 | } 100 | parentNode = parentNode.parentNode; 101 | } 102 | return container; 103 | } 104 | 105 | exports.transformTextarea = function(element, loader) { 106 | var session; 107 | var container = setupContainer(element, function() { 108 | return session.getValue(); 109 | }); 110 | element.style.display = 'none'; 111 | container.style.background = 'white'; 112 | var editorDiv = document.createElement("div"); 113 | applyStyles(editorDiv, { 114 | top: "0px", 115 | left: "0px", 116 | right: "0px", 117 | bottom: "0px", 118 | border: "1px solid gray", 119 | position: "absolute" 120 | }); 121 | container.appendChild(editorDiv); 122 | 123 | var settingOpener = document.createElement("div"); 124 | applyStyles(settingOpener, { 125 | position: "absolute", 126 | right: "0px", 127 | bottom: "0px", 128 | background: "red", 129 | cursor: "nw-resize", 130 | borderStyle: "solid", 131 | borderWidth: "9px 8px 10px 9px", 132 | width: "2px", 133 | borderColor: "lightblue gray gray lightblue", 134 | zIndex: 101 135 | }); 136 | 137 | var settingDiv = document.createElement("div"); 138 | var settingDivStyles = { 139 | top: "0px", 140 | left: "20%", 141 | right: "0px", 142 | bottom: "0px", 143 | position: "absolute", 144 | padding: "5px", 145 | zIndex: 100, 146 | color: "white", 147 | display: "none", 148 | overflow: "auto", 149 | fontSize: "14px", 150 | boxShadow: "-5px 2px 3px gray" 151 | }; 152 | if (!UA.isOldIE) { 153 | settingDivStyles.backgroundColor = "rgba(0, 0, 0, 0.6)"; 154 | } else { 155 | settingDivStyles.backgroundColor = "#333"; 156 | } 157 | 158 | applyStyles(settingDiv, settingDivStyles); 159 | container.appendChild(settingDiv); 160 | var options = {}; 161 | 162 | var editor = ace.edit(editorDiv); 163 | session = editor.getSession(); 164 | 165 | session.setValue(element.value || element.innerHTML); 166 | editor.focus(); 167 | container.appendChild(settingOpener); 168 | setupApi(editor, editorDiv, settingDiv, ace, options, loader); 169 | setupSettingPanel(settingDiv, settingOpener, editor, options); 170 | 171 | var state = ""; 172 | event.addListener(settingOpener, "mousemove", function(e) { 173 | var rect = this.getBoundingClientRect(); 174 | var x = e.clientX - rect.left, y = e.clientY - rect.top; 175 | if (x + y < (rect.width + rect.height)/2) { 176 | this.style.cursor = "pointer"; 177 | state = "toggle"; 178 | } else { 179 | state = "resize"; 180 | this.style.cursor = "nw-resize"; 181 | } 182 | }); 183 | 184 | event.addListener(settingOpener, "mousedown", function(e) { 185 | if (state == "toggle") { 186 | editor.setDisplaySettings(); 187 | return; 188 | } 189 | container.style.zIndex = 100000; 190 | var rect = container.getBoundingClientRect(); 191 | var startX = rect.width + rect.left - e.clientX; 192 | var startY = rect.height + rect.top - e.clientY; 193 | event.capture(settingOpener, function(e) { 194 | container.style.width = e.clientX - rect.left + startX + "px"; 195 | container.style.height = e.clientY - rect.top + startY + "px"; 196 | editor.resize(); 197 | }, function() {}); 198 | }); 199 | 200 | return editor; 201 | }; 202 | 203 | function load(url, module, callback) { 204 | net.loadScript(url, function() { 205 | require([module], callback); 206 | }); 207 | } 208 | 209 | function setupApi(editor, editorDiv, settingDiv, ace, options, loader) { 210 | var session = editor.getSession(); 211 | var renderer = editor.renderer; 212 | loader = loader || load; 213 | 214 | function toBool(value) { 215 | return value === "true" || value == true; 216 | } 217 | 218 | editor.setDisplaySettings = function(display) { 219 | if (display == null) 220 | display = settingDiv.style.display == "none"; 221 | if (display) { 222 | settingDiv.style.display = "block"; 223 | settingDiv.hideButton.focus(); 224 | editor.on("focus", function onFocus() { 225 | editor.removeListener("focus", onFocus); 226 | settingDiv.style.display = "none"; 227 | }); 228 | } else { 229 | editor.focus(); 230 | } 231 | }; 232 | 233 | editor.$setOption = editor.setOption; 234 | editor.setOption = function(key, value) { 235 | if (options[key] == value) return; 236 | 237 | switch (key) { 238 | case "mode": 239 | if (value != "text") { 240 | loader("mode-" + value + ".js", "ace/mode/" + value, function() { 241 | var aceMode = require("../mode/" + value).Mode; 242 | session.setMode(new aceMode()); 243 | }); 244 | } else { 245 | session.setMode(new (require("../mode/text").Mode)); 246 | } 247 | break; 248 | 249 | case "theme": 250 | if (value != "textmate") { 251 | loader("theme-" + value + ".js", "ace/theme/" + value, function() { 252 | editor.setTheme("ace/theme/" + value); 253 | }); 254 | } else { 255 | editor.setTheme("ace/theme/textmate"); 256 | } 257 | break; 258 | 259 | case "fontSize": 260 | editorDiv.style.fontSize = value; 261 | break; 262 | 263 | case "keybindings": 264 | switch (value) { 265 | case "vim": 266 | editor.setKeyboardHandler("ace/keyboard/vim"); 267 | break; 268 | case "emacs": 269 | editor.setKeyboardHandler("ace/keyboard/emacs"); 270 | break; 271 | default: 272 | editor.setKeyboardHandler(null); 273 | } 274 | break; 275 | 276 | case "softWrap": 277 | switch (value) { 278 | case "off": 279 | session.setUseWrapMode(false); 280 | renderer.setPrintMarginColumn(80); 281 | break; 282 | case "40": 283 | session.setUseWrapMode(true); 284 | session.setWrapLimitRange(40, 40); 285 | renderer.setPrintMarginColumn(40); 286 | break; 287 | case "80": 288 | session.setUseWrapMode(true); 289 | session.setWrapLimitRange(80, 80); 290 | renderer.setPrintMarginColumn(80); 291 | break; 292 | case "free": 293 | session.setUseWrapMode(true); 294 | session.setWrapLimitRange(null, null); 295 | renderer.setPrintMarginColumn(80); 296 | break; 297 | } 298 | break; 299 | 300 | default: 301 | editor.$setOption(key, toBool(value)); 302 | } 303 | 304 | options[key] = value; 305 | }; 306 | 307 | editor.getOption = function(key) { 308 | return options[key]; 309 | }; 310 | 311 | editor.getOptions = function() { 312 | return options; 313 | }; 314 | 315 | editor.setOptions(exports.options); 316 | 317 | return editor; 318 | } 319 | 320 | function setupSettingPanel(settingDiv, settingOpener, editor, options) { 321 | var BOOL = null; 322 | 323 | var desc = { 324 | mode: "Mode:", 325 | gutter: "Display Gutter:", 326 | theme: "Theme:", 327 | fontSize: "Font Size:", 328 | softWrap: "Soft Wrap:", 329 | keybindings: "Keyboard", 330 | showPrintMargin: "Show Print Margin:", 331 | useSoftTabs: "Use Soft Tabs:", 332 | showInvisibles: "Show Invisibles" 333 | }; 334 | 335 | var optionValues = { 336 | mode: { 337 | text: "Plain", 338 | javascript: "JavaScript", 339 | xml: "XML", 340 | html: "HTML", 341 | css: "CSS", 342 | scss: "SCSS", 343 | python: "Python", 344 | php: "PHP", 345 | java: "Java", 346 | ruby: "Ruby", 347 | c_cpp: "C/C++", 348 | coffee: "CoffeeScript", 349 | json: "json", 350 | perl: "Perl", 351 | clojure: "Clojure", 352 | ocaml: "OCaml", 353 | csharp: "C#", 354 | haxe: "haXe", 355 | svg: "SVG", 356 | textile: "Textile", 357 | groovy: "Groovy", 358 | liquid: "Liquid", 359 | Scala: "Scala" 360 | }, 361 | theme: { 362 | clouds: "Clouds", 363 | clouds_midnight: "Clouds Midnight", 364 | cobalt: "Cobalt", 365 | crimson_editor: "Crimson Editor", 366 | dawn: "Dawn", 367 | eclipse: "Eclipse", 368 | idle_fingers: "Idle Fingers", 369 | kr_theme: "Kr Theme", 370 | merbivore: "Merbivore", 371 | merbivore_soft: "Merbivore Soft", 372 | mono_industrial: "Mono Industrial", 373 | monokai: "Monokai", 374 | pastel_on_dark: "Pastel On Dark", 375 | solarized_dark: "Solarized Dark", 376 | solarized_light: "Solarized Light", 377 | textmate: "Textmate", 378 | twilight: "Twilight", 379 | vibrant_ink: "Vibrant Ink" 380 | }, 381 | gutter: BOOL, 382 | fontSize: { 383 | "10px": "10px", 384 | "11px": "11px", 385 | "12px": "12px", 386 | "14px": "14px", 387 | "16px": "16px" 388 | }, 389 | softWrap: { 390 | off: "Off", 391 | 40: "40", 392 | 80: "80", 393 | free: "Free" 394 | }, 395 | keybindings: { 396 | ace: "ace", 397 | vim: "vim", 398 | emacs: "emacs" 399 | }, 400 | showPrintMargin: BOOL, 401 | useSoftTabs: BOOL, 402 | showInvisibles: BOOL 403 | }; 404 | 405 | var table = []; 406 | table.push(""); 407 | 408 | function renderOption(builder, option, obj, cValue) { 409 | if (!obj) { 410 | builder.push( 411 | "" 414 | ); 415 | return; 416 | } 417 | builder.push(""); 430 | } 431 | 432 | for (var option in options) { 433 | table.push(""); 434 | table.push(""); 437 | } 438 | table.push("
SettingValue
", desc[option], ""); 435 | renderOption(table, option, optionValues[option], options[option]); 436 | table.push("
"); 439 | settingDiv.innerHTML = table.join(""); 440 | 441 | var onChange = function(e) { 442 | var select = e.currentTarget; 443 | editor.setOption(select.title, select.value); 444 | }; 445 | var onClick = function(e) { 446 | var cb = e.currentTarget; 447 | editor.setOption(cb.title, cb.checked); 448 | }; 449 | var selects = settingDiv.getElementsByTagName("select"); 450 | for (var i = 0; i < selects.length; i++) 451 | selects[i].onchange = onChange; 452 | var cbs = settingDiv.getElementsByTagName("input"); 453 | for (var i = 0; i < cbs.length; i++) 454 | cbs[i].onclick = onClick; 455 | 456 | 457 | var button = document.createElement("input"); 458 | button.type = "button"; 459 | button.value = "Hide"; 460 | event.addListener(button, "click", function() { 461 | editor.setDisplaySettings(false); 462 | }); 463 | settingDiv.appendChild(button); 464 | settingDiv.hideButton = button; 465 | } 466 | exports.options = { 467 | mode: "text", 468 | theme: "textmate", 469 | gutter: "false", 470 | fontSize: "12px", 471 | softWrap: "off", 472 | keybindings: "ace", 473 | showPrintMargin: "false", 474 | useSoftTabs: "true", 475 | showInvisibles: "false" 476 | }; 477 | 478 | }); 479 | -------------------------------------------------------------------------------- /server/src/main/resources/ace/ext-themelist.js: -------------------------------------------------------------------------------- 1 | /* ***** BEGIN LICENSE BLOCK ***** 2 | * Distributed under the BSD license: 3 | * 4 | * Copyright (c) 2013 Matthew Christopher Kastor-Inare III, Atropa Inc. Intl 5 | * All rights reserved. 6 | * 7 | * Contributed to Ajax.org under the BSD license. 8 | * 9 | * Redistribution and use in source and binary forms, with or without 10 | * modification, are permitted provided that the following conditions are met: 11 | * * Redistributions of source code must retain the above copyright 12 | * notice, this list of conditions and the following disclaimer. 13 | * * Redistributions in binary form must reproduce the above copyright 14 | * notice, this list of conditions and the following disclaimer in the 15 | * documentation and/or other materials provided with the distribution. 16 | * * Neither the name of Ajax.org B.V. nor the 17 | * names of its contributors may be used to endorse or promote products 18 | * derived from this software without specific prior written permission. 19 | * 20 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 21 | * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 22 | * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | * DISCLAIMED. IN NO EVENT SHALL AJAX.ORG B.V. BE LIABLE FOR ANY 24 | * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 25 | * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 26 | * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 27 | * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 28 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 29 | * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | * 31 | * ***** END LICENSE BLOCK ***** */ 32 | 33 | ace.define('ace/ext/themelist', ['require', 'exports', 'module' , 'ace/ext/themelist_utils/themes'], function(require, exports, module) { 34 | module.exports.themes = require('ace/ext/themelist_utils/themes').themes; 35 | module.exports.ThemeDescription = function(name) { 36 | this.name = name; 37 | this.desc = name.split('_' 38 | ).map( 39 | function(namePart) { 40 | return namePart[0].toUpperCase() + namePart.slice(1); 41 | } 42 | ).join(' '); 43 | this.theme = "ace/theme/" + name; 44 | }; 45 | 46 | module.exports.themesByName = {}; 47 | 48 | module.exports.themes = module.exports.themes.map(function(name) { 49 | module.exports.themesByName[name] = new module.exports.ThemeDescription(name); 50 | return module.exports.themesByName[name]; 51 | }); 52 | 53 | }); 54 | 55 | ace.define('ace/ext/themelist_utils/themes', ['require', 'exports', 'module' ], function(require, exports, module) { 56 | 57 | module.exports.themes = [ 58 | "ambiance", 59 | "chaos", 60 | "chrome", 61 | "clouds", 62 | "clouds_midnight", 63 | "cobalt", 64 | "crimson_editor", 65 | "dawn", 66 | "dreamweaver", 67 | "eclipse", 68 | "github", 69 | "idle_fingers", 70 | "kr_theme", 71 | "merbivore", 72 | "merbivore_soft", 73 | "mono_industrial", 74 | "monokai", 75 | "pastel_on_dark", 76 | "solarized_dark", 77 | "solarized_light", 78 | "terminal", 79 | "textmate", 80 | "tomorrow", 81 | "tomorrow_night", 82 | "tomorrow_night_blue", 83 | "tomorrow_night_bright", 84 | "tomorrow_night_eighties", 85 | "twilight", 86 | "vibrant_ink", 87 | "xcode" 88 | ]; 89 | 90 | }); -------------------------------------------------------------------------------- /server/src/main/resources/ace/ext-whitespace.js: -------------------------------------------------------------------------------- 1 | /* ***** BEGIN LICENSE BLOCK ***** 2 | * Distributed under the BSD license: 3 | * 4 | * Copyright (c) 2010, Ajax.org B.V. 5 | * All rights reserved. 6 | * 7 | * Redistribution and use in source and binary forms, with or without 8 | * modification, are permitted provided that the following conditions are met: 9 | * * Redistributions of source code must retain the above copyright 10 | * notice, this list of conditions and the following disclaimer. 11 | * * Redistributions in binary form must reproduce the above copyright 12 | * notice, this list of conditions and the following disclaimer in the 13 | * documentation and/or other materials provided with the distribution. 14 | * * Neither the name of Ajax.org B.V. nor the 15 | * names of its contributors may be used to endorse or promote products 16 | * derived from this software without specific prior written permission. 17 | * 18 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 19 | * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | * DISCLAIMED. IN NO EVENT SHALL AJAX.ORG B.V. BE LIABLE FOR ANY 22 | * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 23 | * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 24 | * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 25 | * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | * 29 | * ***** END LICENSE BLOCK ***** */ 30 | 31 | ace.define('ace/ext/whitespace', ['require', 'exports', 'module' , 'ace/lib/lang'], function(require, exports, module) { 32 | 33 | 34 | var lang = require("../lib/lang"); 35 | exports.$detectIndentation = function(lines, fallback) { 36 | var stats = []; 37 | var changes = []; 38 | var tabIndents = 0; 39 | var prevSpaces = 0; 40 | var max = Math.min(lines.length, 1000); 41 | for (var i = 0; i < max; i++) { 42 | var line = lines[i]; 43 | if (!/^\s*[^*+\-\s]/.test(line)) 44 | continue; 45 | 46 | var tabs = line.match(/^\t*/)[0].length; 47 | if (line[0] == "\t") 48 | tabIndents++; 49 | 50 | var spaces = line.match(/^ */)[0].length; 51 | if (spaces && line[spaces] != "\t") { 52 | var diff = spaces - prevSpaces; 53 | if (diff > 0 && !(prevSpaces%diff) && !(spaces%diff)) 54 | changes[diff] = (changes[diff] || 0) + 1; 55 | 56 | stats[spaces] = (stats[spaces] || 0) + 1; 57 | } 58 | prevSpaces = spaces; 59 | while (line[line.length - 1] == "\\") 60 | line = lines[i++]; 61 | } 62 | 63 | function getScore(indent) { 64 | var score = 0; 65 | for (var i = indent; i < stats.length; i += indent) 66 | score += stats[i] || 0; 67 | return score; 68 | } 69 | 70 | var changesTotal = changes.reduce(function(a,b){return a+b}, 0); 71 | 72 | var first = {score: 0, length: 0}; 73 | var spaceIndents = 0; 74 | for (var i = 1; i < 12; i++) { 75 | if (i == 1) { 76 | spaceIndents = getScore(i); 77 | var score = 1; 78 | } else 79 | var score = getScore(i) / spaceIndents; 80 | 81 | if (changes[i]) { 82 | score += changes[i] / changesTotal; 83 | } 84 | 85 | if (score > first.score) 86 | first = {score: score, length: i}; 87 | } 88 | 89 | if (first.score && first.score > 1.4) 90 | var tabLength = first.length; 91 | 92 | if (tabIndents > spaceIndents + 1) 93 | return {ch: "\t", length: tabLength}; 94 | 95 | if (spaceIndents + 1 > tabIndents) 96 | return {ch: " ", length: tabLength}; 97 | }; 98 | 99 | exports.detectIndentation = function(session) { 100 | var lines = session.getLines(0, 1000); 101 | var indent = exports.$detectIndentation(lines) || {}; 102 | 103 | if (indent.ch) 104 | session.setUseSoftTabs(indent.ch == " "); 105 | 106 | if (indent.length) 107 | session.setTabSize(indent.length); 108 | return indent; 109 | }; 110 | 111 | exports.trimTrailingSpace = function(session, trimEmpty) { 112 | var doc = session.getDocument(); 113 | var lines = doc.getAllLines(); 114 | 115 | var min = trimEmpty ? -1 : 0; 116 | 117 | for (var i = 0, l=lines.length; i < l; i++) { 118 | var line = lines[i]; 119 | var index = line.search(/\s+$/); 120 | 121 | if (index > min) 122 | doc.removeInLine(i, index, line.length); 123 | } 124 | }; 125 | 126 | exports.convertIndentation = function(session, ch, len) { 127 | var oldCh = session.getTabString()[0]; 128 | var oldLen = session.getTabSize(); 129 | if (!len) len = oldLen; 130 | if (!ch) ch = oldCh; 131 | 132 | var tab = ch == "\t" ? ch: lang.stringRepeat(ch, len); 133 | 134 | var doc = session.doc; 135 | var lines = doc.getAllLines(); 136 | 137 | var cache = {}; 138 | var spaceCache = {}; 139 | for (var i = 0, l=lines.length; i < l; i++) { 140 | var line = lines[i]; 141 | var match = line.match(/^\s*/)[0]; 142 | if (match) { 143 | var w = session.$getStringScreenWidth(match)[0]; 144 | var tabCount = Math.floor(w/oldLen); 145 | var reminder = w%oldLen; 146 | var toInsert = cache[tabCount] || (cache[tabCount] = lang.stringRepeat(tab, tabCount)); 147 | toInsert += spaceCache[reminder] || (spaceCache[reminder] = lang.stringRepeat(" ", reminder)); 148 | 149 | if (toInsert != match) { 150 | doc.removeInLine(i, 0, match.length); 151 | doc.insertInLine({row: i, column: 0}, toInsert); 152 | } 153 | } 154 | } 155 | session.setTabSize(len); 156 | session.setUseSoftTabs(ch == " "); 157 | }; 158 | 159 | exports.$parseStringArg = function(text) { 160 | var indent = {}; 161 | if (/t/.test(text)) 162 | indent.ch = "\t"; 163 | else if (/s/.test(text)) 164 | indent.ch = " "; 165 | var m = text.match(/\d+/); 166 | if (m) 167 | indent.length = parseInt(m[0], 10); 168 | return indent; 169 | }; 170 | 171 | exports.$parseArg = function(arg) { 172 | if (!arg) 173 | return {}; 174 | if (typeof arg == "string") 175 | return exports.$parseStringArg(arg); 176 | if (typeof arg.text == "string") 177 | return exports.$parseStringArg(arg.text); 178 | return arg; 179 | }; 180 | 181 | exports.commands = [{ 182 | name: "detectIndentation", 183 | exec: function(editor) { 184 | exports.detectIndentation(editor.session); 185 | } 186 | }, { 187 | name: "trimTrailingSpace", 188 | exec: function(editor) { 189 | exports.trimTrailingSpace(editor.session); 190 | } 191 | }, { 192 | name: "convertIndentation", 193 | exec: function(editor, arg) { 194 | var indent = exports.$parseArg(arg); 195 | exports.convertIndentation(editor.session, indent.ch, indent.length); 196 | } 197 | }, { 198 | name: "setIndentation", 199 | exec: function(editor, arg) { 200 | var indent = exports.$parseArg(arg); 201 | indent.length && editor.session.setTabSize(indent.length); 202 | indent.ch && editor.session.setUseSoftTabs(indent.ch == " "); 203 | } 204 | }]; 205 | 206 | }); 207 | -------------------------------------------------------------------------------- /server/src/main/resources/ace/snippets/markdown.js: -------------------------------------------------------------------------------- 1 | ace.define('ace/snippets/markdown', ['require', 'exports', 'module' ], function(require, exports, module) { 2 | 3 | 4 | exports.snippetText = "# Markdown\n\ 5 | \n\ 6 | # Includes octopress (http://octopress.org/) snippets\n\ 7 | \n\ 8 | snippet [\n\ 9 | [${1:text}](http://${2:address} \"${3:title}\")\n\ 10 | snippet [*\n\ 11 | [${1:link}](${2:`@*`} \"${3:title}\")${4}\n\ 12 | \n\ 13 | snippet [:\n\ 14 | [${1:id}]: http://${2:url} \"${3:title}\"\n\ 15 | snippet [:*\n\ 16 | [${1:id}]: ${2:`@*`} \"${3:title}\"\n\ 17 | \n\ 18 | snippet ![\n\ 19 | ![${1:alttext}](${2:/images/image.jpg} \"${3:title}\")\n\ 20 | snippet ![*\n\ 21 | ![${1:alt}](${2:`@*`} \"${3:title}\")${4}\n\ 22 | \n\ 23 | snippet ![:\n\ 24 | ![${1:id}]: ${2:url} \"${3:title}\"\n\ 25 | snippet ![:*\n\ 26 | ![${1:id}]: ${2:`@*`} \"${3:title}\"\n\ 27 | \n\ 28 | snippet ===\n\ 29 | `repeat('=', strlen(getline(line(\".\") - 1)))`\n\ 30 | \n\ 31 | ${1}\n\ 32 | snippet ---\n\ 33 | `repeat('-', strlen(getline(line(\".\") - 1)))`\n\ 34 | \n\ 35 | ${1}\n\ 36 | \n\ 37 | snippet blockquote\n\ 38 | {% blockquote %}\n\ 39 | ${1:quote}\n\ 40 | {% endblockquote %}\n\ 41 | \n\ 42 | snippet blockquote-author\n\ 43 | {% blockquote ${1:author}, ${2:title} %}\n\ 44 | ${3:quote}\n\ 45 | {% endblockquote %}\n\ 46 | \n\ 47 | snippet blockquote-link\n\ 48 | {% blockquote ${1:author} ${2:URL} ${3:link_text} %}\n\ 49 | ${4:quote}\n\ 50 | {% endblockquote %}\n\ 51 | \n\ 52 | snippet bt-codeblock-short\n\ 53 | ```\n\ 54 | ${1:code_snippet}\n\ 55 | ```\n\ 56 | \n\ 57 | snippet bt-codeblock-full\n\ 58 | ``` ${1:language} ${2:title} ${3:URL} ${4:link_text}\n\ 59 | ${5:code_snippet}\n\ 60 | ```\n\ 61 | \n\ 62 | snippet codeblock-short\n\ 63 | {% codeblock %}\n\ 64 | ${1:code_snippet}\n\ 65 | {% endcodeblock %}\n\ 66 | \n\ 67 | snippet codeblock-full\n\ 68 | {% codeblock ${1:title} lang:${2:language} ${3:URL} ${4:link_text} %}\n\ 69 | ${5:code_snippet}\n\ 70 | {% endcodeblock %}\n\ 71 | \n\ 72 | snippet gist-full\n\ 73 | {% gist ${1:gist_id} ${2:filename} %}\n\ 74 | \n\ 75 | snippet gist-short\n\ 76 | {% gist ${1:gist_id} %}\n\ 77 | \n\ 78 | snippet img\n\ 79 | {% img ${1:class} ${2:URL} ${3:width} ${4:height} ${5:title_text} ${6:alt_text} %}\n\ 80 | \n\ 81 | snippet youtube\n\ 82 | {% youtube ${1:video_id} %}\n\ 83 | \n\ 84 | # The quote should appear only once in the text. It is inherently part of it.\n\ 85 | # See http://octopress.org/docs/plugins/pullquote/ for more info.\n\ 86 | \n\ 87 | snippet pullquote\n\ 88 | {% pullquote %}\n\ 89 | ${1:text} {\" ${2:quote} \"} ${3:text}\n\ 90 | {% endpullquote %}\n\ 91 | "; 92 | exports.scope = "markdown"; 93 | 94 | }); 95 | -------------------------------------------------------------------------------- /server/src/main/resources/ace/theme-merbivore.js: -------------------------------------------------------------------------------- 1 | /* ***** BEGIN LICENSE BLOCK ***** 2 | * Distributed under the BSD license: 3 | * 4 | * Copyright (c) 2010, Ajax.org B.V. 5 | * All rights reserved. 6 | * 7 | * Redistribution and use in source and binary forms, with or without 8 | * modification, are permitted provided that the following conditions are met: 9 | * * Redistributions of source code must retain the above copyright 10 | * notice, this list of conditions and the following disclaimer. 11 | * * Redistributions in binary form must reproduce the above copyright 12 | * notice, this list of conditions and the following disclaimer in the 13 | * documentation and/or other materials provided with the distribution. 14 | * * Neither the name of Ajax.org B.V. nor the 15 | * names of its contributors may be used to endorse or promote products 16 | * derived from this software without specific prior written permission. 17 | * 18 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 19 | * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | * DISCLAIMED. IN NO EVENT SHALL AJAX.ORG B.V. BE LIABLE FOR ANY 22 | * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 23 | * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 24 | * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 25 | * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | * 29 | * ***** END LICENSE BLOCK ***** */ 30 | 31 | ace.define('ace/theme/merbivore', ['require', 'exports', 'module' , 'ace/lib/dom'], function(require, exports, module) { 32 | 33 | exports.isDark = true; 34 | exports.cssClass = "ace-merbivore"; 35 | exports.cssText = ".ace-merbivore .ace_gutter {\ 36 | background: #202020;\ 37 | color: #E6E1DC\ 38 | }\ 39 | .ace-merbivore .ace_print-margin {\ 40 | width: 1px;\ 41 | background: #555651\ 42 | }\ 43 | .ace-merbivore {\ 44 | background-color: #161616;\ 45 | color: #E6E1DC\ 46 | }\ 47 | .ace-merbivore .ace_cursor {\ 48 | border-left: 2px solid #FFFFFF\ 49 | }\ 50 | .ace-merbivore .ace_overwrite-cursors .ace_cursor {\ 51 | border-left: 0px;\ 52 | border-bottom: 1px solid #FFFFFF\ 53 | }\ 54 | .ace-merbivore .ace_marker-layer .ace_selection {\ 55 | background: #454545\ 56 | }\ 57 | .ace-merbivore.ace_multiselect .ace_selection.ace_start {\ 58 | box-shadow: 0 0 3px 0px #161616;\ 59 | border-radius: 2px\ 60 | }\ 61 | .ace-merbivore .ace_marker-layer .ace_step {\ 62 | background: rgb(102, 82, 0)\ 63 | }\ 64 | .ace-merbivore .ace_marker-layer .ace_bracket {\ 65 | margin: -1px 0 0 -1px;\ 66 | border: 1px solid #404040\ 67 | }\ 68 | .ace-merbivore .ace_marker-layer .ace_active-line {\ 69 | background: #333435\ 70 | }\ 71 | .ace-merbivore .ace_gutter-active-line {\ 72 | background-color: #333435\ 73 | }\ 74 | .ace-merbivore .ace_marker-layer .ace_selected-word {\ 75 | border: 1px solid #454545\ 76 | }\ 77 | .ace-merbivore .ace_invisible {\ 78 | color: #404040\ 79 | }\ 80 | .ace-merbivore .ace_entity.ace_name.ace_tag,\ 81 | .ace-merbivore .ace_keyword,\ 82 | .ace-merbivore .ace_meta,\ 83 | .ace-merbivore .ace_meta.ace_tag,\ 84 | .ace-merbivore .ace_storage,\ 85 | .ace-merbivore .ace_support.ace_function {\ 86 | color: #FC6F09\ 87 | }\ 88 | .ace-merbivore .ace_constant,\ 89 | .ace-merbivore .ace_constant.ace_character,\ 90 | .ace-merbivore .ace_constant.ace_character.ace_escape,\ 91 | .ace-merbivore .ace_constant.ace_other,\ 92 | .ace-merbivore .ace_support.ace_type {\ 93 | color: #1EDAFB\ 94 | }\ 95 | .ace-merbivore .ace_constant.ace_character.ace_escape {\ 96 | color: #519F50\ 97 | }\ 98 | .ace-merbivore .ace_constant.ace_language {\ 99 | color: #FDC251\ 100 | }\ 101 | .ace-merbivore .ace_constant.ace_library,\ 102 | .ace-merbivore .ace_string,\ 103 | .ace-merbivore .ace_support.ace_constant {\ 104 | color: #8DFF0A\ 105 | }\ 106 | .ace-merbivore .ace_constant.ace_numeric {\ 107 | color: #58C554\ 108 | }\ 109 | .ace-merbivore .ace_invalid {\ 110 | color: #FFFFFF;\ 111 | background-color: #990000\ 112 | }\ 113 | .ace-merbivore .ace_fold {\ 114 | background-color: #FC6F09;\ 115 | border-color: #E6E1DC\ 116 | }\ 117 | .ace-merbivore .ace_comment {\ 118 | font-style: italic;\ 119 | color: #AD2EA4\ 120 | }\ 121 | .ace-merbivore .ace_entity.ace_other.ace_attribute-name {\ 122 | color: #FFFF89\ 123 | }\ 124 | .ace-merbivore .ace_indent-guide {\ 125 | background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAACCAYAAACZgbYnAAAAEklEQVQImWMQFxf3ZXB1df0PAAdsAmERTkEHAAAAAElFTkSuQmCC) right repeat-y;\ 126 | }"; 127 | 128 | var dom = require("../lib/dom"); 129 | dom.importCssString(exports.cssText, exports.cssClass); 130 | }); 131 | -------------------------------------------------------------------------------- /server/src/main/resources/editor.js: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Editor controls. 4 | 5 | - Creates a WOOT client. 6 | - Connects the web socket. 7 | - Provides display functions for the WOOT client to call. 8 | - Broadcasts changes to the web socket. 9 | */ 10 | //var trace = function() { if (console & console.log) console.log.apply(console, arguments); }; 11 | var trace = function() {}; 12 | 13 | // 14 | // Not all changes need to be broadcast down the web socket 15 | // 16 | // For example, when we insert a character we've received, 17 | // we don't rebroadcast that. 18 | // 19 | 20 | var onAir = true; // Are we broadcasting changes? 21 | 22 | // Execute a code block without broadcasting the change 23 | function offAir(thunk) { 24 | var was = onAir; 25 | onAir = false; 26 | thunk(); 27 | onAir = was; 28 | } 29 | 30 | // 31 | // The editor itself 32 | // 33 | 34 | var editor = ace.edit("editor"); 35 | editor.setTheme("ace/theme/merbivore"); 36 | editor.getSession().getDocument().setNewLineMode("unix"); 37 | editor.getSession().setMode("ace/mode/markdown"); 38 | 39 | 40 | // 41 | // Translation of WOOT changes into Ace changes (deltas) 42 | // 43 | 44 | // Convert ACE "position" (row/column) to WOOT "index": 45 | function idx(position) { 46 | return editor.getSession().getDocument().positionToIndex(position); 47 | } 48 | 49 | // Covert a WOOT "index" into an ACE "position" 50 | function pos(idx) { 51 | return editor.getSession().getDocument().indexToPosition(idx); 52 | } 53 | 54 | // Convert a WOOT operation to an ACE delta object for WOOT index i: 55 | function asDelta(ch, isVisible, i) { 56 | return { 57 | action: isVisible ? "insertText" : "removeText", 58 | range: { 59 | start: pos(i), 60 | end: pos(i+1) 61 | }, 62 | text: ch 63 | }; 64 | } 65 | 66 | // 67 | // Call back for the WOOT client to trigger side-effects in the editor 68 | // I.e., add and remove characters 69 | // 70 | 71 | var updateEditor = function(ch, isVisible, visiblePos) { 72 | trace("Updating Editor", ch, isVisible, visiblePos); 73 | var delta = asDelta(ch, isVisible, visiblePos); 74 | offAir(function() { 75 | editor.getSession().getDocument().applyDeltas([delta]); 76 | }); 77 | } 78 | 79 | // Populate the editor with an existing document 80 | var setEditor = function(text) { 81 | offAir(function() { 82 | editor.getSession().getDocument().setValue(text); 83 | }); 84 | }; 85 | 86 | // 87 | // On page load, create a client and establish a web socket connection 88 | // 89 | // TODO: Make editor read-only until connected to server? 90 | 91 | var client; 92 | jQuery(document).ready(function() { 93 | client = new client.WootClient(setEditor, updateEditor); 94 | wsInit(); 95 | }); 96 | 97 | 98 | // 99 | // Functions to broadcast updates down the web socket 100 | // 101 | 102 | // `text` is of arbitrary size. For now we serialize as individual operations: 103 | var broadcast = function(op, text, range) { 104 | var base = idx(range.start), len = text.length; 105 | // When removing multiple character, the position for delete does not change: 106 | function charpos(p) { return op == "insertText" ? base+p : base; } 107 | 108 | for(var p=0; p 2 | 3 | 4 | 5 | Woot.js Demo 6 | 7 | 8 | 9 | 10 | 11 | 12 | 37 | 38 | 39 | 40 |
41 |
42 | 43 |
44 |
45 | 46 |
47 | 48 | -------------------------------------------------------------------------------- /server/src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | [%level] %logger{36} - %msg%n%ex{20} 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /server/src/main/resources/underscore-min.js: -------------------------------------------------------------------------------- 1 | // Underscore.js 1.5.2 2 | // http://underscorejs.org 3 | // (c) 2009-2013 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors 4 | // Underscore may be freely distributed under the MIT license. 5 | (function(){var n=this,t=n._,r={},e=Array.prototype,u=Object.prototype,i=Function.prototype,a=e.push,o=e.slice,c=e.concat,l=u.toString,f=u.hasOwnProperty,s=e.forEach,p=e.map,h=e.reduce,v=e.reduceRight,g=e.filter,d=e.every,m=e.some,y=e.indexOf,b=e.lastIndexOf,x=Array.isArray,w=Object.keys,_=i.bind,j=function(n){return n instanceof j?n:this instanceof j?(this._wrapped=n,void 0):new j(n)};"undefined"!=typeof exports?("undefined"!=typeof module&&module.exports&&(exports=module.exports=j),exports._=j):n._=j,j.VERSION="1.5.2";var A=j.each=j.forEach=function(n,t,e){if(null!=n)if(s&&n.forEach===s)n.forEach(t,e);else if(n.length===+n.length){for(var u=0,i=n.length;i>u;u++)if(t.call(e,n[u],u,n)===r)return}else for(var a=j.keys(n),u=0,i=a.length;i>u;u++)if(t.call(e,n[a[u]],a[u],n)===r)return};j.map=j.collect=function(n,t,r){var e=[];return null==n?e:p&&n.map===p?n.map(t,r):(A(n,function(n,u,i){e.push(t.call(r,n,u,i))}),e)};var E="Reduce of empty array with no initial value";j.reduce=j.foldl=j.inject=function(n,t,r,e){var u=arguments.length>2;if(null==n&&(n=[]),h&&n.reduce===h)return e&&(t=j.bind(t,e)),u?n.reduce(t,r):n.reduce(t);if(A(n,function(n,i,a){u?r=t.call(e,r,n,i,a):(r=n,u=!0)}),!u)throw new TypeError(E);return r},j.reduceRight=j.foldr=function(n,t,r,e){var u=arguments.length>2;if(null==n&&(n=[]),v&&n.reduceRight===v)return e&&(t=j.bind(t,e)),u?n.reduceRight(t,r):n.reduceRight(t);var i=n.length;if(i!==+i){var a=j.keys(n);i=a.length}if(A(n,function(o,c,l){c=a?a[--i]:--i,u?r=t.call(e,r,n[c],c,l):(r=n[c],u=!0)}),!u)throw new TypeError(E);return r},j.find=j.detect=function(n,t,r){var e;return O(n,function(n,u,i){return t.call(r,n,u,i)?(e=n,!0):void 0}),e},j.filter=j.select=function(n,t,r){var e=[];return null==n?e:g&&n.filter===g?n.filter(t,r):(A(n,function(n,u,i){t.call(r,n,u,i)&&e.push(n)}),e)},j.reject=function(n,t,r){return j.filter(n,function(n,e,u){return!t.call(r,n,e,u)},r)},j.every=j.all=function(n,t,e){t||(t=j.identity);var u=!0;return null==n?u:d&&n.every===d?n.every(t,e):(A(n,function(n,i,a){return(u=u&&t.call(e,n,i,a))?void 0:r}),!!u)};var O=j.some=j.any=function(n,t,e){t||(t=j.identity);var u=!1;return null==n?u:m&&n.some===m?n.some(t,e):(A(n,function(n,i,a){return u||(u=t.call(e,n,i,a))?r:void 0}),!!u)};j.contains=j.include=function(n,t){return null==n?!1:y&&n.indexOf===y?n.indexOf(t)!=-1:O(n,function(n){return n===t})},j.invoke=function(n,t){var r=o.call(arguments,2),e=j.isFunction(t);return j.map(n,function(n){return(e?t:n[t]).apply(n,r)})},j.pluck=function(n,t){return j.map(n,function(n){return n[t]})},j.where=function(n,t,r){return j.isEmpty(t)?r?void 0:[]:j[r?"find":"filter"](n,function(n){for(var r in t)if(t[r]!==n[r])return!1;return!0})},j.findWhere=function(n,t){return j.where(n,t,!0)},j.max=function(n,t,r){if(!t&&j.isArray(n)&&n[0]===+n[0]&&n.length<65535)return Math.max.apply(Math,n);if(!t&&j.isEmpty(n))return-1/0;var e={computed:-1/0,value:-1/0};return A(n,function(n,u,i){var a=t?t.call(r,n,u,i):n;a>e.computed&&(e={value:n,computed:a})}),e.value},j.min=function(n,t,r){if(!t&&j.isArray(n)&&n[0]===+n[0]&&n.length<65535)return Math.min.apply(Math,n);if(!t&&j.isEmpty(n))return 1/0;var e={computed:1/0,value:1/0};return A(n,function(n,u,i){var a=t?t.call(r,n,u,i):n;ae||r===void 0)return 1;if(e>r||e===void 0)return-1}return n.index-t.index}),"value")};var F=function(n){return function(t,r,e){var u={},i=null==r?j.identity:k(r);return A(t,function(r,a){var o=i.call(e,r,a,t);n(u,o,r)}),u}};j.groupBy=F(function(n,t,r){(j.has(n,t)?n[t]:n[t]=[]).push(r)}),j.indexBy=F(function(n,t,r){n[t]=r}),j.countBy=F(function(n,t){j.has(n,t)?n[t]++:n[t]=1}),j.sortedIndex=function(n,t,r,e){r=null==r?j.identity:k(r);for(var u=r.call(e,t),i=0,a=n.length;a>i;){var o=i+a>>>1;r.call(e,n[o])=0})})},j.difference=function(n){var t=c.apply(e,o.call(arguments,1));return j.filter(n,function(n){return!j.contains(t,n)})},j.zip=function(){for(var n=j.max(j.pluck(arguments,"length").concat(0)),t=new Array(n),r=0;n>r;r++)t[r]=j.pluck(arguments,""+r);return t},j.object=function(n,t){if(null==n)return{};for(var r={},e=0,u=n.length;u>e;e++)t?r[n[e]]=t[e]:r[n[e][0]]=n[e][1];return r},j.indexOf=function(n,t,r){if(null==n)return-1;var e=0,u=n.length;if(r){if("number"!=typeof r)return e=j.sortedIndex(n,t),n[e]===t?e:-1;e=0>r?Math.max(0,u+r):r}if(y&&n.indexOf===y)return n.indexOf(t,r);for(;u>e;e++)if(n[e]===t)return e;return-1},j.lastIndexOf=function(n,t,r){if(null==n)return-1;var e=null!=r;if(b&&n.lastIndexOf===b)return e?n.lastIndexOf(t,r):n.lastIndexOf(t);for(var u=e?r:n.length;u--;)if(n[u]===t)return u;return-1},j.range=function(n,t,r){arguments.length<=1&&(t=n||0,n=0),r=arguments[2]||1;for(var e=Math.max(Math.ceil((t-n)/r),0),u=0,i=new Array(e);e>u;)i[u++]=n,n+=r;return i};var R=function(){};j.bind=function(n,t){var r,e;if(_&&n.bind===_)return _.apply(n,o.call(arguments,1));if(!j.isFunction(n))throw new TypeError;return r=o.call(arguments,2),e=function(){if(!(this instanceof e))return n.apply(t,r.concat(o.call(arguments)));R.prototype=n.prototype;var u=new R;R.prototype=null;var i=n.apply(u,r.concat(o.call(arguments)));return Object(i)===i?i:u}},j.partial=function(n){var t=o.call(arguments,1);return function(){return n.apply(this,t.concat(o.call(arguments)))}},j.bindAll=function(n){var t=o.call(arguments,1);if(0===t.length)throw new Error("bindAll must be passed function names");return A(t,function(t){n[t]=j.bind(n[t],n)}),n},j.memoize=function(n,t){var r={};return t||(t=j.identity),function(){var e=t.apply(this,arguments);return j.has(r,e)?r[e]:r[e]=n.apply(this,arguments)}},j.delay=function(n,t){var r=o.call(arguments,2);return setTimeout(function(){return n.apply(null,r)},t)},j.defer=function(n){return j.delay.apply(j,[n,1].concat(o.call(arguments,1)))},j.throttle=function(n,t,r){var e,u,i,a=null,o=0;r||(r={});var c=function(){o=r.leading===!1?0:new Date,a=null,i=n.apply(e,u)};return function(){var l=new Date;o||r.leading!==!1||(o=l);var f=t-(l-o);return e=this,u=arguments,0>=f?(clearTimeout(a),a=null,o=l,i=n.apply(e,u)):a||r.trailing===!1||(a=setTimeout(c,f)),i}},j.debounce=function(n,t,r){var e,u,i,a,o;return function(){i=this,u=arguments,a=new Date;var c=function(){var l=new Date-a;t>l?e=setTimeout(c,t-l):(e=null,r||(o=n.apply(i,u)))},l=r&&!e;return e||(e=setTimeout(c,t)),l&&(o=n.apply(i,u)),o}},j.once=function(n){var t,r=!1;return function(){return r?t:(r=!0,t=n.apply(this,arguments),n=null,t)}},j.wrap=function(n,t){return function(){var r=[n];return a.apply(r,arguments),t.apply(this,r)}},j.compose=function(){var n=arguments;return function(){for(var t=arguments,r=n.length-1;r>=0;r--)t=[n[r].apply(this,t)];return t[0]}},j.after=function(n,t){return function(){return--n<1?t.apply(this,arguments):void 0}},j.keys=w||function(n){if(n!==Object(n))throw new TypeError("Invalid object");var t=[];for(var r in n)j.has(n,r)&&t.push(r);return t},j.values=function(n){for(var t=j.keys(n),r=t.length,e=new Array(r),u=0;r>u;u++)e[u]=n[t[u]];return e},j.pairs=function(n){for(var t=j.keys(n),r=t.length,e=new Array(r),u=0;r>u;u++)e[u]=[t[u],n[t[u]]];return e},j.invert=function(n){for(var t={},r=j.keys(n),e=0,u=r.length;u>e;e++)t[n[r[e]]]=r[e];return t},j.functions=j.methods=function(n){var t=[];for(var r in n)j.isFunction(n[r])&&t.push(r);return t.sort()},j.extend=function(n){return A(o.call(arguments,1),function(t){if(t)for(var r in t)n[r]=t[r]}),n},j.pick=function(n){var t={},r=c.apply(e,o.call(arguments,1));return A(r,function(r){r in n&&(t[r]=n[r])}),t},j.omit=function(n){var t={},r=c.apply(e,o.call(arguments,1));for(var u in n)j.contains(r,u)||(t[u]=n[u]);return t},j.defaults=function(n){return A(o.call(arguments,1),function(t){if(t)for(var r in t)n[r]===void 0&&(n[r]=t[r])}),n},j.clone=function(n){return j.isObject(n)?j.isArray(n)?n.slice():j.extend({},n):n},j.tap=function(n,t){return t(n),n};var S=function(n,t,r,e){if(n===t)return 0!==n||1/n==1/t;if(null==n||null==t)return n===t;n instanceof j&&(n=n._wrapped),t instanceof j&&(t=t._wrapped);var u=l.call(n);if(u!=l.call(t))return!1;switch(u){case"[object String]":return n==String(t);case"[object Number]":return n!=+n?t!=+t:0==n?1/n==1/t:n==+t;case"[object Date]":case"[object Boolean]":return+n==+t;case"[object RegExp]":return n.source==t.source&&n.global==t.global&&n.multiline==t.multiline&&n.ignoreCase==t.ignoreCase}if("object"!=typeof n||"object"!=typeof t)return!1;for(var i=r.length;i--;)if(r[i]==n)return e[i]==t;var a=n.constructor,o=t.constructor;if(a!==o&&!(j.isFunction(a)&&a instanceof a&&j.isFunction(o)&&o instanceof o))return!1;r.push(n),e.push(t);var c=0,f=!0;if("[object Array]"==u){if(c=n.length,f=c==t.length)for(;c--&&(f=S(n[c],t[c],r,e)););}else{for(var s in n)if(j.has(n,s)&&(c++,!(f=j.has(t,s)&&S(n[s],t[s],r,e))))break;if(f){for(s in t)if(j.has(t,s)&&!c--)break;f=!c}}return r.pop(),e.pop(),f};j.isEqual=function(n,t){return S(n,t,[],[])},j.isEmpty=function(n){if(null==n)return!0;if(j.isArray(n)||j.isString(n))return 0===n.length;for(var t in n)if(j.has(n,t))return!1;return!0},j.isElement=function(n){return!(!n||1!==n.nodeType)},j.isArray=x||function(n){return"[object Array]"==l.call(n)},j.isObject=function(n){return n===Object(n)},A(["Arguments","Function","String","Number","Date","RegExp"],function(n){j["is"+n]=function(t){return l.call(t)=="[object "+n+"]"}}),j.isArguments(arguments)||(j.isArguments=function(n){return!(!n||!j.has(n,"callee"))}),"function"!=typeof/./&&(j.isFunction=function(n){return"function"==typeof n}),j.isFinite=function(n){return isFinite(n)&&!isNaN(parseFloat(n))},j.isNaN=function(n){return j.isNumber(n)&&n!=+n},j.isBoolean=function(n){return n===!0||n===!1||"[object Boolean]"==l.call(n)},j.isNull=function(n){return null===n},j.isUndefined=function(n){return n===void 0},j.has=function(n,t){return f.call(n,t)},j.noConflict=function(){return n._=t,this},j.identity=function(n){return n},j.times=function(n,t,r){for(var e=Array(Math.max(0,n)),u=0;n>u;u++)e[u]=t.call(r,u);return e},j.random=function(n,t){return null==t&&(t=n,n=0),n+Math.floor(Math.random()*(t-n+1))};var I={escape:{"&":"&","<":"<",">":">",'"':""","'":"'"}};I.unescape=j.invert(I.escape);var T={escape:new RegExp("["+j.keys(I.escape).join("")+"]","g"),unescape:new RegExp("("+j.keys(I.unescape).join("|")+")","g")};j.each(["escape","unescape"],function(n){j[n]=function(t){return null==t?"":(""+t).replace(T[n],function(t){return I[n][t]})}}),j.result=function(n,t){if(null==n)return void 0;var r=n[t];return j.isFunction(r)?r.call(n):r},j.mixin=function(n){A(j.functions(n),function(t){var r=j[t]=n[t];j.prototype[t]=function(){var n=[this._wrapped];return a.apply(n,arguments),z.call(this,r.apply(j,n))}})};var N=0;j.uniqueId=function(n){var t=++N+"";return n?n+t:t},j.templateSettings={evaluate:/<%([\s\S]+?)%>/g,interpolate:/<%=([\s\S]+?)%>/g,escape:/<%-([\s\S]+?)%>/g};var q=/(.)^/,B={"'":"'","\\":"\\","\r":"r","\n":"n"," ":"t","\u2028":"u2028","\u2029":"u2029"},D=/\\|'|\r|\n|\t|\u2028|\u2029/g;j.template=function(n,t,r){var e;r=j.defaults({},r,j.templateSettings);var u=new RegExp([(r.escape||q).source,(r.interpolate||q).source,(r.evaluate||q).source].join("|")+"|$","g"),i=0,a="__p+='";n.replace(u,function(t,r,e,u,o){return a+=n.slice(i,o).replace(D,function(n){return"\\"+B[n]}),r&&(a+="'+\n((__t=("+r+"))==null?'':_.escape(__t))+\n'"),e&&(a+="'+\n((__t=("+e+"))==null?'':__t)+\n'"),u&&(a+="';\n"+u+"\n__p+='"),i=o+t.length,t}),a+="';\n",r.variable||(a="with(obj||{}){\n"+a+"}\n"),a="var __t,__p='',__j=Array.prototype.join,"+"print=function(){__p+=__j.call(arguments,'');};\n"+a+"return __p;\n";try{e=new Function(r.variable||"obj","_",a)}catch(o){throw o.source=a,o}if(t)return e(t,j);var c=function(n){return e.call(this,n,j)};return c.source="function("+(r.variable||"obj")+"){\n"+a+"}",c},j.chain=function(n){return j(n).chain()};var z=function(n){return this._chain?j(n).chain():n};j.mixin(j),A(["pop","push","reverse","shift","sort","splice","unshift"],function(n){var t=e[n];j.prototype[n]=function(){var r=this._wrapped;return t.apply(r,arguments),"shift"!=n&&"splice"!=n||0!==r.length||delete r[0],z.call(this,r)}}),A(["concat","join","slice"],function(n){var t=e[n];j.prototype[n]=function(){return z.call(this,t.apply(this._wrapped,arguments))}}),j.extend(j.prototype,{chain:function(){return this._chain=!0,this},value:function(){return this._wrapped}})}).call(this); 6 | //# sourceMappingURL=underscore-min.map -------------------------------------------------------------------------------- /server/src/main/resources/ws.js: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | This is pretty much the http://websockets.org/ example code 4 | with small changes. 5 | 6 | */ 7 | var wsUri = "ws://127.0.0.1:8080/woot/edit/default"; 8 | var statusIndicator, statusMsg; 9 | var websocket; 10 | 11 | function wsInit() 12 | { 13 | statusIndicator = document.getElementById("wsStatus"); 14 | statusMsg = document.getElementById("wsMsg"); 15 | status("#ff9000", "Connecting..."); 16 | websocket = new WebSocket(wsUri); 17 | testWebSocket(); 18 | } 19 | 20 | function testWebSocket() 21 | { 22 | websocket.onopen = function(evt) { onOpen(evt) }; 23 | websocket.onclose = function(evt) { onClose(evt) }; 24 | websocket.onmessage = function(evt) { onMessage(evt) }; 25 | websocket.onerror = function(evt) { onError(evt) }; 26 | } 27 | 28 | function onOpen(evt) { 29 | status("#00ff00", "Editing: default"); 30 | } 31 | 32 | function onClose(evt) { 33 | // console.log(evt); 34 | status("#ff0000", "Connection Closed"); 35 | } 36 | 37 | function onMessage(evt) 38 | { 39 | // console.log(evt, JSON.parse(evt.data)); 40 | client.ingest(evt.data); 41 | } 42 | 43 | function onError(evt) { 44 | status("#ff000", evt.data); 45 | } 46 | 47 | function doSend(message) { 48 | websocket.send(message); 49 | } 50 | 51 | function status(colour, msg) { 52 | statusIndicator.style.color = colour; 53 | statusMsg.innerHTML = msg; 54 | } -------------------------------------------------------------------------------- /server/src/main/scala/main.scala: -------------------------------------------------------------------------------- 1 | 2 | class WootServer(host: String, port: Int) { 3 | import java.net.InetSocketAddress 4 | import org.log4s.getLogger 5 | import org.http4s.server.blaze.BlazeBuilder 6 | import scala.concurrent.duration.Duration 7 | 8 | private val logger = getLogger 9 | logger.info(s"Starting Http4s-blaze WootServer on '$host:$port'") 10 | 11 | def run(): Unit = { 12 | BlazeBuilder. 13 | bindSocketAddress(new InetSocketAddress(host, port)). 14 | withWebSockets(true). 15 | withIdleTimeout(Duration.Inf). 16 | mountService(new StaticRoutes().service, "/"). 17 | mountService(new WootRoutes().service, "/woot/"). 18 | run. 19 | awaitShutdown() 20 | } 21 | } 22 | 23 | object Main extends App { 24 | import scala.util.Properties.envOrNone 25 | val ip = envOrNone("HOST") getOrElse("0.0.0.0") 26 | val port = envOrNone("PORT").map(_.toInt) getOrElse(8080) 27 | new WootServer(ip, port).run() 28 | } -------------------------------------------------------------------------------- /server/src/main/scala/static-routes.scala: -------------------------------------------------------------------------------- 1 | import org.http4s.{Request,StaticFile,Response} 2 | import org.http4s.dsl._ 3 | import org.http4s.server.HttpService 4 | 5 | import scalaz.concurrent.Task 6 | 7 | import org.log4s.getLogger 8 | 9 | class StaticRoutes { 10 | private val logger = getLogger 11 | 12 | implicit class ReqOps(req: Request) { 13 | def endsWith(exts: String*): Boolean = exts.exists(req.pathInfo endsWith _) 14 | 15 | def serve(path: String = req.pathInfo) = { 16 | logger.info(s"Resource: ${req.pathInfo} -> $path") 17 | StaticFile.fromResource(path, Some(req)) 18 | .map(Task.now) 19 | .getOrElse(NotFound()) 20 | } 21 | } 22 | 23 | val service = HttpService { 24 | case req if req.pathInfo == "/" => req.serve("/index.html") 25 | case req if req.endsWith(".html", ".js", ".map") => req.serve() 26 | } 27 | } -------------------------------------------------------------------------------- /server/src/main/scala/woot-routes.scala: -------------------------------------------------------------------------------- 1 | import woot._ 2 | 3 | import scalaz.stream.{Exchange, Process, time} 4 | import scalaz.stream.async.topic 5 | import scalaz.syntax.either._ 6 | import scalaz.{\/,-\/,\/-} 7 | import scalaz.concurrent.Task 8 | 9 | import org.http4s.server.websocket.WS 10 | import org.http4s.websocket.WebsocketBits._ 11 | 12 | import org.http4s.server.HttpService 13 | import org.http4s.dsl._ 14 | 15 | import org.log4s.getLogger 16 | import upickle._ 17 | 18 | class WootRoutes { 19 | private val logger = getLogger 20 | 21 | // Local view of the document, starting from blank. 22 | // In a real system, we'd load from a backing store based on the name or ID of the document 23 | var doc = new WString(SiteId("server"), ClockValue(0)) 24 | 25 | val updateDoc: Operation => Operation = op => { 26 | val (_, updated) = doc.integrate(op) 27 | doc = updated 28 | op 29 | } 30 | 31 | // The queue of WOOT operations: 32 | private val ops = topic[Operation]() 33 | 34 | // Encoding a decoding Web Socket data into Operations: 35 | val encodeOp: Operation => Text = 36 | op => Text(write(op)) 37 | 38 | val parse: String => Throwable \/ Operation = 39 | json => \/.fromTryCatchNonFatal { read[Operation](json) } 40 | 41 | // We can fail in at least two ways: 42 | // we're sent invalid JSON; or we're not sent text. 43 | val decodeFrame: WebSocketFrame => Throwable \/ Operation = 44 | _ match { 45 | case Text(json, _) => parse(json).map(updateDoc) 46 | case nonText => new IllegalArgumentException(s"Cannot handle: $nonText").left 47 | } 48 | 49 | val errorHandler: Throwable => Task[Unit] = 50 | err => { 51 | logger.warn(s"Failed to consume message: $err") 52 | Task.fail(err) 53 | } 54 | 55 | def safeConsume(consume: Operation => Task[Unit]): WebSocketFrame => Task[Unit] = 56 | ws => decodeFrame(ws).fold(errorHandler, consume) 57 | 58 | val service: HttpService = HttpService { 59 | 60 | // Joining an editing session, whicch sets up a web socket connection to the `ops` topic. 61 | case GET -> Root / "edit" / name => 62 | 63 | // Set up source for documents and operations to send to the client: 64 | val clientSite = SiteId.random 65 | logger.info(s"Subscribing $clientSite to document $name") 66 | 67 | val clientCopy = doc.copy(site=clientSite) 68 | val document = Text(write(clientCopy)) 69 | val src = Process.emit(document) ++ ops.subscribe.map(encodeOp) 70 | 71 | // Set up sink for operations sent from the client: 72 | val snk = ops.publish.map(safeConsume).onComplete(cleanup) 73 | 74 | WS(Exchange(src, snk)) 75 | } 76 | 77 | private[this] def cleanup() = { 78 | logger.info("Subscriber left") 79 | Process.halt 80 | } 81 | } -------------------------------------------------------------------------------- /wootModel/shared/src/main/scala/woot/Woot.scala: -------------------------------------------------------------------------------- 1 | package woot 2 | 3 | // # Each character has an `Id`. 4 | // An `Id` is usually made up of a `SiteId` and a `ClockValue`, but there are two special cases 5 | // called `Beginning` and `Ending`. This gives something for the first and last characters to point to. 6 | 7 | sealed trait Id { 8 | def <(that: Id): Boolean 9 | } 10 | 11 | object Beginning extends Id { 12 | def <(that: Id) = true 13 | } 14 | 15 | object Ending extends Id { 16 | def <(that: Id) = false 17 | } 18 | 19 | case class SiteId(value: String) extends AnyVal { 20 | def <(that: SiteId) = this.value < that.value 21 | } 22 | 23 | object SiteId { 24 | import util.Random 25 | // Some arbitrary session identifier for a site: 26 | def random: SiteId = 27 | SiteId(Random.alphanumeric.take(8).mkString) 28 | } 29 | 30 | case class ClockValue(value: Int) extends AnyVal { 31 | def incr: ClockValue = ClockValue(value + 1) 32 | def <(that: ClockValue) = this.value < that.value 33 | } 34 | 35 | 36 | case class CharId(ns: SiteId, ng: ClockValue) extends Id { 37 | // The `<` comparison is defined in _Definition 7_ (p. 8) of [RR5580] 38 | def <(that: Id) = that match { 39 | case CharId(site, clock) => (ns < site) || (ns == site && ng < clock) 40 | case Beginning => false 41 | case Ending => true 42 | } 43 | } 44 | 45 | // # Character 46 | case class WChar(id: CharId, alpha: Char, prev: Id, next: Id, isVisible: Boolean = true) 47 | 48 | // # Operations are inserts or deletes 49 | sealed trait Operation { 50 | def wchar: WChar 51 | def from: SiteId 52 | } 53 | 54 | case class InsertOp(override val wchar: WChar, override val from: SiteId) extends Operation 55 | case class DeleteOp(override val wchar: WChar, override val from: SiteId) extends Operation 56 | 57 | // Convenience for getting a empty document 58 | object WString { 59 | def empty(site: SiteId = SiteId.random) = 60 | WString(site, ClockValue(0)) 61 | } 62 | 63 | // # String representation 64 | // Note there there is no `WChar` representation of Beginning and Ending: they are not included in the vector. 65 | // The intention is for this data structure to be immutable: the `integrate` and `delete` operations produce new `WString` instances. 66 | case class WString( 67 | site: SiteId, 68 | nextTick: ClockValue, 69 | chars: Vector[WChar] = Vector.empty, 70 | queue: Vector[Operation] = Vector.empty) { 71 | 72 | lazy val visible = chars.filter(_.isVisible) 73 | 74 | // ## The visible text 75 | lazy val text: String = visible.map(_.alpha).mkString 76 | 77 | // When a character is added locally, the clock increments 78 | private def tick: WString = copy(nextTick = nextTick.incr) 79 | 80 | // The largest clock value for a site, when incremented, is the starting clock value for that site. 81 | def maxClockValue(site: SiteId): ClockValue = ClockValue( 82 | chars.filter(_.id.ns == site).map(_.id.ng.value).foldLeft(0) { 83 | math.max 84 | }) 85 | 86 | // ## Insert a `WChar` into the internal vector at position `pos`, returning a new `WString` 87 | // Position are indexed from zero 88 | // 89 | private[this] def ins(char: WChar, pos: Int): WString = { 90 | val (before, after) = chars splitAt pos 91 | copy(chars = (before :+ char) ++ after) 92 | } 93 | 94 | // ## Lookup the position in `chars` of a given `id` 95 | // 96 | // Note that the `id` is required to exist in the `WString`. 97 | private[this] def indexOf(id: Id): Int = { 98 | val p = id match { 99 | case Ending => chars.length 100 | case Beginning => 0 101 | case _ => chars.indexWhere(_.id == id) 102 | } 103 | require(p != -1) 104 | p 105 | } 106 | 107 | // # Lookup the visible position of the given `id`. 108 | // 109 | // Useful for updating an external editor, for example. 110 | // Id must exist in the document. 111 | def visibleIndexOf(id: Id): Int = { 112 | 113 | // To allow a deleted character to be located in an editor 114 | // (e.g., to side effect and update the editor) 115 | // we will locate where a deleted character was: 116 | lazy val indexWas = chars.take(indexOf(id)).filter(_.isVisible).length 117 | 118 | val p = id match { 119 | case Ending => visible.length 120 | case Beginning => 0 121 | case _ => indexWas 122 | } 123 | require(p != -1) 124 | p 125 | } 126 | 127 | // ## The parts of this `WString` between `prev` and `next` 128 | // ...but excluding the neighbours themselves as required (see [RR5580] p. 8) 129 | private[this] def subseq(prev: Id, next: Id): Vector[WChar] = { 130 | 131 | val from = prev match { 132 | case Beginning => 0 133 | case id => indexOf(id) + 1 134 | } 135 | 136 | val until = next match { 137 | case Ending => chars.length 138 | case id => indexOf(id) 139 | } 140 | 141 | chars.slice(from,until) 142 | } 143 | 144 | // # Applicability test 145 | private[this] def canIntegrate(op: Operation): Boolean = op match { 146 | case InsertOp(c,_) => canIntegrate(c.next) && canIntegrate(c.prev) 147 | case DeleteOp(c,_) => chars.exists(_.id == c.id) 148 | } 149 | 150 | private[this] def canIntegrate(id: Id): Boolean = 151 | id == Ending || id == Beginning || chars.exists(_.id == id) 152 | 153 | // # Waiting integrations 154 | // If we cannot currently integrate a character, it goes into a queue. 155 | private[this] def enqueue(op: Operation): WString = copy(queue = queue :+ op) 156 | 157 | @scala.annotation.tailrec 158 | private def dequeue(ops: Vector[Operation] = Vector.empty): (Vector[Operation], WString) = { 159 | 160 | def without(op: Operation): Vector[Operation] = queue.filterNot(_ == op) 161 | 162 | queue.find(canIntegrate) match { 163 | case None => (ops, this) 164 | case Some(op @ InsertOp(c,_)) => 165 | // Remove the operation from the queue, and integrate: 166 | val doc = copy(queue = without(op)).integrate(c, c.prev, c.next) 167 | doc.dequeue(op +: ops) 168 | case Some(op @ DeleteOp(c,_)) => 169 | copy(queue = without(op)).hide(c).dequeue(op +: ops) 170 | } 171 | } 172 | 173 | // # Delete means making the character invisible. 174 | private def hide(c: WChar): WString = { 175 | val p = chars.indexWhere(_.id == c.id) 176 | require(p != -1) 177 | val replacement = c.copy(isVisible=false) 178 | val (before, rest) = chars splitAt p 179 | copy(chars = (before :+ replacement) ++ (rest drop 1) ) 180 | } 181 | 182 | 183 | // # Integrate a remotely received insert or delete. 184 | // The result is a new `WString` plus a list of the operations that were applied. 185 | // The operations can be: 186 | // - empty, if the operation was queued 187 | // - one operation, the typical expected case 188 | // - more than one operation, if the integration unblocked items on the queue. 189 | def integrate(op: Operation): (Vector[Operation], WString) = op match { 190 | // - Don't insert the same ID twice: 191 | case InsertOp(c,_) if chars.exists(_.id == c.id) => (Vector.empty, this) 192 | 193 | // - Insert can go ahead if the next & prev exist: 194 | case InsertOp(c,_) if canIntegrate(op) => 195 | val (ops, doc) = integrate(c, c.prev, c.next).dequeue() 196 | (op +: ops, doc) 197 | 198 | // - We can delete any char that exists: 199 | case DeleteOp(c,_) if canIntegrate(op) => (Vector(op), hide(c)) 200 | 201 | // - Anything else goes onto the queue for another time: 202 | case _ => (Vector.empty, enqueue(op)) 203 | } 204 | 205 | @scala.annotation.tailrec 206 | private def integrate(c: WChar, before: Id, after: Id): WString = { 207 | 208 | // Looking at all the characters between the previous and next positions: 209 | subseq(before, after) match { 210 | // - when where's no option about where to insert, perform the insert 211 | case Vector() => 212 | ins(c, indexOf(after)) 213 | 214 | // - when there's a choice, locate an insert point based on `Id.<` 215 | case search: Vector[WChar] => 216 | val L: Vector[Id] = before +: trim(search).map(_.id) :+ after 217 | val i = math.max(1, math.min(L.length-1, L.takeWhile( _ < c.id ).length)) 218 | integrate(c, L(i-1), L(i)) 219 | } 220 | } 221 | 222 | // Don't consider characters that have a `prev` or `next` in the set of 223 | // locations to consider (i.e., ones that are between the insert points of interest) 224 | // See last paragraph of first column of page 5 of CSCW06 (relating to figure 4b) 225 | private[this] def trim(cs: Vector[WChar]): Vector[WChar] = 226 | for { 227 | c <- cs 228 | if cs.forall(x => x.id != c.next && x.id != c.prev) 229 | } yield c 230 | 231 | // # A local insert of a character, producing an operation to send around the network. 232 | def insert(ch: Char, visiblePos: Int): (InsertOp, WString) = { 233 | 234 | require(visiblePos > -1) 235 | 236 | val prev: Id = 237 | if (visiblePos == 0) Beginning 238 | else visible(visiblePos-1).id 239 | 240 | val next: Id = 241 | if (visiblePos >= visible.length) Ending 242 | else visible(visiblePos).id // Not +1 because we are inserting just before this char 243 | 244 | val wchar = WChar(CharId(site, nextTick), ch, prev, next) 245 | val op = InsertOp(wchar, site) 246 | val (_, wstring) = integrate(op) 247 | 248 | (op, wstring.tick) 249 | } 250 | 251 | 252 | // Convenience for setting up a model from some arbitrary text. 253 | // Useful for garbage collection and testing 254 | def insert(text: String): (Vector[Operation], WString) = { 255 | val zero = (Vector.empty, this) 256 | 257 | val insertFrom = this.visible.length 258 | 259 | (text.zipWithIndex).foldLeft[(Vector[Operation],WString)](zero) { 260 | case ( (ops, wstring), (ch, pos) ) => 261 | val (op, updated) = wstring.insert(ch, insertFrom+pos) 262 | (op +: ops, updated) 263 | } 264 | } 265 | 266 | // # Remove a local character, giving the operation and updated `WString`. 267 | def delete(visiblePos: Int): (DeleteOp, WString) = { 268 | 269 | require(visiblePos > -1) 270 | require(visiblePos < visible.length) 271 | 272 | val wchar = visible(visiblePos) 273 | val op = DeleteOp(wchar, site) 274 | val (_, wstring) = integrate(op) 275 | 276 | (op, wstring) 277 | } 278 | } -------------------------------------------------------------------------------- /wootModel/shared/src/test/scala/woot/WootSpec.scala: -------------------------------------------------------------------------------- 1 | package woot 2 | 3 | import util.Random 4 | 5 | import org.scalacheck.{Properties, Gen} 6 | import org.scalacheck.Prop.{forAll, BooleanOperators} 7 | import org.scalacheck.Arbitrary, Arbitrary.arbitrary 8 | 9 | object WootModelSpec extends Properties("WOOT Model") with WootOperationHelpers { 10 | 11 | import NonEmptyStringGenerator._ 12 | import WootGenerator._ 13 | 14 | property("local insert preserves original text") = forAll { (text: String) => 15 | val (_, wstring) = WString.empty().insert(text) 16 | wstring.text == text 17 | } 18 | 19 | property("insert order is irrelevant") = forAll { (text: String, siteId: String, clockStart: Int) => 20 | // Generate the operations on a local site: 21 | val (ops, _) = WString.empty().insert(text) 22 | 23 | // Shuffle the operations and apply them on a new site: 24 | val integrated = applyOps(Random shuffle ops) 25 | 26 | // The text on the new site will be the same as the original: 27 | integrated.text == text 28 | } 29 | 30 | property("concurrent inserting produces consistent text") = 31 | forAll { 32 | (starting: NonEmptyString, text1: NonEmptyString, text2: NonEmptyString) => 33 | val randomPos = Gen.choose(0, starting.length) 34 | forAll(randomPos, randomPos) { (insert1: Int, insert2: Int) => 35 | // Two sites starting with the same text: 36 | val (ops, site1) = WString.empty().insert(starting) 37 | val site2 = applyOps(ops) 38 | 39 | // Each site then inserts different text at a random point 40 | val (site1Ops, site1Updated) = applyWoot(site1, text1, insert1) 41 | val (site2Ops, site2Updated) = applyWoot(site2, text2, insert2) 42 | 43 | // The sites exchange updates: 44 | val site1Final = applyOps(site2Ops, site1Updated) 45 | val site2Final = applyOps(site1Ops, site2Updated) 46 | 47 | // Both sites should have the same text: 48 | site1Final.text == site2Final.text 49 | } 50 | } 51 | 52 | property("insert is idempotent") = forAll { text: NonEmptyString => 53 | val (ops, doc) = WString.empty().insert(text) 54 | forAll(Gen.oneOf(ops)) { op: Operation => 55 | val (ops, updated) = doc.integrate(op) 56 | (updated eq doc) && ops.length == 0 57 | } 58 | } 59 | 60 | property("local delete removes a character") = forAll { text: NonEmptyString => 61 | forAll(text.chooseIndex) { index: Int => 62 | val (_, doc) = WString.empty().insert(text) 63 | val (_, updated) = doc.delete(index) 64 | val full: String = text // coerce to regular string to access take and drop 65 | val expected = full.take(index) + full.drop(index+1) 66 | (updated.text == expected) :| s"${updated.text} not $expected" 67 | } 68 | } 69 | 70 | property("remote delete produces consistent results") = forAll { text: NonEmptyString => 71 | forAll(text.chooseIndex) { index: Int => 72 | // Construct a document from the text and randomly delete a character: 73 | val (ops, doc) = WString.empty().insert(text) 74 | val (op, updated) = doc.delete(index) 75 | // Apply the operations to a new site (order is irrelevant) 76 | val site2 = applyOps(Random shuffle (ops :+ op)) 77 | // The resultant text should be consistent: 78 | updated.text == site2.text 79 | } 80 | } 81 | 82 | property("delete operations should be emitted after corresponding insert") = { 83 | /* 84 | Imagine three sites, where there is a lag between site 1 and 3. 85 | Site 1 will insert a character, and site 2 will delete it. 86 | It takes 1 second for an insert from Site 1 to reach Site 2, but 5 seconds to reach site 3. 87 | Site 2 immediately deletes the character and broadcasts it. 88 | The delete will reach site 3 before the insert: 89 | 90 | (1 sec) 91 | +--------+ INS A +--------+ 92 | | Site 1 |--------+------->| Site 2 | 93 | +--------+ | +--------+ 94 | ^ | | 95 | | | | 96 | +------------+---- DEL A --+ (1 sec) 97 | | | 98 | | | 99 | | v 100 | | +--------+ 101 | +------->| Site 3 | 102 | +--------+ 103 | (5 secs) 104 | 105 | 106 | Site 3 will queue the delete until it receives the insert. 107 | When applying the insert, the delete will be dequeued and applied. 108 | 109 | To ensure the side-effects are applied in the correct order, 110 | the result of integrate at site 3 must be `Vector(insert,delete)`. 111 | 112 | Alternatively: we could optimize the implementation and turn a 113 | delete-before-an-insert into an automatic insert of an invisible 114 | character, with resultant operations of `Vector.empty`. 115 | */ 116 | val wstring = WString.empty() 117 | 118 | forAll(wcharGen(wstring)) { wchar: WChar => 119 | val insert = InsertOp(wchar, wstring.site) 120 | val delete = DeleteOp(wchar, wstring.site) 121 | 122 | val (_, doc) = wstring.integrate(delete) 123 | val (ops, _) = doc.integrate(insert) 124 | 125 | ops == Vector(insert, delete) 126 | } 127 | } 128 | 129 | 130 | property("site max clock value is the largest operation from that site") = 131 | forAll { text: NonEmptyString => 132 | val originalDoc = WString.empty() 133 | val site = originalDoc.site 134 | val (ops, finalDoc) = originalDoc.insert(text) 135 | // As `nextTick` is an unused value, the maximum value will the 136 | // the starting point (less 1) plus the number of operations applied 137 | finalDoc.maxClockValue(site).value == (originalDoc.nextTick.value - 1 + ops.length) 138 | } 139 | 140 | property("can locate visible index of a character after deletion") = forAll { text: NonEmptyString => 141 | forAll(text.chooseIndex) { index: Int => 142 | // It is convenient to apply an operation to WOOT, such as delete, but 143 | // then be able to tell what the visible index was of the character 144 | // just removed. Without this you need to lookup the character location 145 | // before you delete it (which is OK, but not always convenient) 146 | val (ops, doc) = WString.empty().insert(text) 147 | val (op, updated) = doc.delete(index) 148 | updated.visibleIndexOf(op.wchar.id) == index 149 | } 150 | } 151 | } 152 | 153 | // Functions to make it easier to work with WOOT and Operations in tests 154 | trait WootOperationHelpers { 155 | // Apply a set of operations to a WString 156 | def applyOps(ops: Vector[Operation], startingDoc: WString = WString.empty()): WString = { 157 | ops.foldLeft(startingDoc) { (wstring, op) => 158 | val (ops, doc) = wstring integrate op 159 | doc 160 | } 161 | } 162 | 163 | // Turn text into a sequence of operations on a new WString 164 | // A variation on WString.insert where we insert at the same position in an existing WString 165 | def applyWoot(w: WString, text: String, pos: Int): (Vector[Operation], WString) = { 166 | text.foldLeft((Vector.empty[Operation], w)) { 167 | case ((ops, wstring), ch) => 168 | val (op, updated) = wstring.insert(ch, pos) 169 | (op +: ops, updated) 170 | } 171 | } 172 | } 173 | 174 | object WootGenerator { 175 | 176 | // Implementation design problem: doc.nextTick implies returning a modified doc 177 | def wcharGen(doc: WString): Gen[WChar] = 178 | for { 179 | alpha <- arbitrary[Char] 180 | visible <- arbitrary[Boolean] 181 | id = CharId(doc.site, doc.nextTick) 182 | pos <- Gen.choose(0, doc.chars.length) 183 | prev = if (pos == 0) Beginning else doc.chars(pos-1).id 184 | next = if (pos >= doc.chars.length) Ending else doc.chars(pos).id 185 | } yield WChar(id, alpha, prev, next, visible) 186 | 187 | } 188 | 189 | object NonEmptyStringGenerator { 190 | 191 | case class NonEmptyString(text: String) extends AnyVal { 192 | def chooseIndex = Gen.choose(0, text.length-1) 193 | } 194 | 195 | // To allow us to treat the NonEmptyString as a String 196 | implicit def nes2s(nes: NonEmptyString): String = nes.text 197 | 198 | lazy val nonEmptyStringGen: Gen[NonEmptyString] = 199 | Gen.nonEmptyListOf(Gen.alphaChar).map(_.mkString).map(NonEmptyString.apply) 200 | 201 | implicit lazy val arbitraryNonEmptyString: Arbitrary[NonEmptyString] = Arbitrary(nonEmptyStringGen) 202 | } --------------------------------------------------------------------------------