Repository: atom-archive/xray Branch: master Commit: cb6c5809f18c Files: 141 Total size: 1.1 MB Directory structure: gitextract_hfrilqj1/ ├── .gitignore ├── .travis.yml ├── CONTRIBUTING.md ├── Cargo.toml ├── LICENSE ├── README.md ├── docs/ │ ├── architecture/ │ │ ├── 001_client_server_protocol.md │ │ ├── 002_shared_workspaces.md │ │ └── 003_memo_epochs.md │ └── updates/ │ ├── 2018_03_05.md │ ├── 2018_03_12.md │ ├── 2018_03_19.md │ ├── 2018_03_26.md │ ├── 2018_04_02.md │ ├── 2018_04_09.md │ ├── 2018_04_16.md │ ├── 2018_04_23.md │ ├── 2018_04_30.md │ ├── 2018_05_07.md │ ├── 2018_05_14.md │ ├── 2018_05_28.md │ ├── 2018_07_10.md │ ├── 2018_07_16.md │ ├── 2018_07_23.md │ ├── 2018_07_31.md │ ├── 2018_08_21.md │ ├── 2018_08_28.md │ ├── 2018_09_14.md │ └── 2018_10_02.md ├── memo_core/ │ ├── Cargo.toml │ ├── README.md │ ├── rustfmt.toml │ ├── script/ │ │ └── compile_flatbuffers │ └── src/ │ ├── btree.rs │ ├── buffer.rs │ ├── epoch.rs │ ├── lib.rs │ ├── operation_queue.rs │ ├── serialization/ │ │ ├── mod.rs │ │ ├── schema.fbs │ │ └── schema_generated.rs │ ├── time.rs │ └── work_tree.rs ├── memo_js/ │ ├── .npmignore │ ├── .nvmrc │ ├── Cargo.toml │ ├── README.md │ ├── package.json │ ├── rustfmt.toml │ ├── script/ │ │ └── build │ ├── src/ │ │ ├── index.ts │ │ ├── lib.rs │ │ └── support.ts │ ├── test/ │ │ ├── tests.ts │ │ └── tsconfig.json │ ├── tsconfig.json │ └── webpack.config.js ├── rust-toolchain ├── script/ │ ├── bench │ ├── build │ ├── cibuild │ └── test ├── xray_browser/ │ ├── README.md │ ├── package.json │ ├── script/ │ │ ├── build │ │ └── server │ ├── src/ │ │ ├── client.js │ │ ├── ui.js │ │ └── worker.js │ └── static/ │ └── index.html ├── xray_cli/ │ ├── Cargo.toml │ ├── README.md │ └── src/ │ └── main.rs ├── xray_core/ │ ├── Cargo.toml │ ├── README.md │ ├── benches/ │ │ └── bench.rs │ └── src/ │ ├── app.rs │ ├── buffer.rs │ ├── buffer_view.rs │ ├── cross_platform.rs │ ├── file_finder.rs │ ├── fs.rs │ ├── fuzzy.rs │ ├── lib.rs │ ├── movement.rs │ ├── never.rs │ ├── notify_cell.rs │ ├── project.rs │ ├── rpc/ │ │ ├── client.rs │ │ ├── messages.rs │ │ ├── mod.rs │ │ └── server.rs │ ├── stream_ext.rs │ ├── tree.rs │ ├── wasm_logging.rs │ ├── window.rs │ └── workspace.rs ├── xray_electron/ │ ├── .gitignore │ ├── README.md │ ├── index.html │ ├── lib/ │ │ ├── main_process/ │ │ │ └── main.js │ │ ├── render_process/ │ │ │ └── main.js │ │ └── shared/ │ │ └── xray_client.js │ └── package.json ├── xray_server/ │ ├── Cargo.toml │ ├── README.md │ └── src/ │ ├── fs.rs │ ├── json_lines_codec.rs │ ├── main.rs │ ├── messages.rs │ └── server.rs ├── xray_ui/ │ ├── README.md │ ├── lib/ │ │ ├── action_dispatcher.js │ │ ├── app.js │ │ ├── debounce.js │ │ ├── file_finder.js │ │ ├── index.js │ │ ├── modal.js │ │ ├── text_editor/ │ │ │ ├── shaders.js │ │ │ ├── text_editor.js │ │ │ └── text_plane.js │ │ ├── theme_provider.js │ │ ├── view.js │ │ ├── view_registry.js │ │ └── workspace.js │ ├── package.json │ └── test/ │ ├── action_dispatcher.test.js │ ├── file_finder.test.js │ ├── helpers/ │ │ └── component_helpers.js │ ├── modal.test.js │ ├── view.test.js │ └── view_registry.test.js └── xray_wasm/ ├── .gitignore ├── Cargo.toml ├── lib/ │ ├── main.js │ └── support.js ├── package.json ├── script/ │ ├── build │ └── test ├── src/ │ └── lib.rs └── test/ └── tests.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ **/node_modules **/target/ **/*.rs.bk **/.DS_Store **/.cargo Icon* .tags* xray_wasm/dist xray_browser/dist memo_js/dist memo_js/test/dist ================================================ FILE: .travis.yml ================================================ language: rust before_install: - curl -o- -L https://yarnpkg.com/install.sh | bash - export PATH="$HOME/.yarn/bin:$PATH" - curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.33.11/install.sh | bash - nvm install v11 # Create a virtual display for electron - export DISPLAY=':99.0' - Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 & script: script/cibuild cache: cargo: true yarn: true branches: only: - master notifications: email: on_success: never on_failure: change ================================================ FILE: CONTRIBUTING.md ================================================ # Contributing to Xray This project is still in the very early days, and isn't going to be usable for even basic editing for some time. At this point, we're looking for contributors that are willing to roll up their sleeves and solve problems. Please communicate with us however it makes sense, but in general opening a *pull request that fixes an issue* is going to be far more valuable than just reporting an issue. As the architecture stabilizes and the surface area of the project expands, there will be increasing opportunities to help out. To get some ideas for specific projects that could help in the short term, check out [issues that are labeled "help wanted"](https://github.com/atom/xray/issues?q=is%3Aopen+is%3Aissue+label%3A%22help+wanted%22). If you have an idea you'd like to pursue outside of these, that's awesome, but you may want to discuss it with us in an issue first to ensure it fits before spending too much time on it. It's really important to us to have a smooth on-ramp for contributors, and one great way you can contribute is by helping us improve this guide. If your experience is bumpy, can you open a pull request that makes it smoother for the next person? ## Communicating with maintainers The best way to communicate with maintainers is by posting a issue to this repository. The more thought you put into articulating your question or idea, the more value you'll be adding to the community and the easier it will be for maintainers to respond. That said, just try your best. If you have something you want to say, we'd prefer that you say it imperfectly rather than not saying it at all. You can also communicate with maintainers or other community members on the `#xray` channel on Atom's public slack instance. After you [request an invite via this form](http://atom-slack.herokuapp.com/), you can access our Slack instance at https://atomio.slack.com. ## Building So far, we have only built this project on macOS. If you'd like to help us improve our build or documentation to support other platforms, that would be a huge help! ### Install system dependencies #### Install Node v8.9.3 To install Node, you can install [`nvm`](https://github.com/creationix/nvm) and then run `nvm install v8.9.3`. Later versions may work, but you should ideally run the build with the same version of Node that is bundled into Xray's current Electron dependency. If in doubt, you can check the version of the `electron` dependency in [`xray_electron/package.json`](https://github.com/atom/xray/blob/master/xray_electron/package.json), then run `process.versions.node` in the console of that version of Electron to ensure that these instructions haven't gotten out of date. #### Install Rust You can install Rust via [`rustup`](https://www.rustup.rs/). We currently require building on the nightly channel in order to use `wasm_bindgen` for browser support. #### Install Yarn Follow the [installation instructions](https://yarnpkg.com/en/docs/install) on the Yarn site. ### Run the build script This repository contains several components in top-level folders prefixed with `xray_*`. To build all of the components, simply run this in the root of the repository: ```sh script/build ``` To build a release version (which will be much faster): ```sh script/build --release ``` ## Running We currently *only* support launching the application via the CLI. For this to work, you need to set the `XRAY_SRC_PATH` environment variable to the location of your repository. The CLI also currently *requires* an argument: ```sh XRAY_SRC_PATH=. script/xray . ``` That assumes you built with `--release`. To run the debug version, use `xray_debug` instead: ```sh XRAY_SRC_PATH=. script/xray_debug . ``` Once a blank window has opened, press cmd-t to open the file selection menu. Search for a file, and press enter to open it. The contents of the file should appear in the window. If something does not go as expected, check the dev tools (cmd-shift-i) for errors. ## Running tests and benchmarks * All tests: `script/test` * Rust tests: `cargo test` in the root of the repository or a Rust subfolder. * Front-end tests: `cd xray_ui && yarn test` * Benchmarks: `cargo bench` ================================================ FILE: Cargo.toml ================================================ [workspace] members = [ "memo_core", "memo_js", "xray_core", "xray_server", "xray_cli", "xray_wasm", ] ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2018 GitHub Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ **Attention:** GitHub has decided not to move forward with any aspect of this project. We'll archive the repository in case anybody finds value here, but we don't expect to actively work on this in the foreseeable future. Thanks to everyone for their interest and support. # Xray [![Build Status](https://travis-ci.org/atom/xray.svg?branch=master)](https://travis-ci.org/atom/xray) Xray is an experimental Electron-based text editor informed by what we've learned in the four years since the launch of Atom. In the short term, this project is a testbed for rapidly iterating on several radical ideas without risking the stability of Atom. The longer term future of the code in this repository will become clearer after a few months of progress. For now, our primary goal is to iterate rapidly and learn as much as possible. ## Q3 2018 Focus We're currently focused on a sub-project of Xray called [Memo](./memo_core), which will serve as the foundation of Xray but also be available as a standalone tool. Memo is an operation-based version control system that tracks changes at the level of individual keystrokes and synchronizes branches in real time. ## Updates * [October 2, 2018](./docs/updates/2018_10_02.md) * [September 14, 2018](./docs/updates/2018_09_14.md) * [August 28, 2018](./docs/updates/2018_08_28.md) * [August 21, 2018](./docs/updates/2018_08_21.md) * [July 31, 2018](./docs/updates/2018_07_31.md) * [July 23, 2018](./docs/updates/2018_07_23.md) * [July 16, 2018](./docs/updates/2018_07_16.md) * [July 10, 2018](./docs/updates/2018_07_10.md) * [Archives](./docs/updates/) ## Foundational priorities Our goal is to build a cross-platform text editor that is designed from the beginning around the following foundational priorities: ### Collaboration *Xray makes it as easy to code together as it is to code alone.* We design features for collaborative use from the beginning. Editors and other relevant UI elements are designed to be occupied by multiple users. Interactions with the file system and other resources such as subprocesses are abstracted to work over network connections. ### High performance *Xray feels lightweight and responsive.* We design our features to be responsive from the beginning. We reliably provide visual feedback within the latency windows suggested by the [RAIL performance model](https://developers.google.com/web/fundamentals/performance/rail). For all interactions, we shoot for the following targets on the hardware of our median user: | Duration | Action | | - | - | | 8ms | Scrolling, animations, and fine-grained interactions such as typing or cursor movement. | | 50ms | Coarse-grained interactions such as opening a file or initiating a search. If we can't complete the action within this window, we should show a progress bar. | | 150ms | Opening an application window. | We are careful to maximize throughput of batch operations such as project-wide search. Memory consumption is kept within a low constant factor of the size of the project and open buffer set, but we trade memory for speed and extensibility so long as memory requirements are reasonable. ### Extensibility *Xray gives developers control over their own tools.* We expose convenient and powerful APIs to enable users to add non-trivial functionality to the application. We balance the power of our APIs with the ability to ensure the responsiveness, stability, and security of the application as a whole. We avoid leaking implementation details and use versioning where possible to enable a sustained rapid development without destabilizing the package ecosystem. ### Web compatibility *Editing on GitHub feels like editing in Xray.* We want to provide a full-featured editor experience that can be used from within a browser. This will ultimately help us provide a more unified experience between GitHub.com and Xray and give us a stronger base of stakeholders in the core editing technology. ## Architecture Martin Fowler defines software architecture as those decisions which are both important and hard to change. Since these decisions are hard to change, we need to be sure that our foundational priorities are well-served by these decisions. ![Architecture](docs/images/architecture.png) ### The UI is built with web technology Web tech adds a lot of overhead, which detracts from our top priority of high-performance. However, web standards are also the best approach that we know of to deliver a cross-platform, extensible user interface. Atom proved that developers want to add non-trivial UI elements to their editor, and we still see web technologies as the most viable way to offer them that ability. The fundamental question is whether we can gain the web's benefits for extensibility while still meeting our desired performance goals. Our hypothesis is that it's possible–with the right architecture. ### Core application logic is written in Rust While the UI will be web-based, the core of the application is implemented in a server process written in Rust. We place as much logic as possible in a library crate located in `/xray_core`, then expose this logic as a server when running Xray on the desktop (`/xray_server`) and a web-assembly library running on a worker thread when running Xray in the browser (`/xray_wasm`). We communicate between the UI and the back end process via JSON RPC. All of the core application code other than the view logic should be written in Rust. This will ensure that it has a minimal footprint to load and execute, and Rust's robust type system will help us maintain it more efficiently than dynamically typed code. A language that is fundamentally designed for multi-threading will also make it easier to exploit parallelism whenever the need arises, whereas JavaScript's single-threaded nature makes parallelism awkward and challenging. Fundamentally, we want to spend our time writing in a language that is fast by default. It's true that it's possible to write slow Rust, and also possible to write fast JavaScript. It's *also* true that it's much harder to write slow Rust than it is to write slow JavaScript. By spending fewer resources on the implementation of the platform itself, we'll make more resources available to run package code. ### I/O will be centralized in the server The server will serialize buffer loads and saves on a per-path basis, and maintains a persistent database of CRDT operations for each file. As edits are performed in windows, they will be streamed to the host process to be stored and echoed out to any other windows with the same open buffer. This will enable unsaved changes to always be incrementally preserved in case of a crash or power failure and preserves the history associated with a file indefinitely. Early on, we should design the application process to be capable of connecting to multiple workspace servers to facilitate real-time collaboration or editing files on a remote server by running a headless host process. To support these use cases, all code paths that touch the file system or spawn subprocesses will occur in the server process. The UI will not make use of the I/O facilities provided by Electron, and instead interact with the server via RPC. ### Packages will run in a JavaScript VM in the server process A misbehaving package should not be able to impact the responsiveness of the application. The best way to guarantee this while preserving ease of development is to activate packages on their own threads. We can run a worker thread per package or run packages in their own contexts across a pool of threads. Packages *can* run code on the render thread by specifying versioned components in their `package.json`. ```json "components": { "TodoList": "./components/todo-list.js" } ``` If a package called `my-todos` had the above entry in its `package.json`, it could request that the workspace attach that component by referring to `myTodos.TodoList` when adding an item. During package installation on the desktop, we can automatically update the V8 snapshot of the UI to include the components of every installed package. Components will only be dynamically loaded from the provided paths in development mode. Custom views will only have access to the DOM and an asynchronous channel to communicate with the package's back end running on the server. APIs for interacting with the core application state and the underlying operating system will only be available within the server process, discouraging package authors from putting too much logic into their views. We'll use a combination of asynchronous channels and CRDTs to present convenient APIs to package authors within worker threads. ### Text is stored in a copy-on-write CRDT To fully exploit Rust's unique advantage of parallelism, we need to store text in a concurrency-friendly way. We use a variant of RGA called RGASplit, which is described in [this research paper](https://pages.lip6.fr/Marc.Shapiro/papers/rgasplit-group2016-11.pdf). ![CRDT diagram](docs/images/crdt.png) In RGA split, the document is stored as a sequence of insertion fragments. In the example above, the document starts as just a single insertion containing `hello world`. We then introduce `, there` and `!` as additional insertions, splitting the original insertion into two fragments. To delete the `ld` at the end of `world` in the third step, we create another fragment containing just the `ld` and mark it as deleted with a tombstone. Structuring the document in this way has a number of advantages. * Real-time collaboration works out of the box * Concurrent edits: Any thread can read or write its own replica of the document without diverging in the presence of concurrent edits. * Integrated non-linear history: To undo any group of operations, we increment an undo counter associated with any insertions and deletions that controls their visibility. This means we only need to store operation ids in the history rather than operations themselves, and we can undo any operation at any time rather than adhering to historical order. * Stable logical positions: Instead of tracking the location of markers on every edit, we can refer to stable positions that are guaranteed to be valid for any future buffer state. For example, we can mark the positions of all search results in a background thread and continue to interpret them in a foreground thread if edits are performed in the meantime. Our use of a CRDT is similar to the Xi editor, but the approach we're exploring is somewhat different. Our current understanding is that in Xi, the buffer is stored in a rope data structure, then a secondary layer is used to incorporate edits. In Xray, the fundamental storage structure of all text is itself a CRDT. It's similar to Xi's rope in that it uses a copy-on-write B-tree to index all inserted fragments, but it does not require any secondary system for incorporating edits. ### Derived state will be computed asynchronously We should avoid implementing synchronous APIs that depend on open-ended computations of derived state. For example, when soft wrapping is enabled in Atom, we synchronously update a display index that maps display coordinates to buffer coordinates, which can block the UI. In Xray, we want to avoid making these kinds of promises in our API. For example, we will allow the display index to be accessed synchronously after a buffer edit, but only provide an interpolated version of its state that can be produced in logarithmic time. This means it will be spatially consistent with the underlying buffer, but may contain lines that have not yet been soft-wrapped. We can expose an asynchronous API that allows a package author to wait until the display layer is up to date with a specific version of the buffer. In the user interface, we can display a progress bar for any derived state updates that exceed 50ms, which may occur when the user pastes multiple megabytes of text into the editor. ### React will be used for presentation By using React, we completely eliminate the view framework as a concern that we need to deal with and give package authors access to a tool they're likely to be familiar with. We also raise the level of abstraction above basic DOM APIs. The risk of using React is of course that it is not standardized and could have breaking API changes. To mitigate this risk, we will require packages to declare which version of React they depend on. We will attempt using this version information to provide shims to older versions of React when we upgrade the bundled version. When it's not possible to shim breaking changes, we'll use the version information to present a warning. ### Styling will be specified in JS CSS is a widely-known and well-supported tool for styling user interfaces, which is why we embraced it in Atom. Unfortunately, the performance and maintainability of CSS degrade as the number of selectors increases. CSS also lacks good tools for exposing a versioned theming API and applying programmatic logic such as altering colors. Finally, the browser does not expose APIs for being notified when computed styles change, making it difficult to use CSS as a source of truth for complex components. For a theming system that performs well and scales, we need more direct control. We plan to use a CSS-in-JS approach that automatically generates atomic selectors so as to keep our total number of selectors minimal. ### Text is rendered via WebGL In Atom, the vast majority of computation of any given frame is spent manipulating the DOM, recalculating styles, and performing layout. To achieve good text rendering performance, it is critical that we bypass this overhead and take direct control over rendering. Like Alacritty and Xi, we plan to employ OpenGL to position quads that are mapped to glyph bitmaps in a texture atlas. There isn't always a 1:1 relationship between code units inside a JavaScript string and glyphs on screen. Characters (code points) can be expressed as two 16-bit units, but this situation is simple to detect by examining the numeric ranges of the code units. In other cases, the correspondence between code units and glyphs is less straightforward to determine. If the current font and/or locale depends on ligatures or contextual alternates to render correctly, determining the correspondence between code points and glyphs requires support for complex text shaping that references metadata embedded in the font. Bi-directional text complicates the situation further. For now, our plan is to detect the presence of characters that may require such complex text shaping and fall back to rendering with HTML on the specific lines that require these features. This will enable us to support scripts such as Arabic and Devanagari. For fonts like FiraCode, which include ligatures for common character sequences used in programming, we'll need a different approach. One idea would be to perform a limited subset of text-shaping that just handles ligatures, so as to keep performance high. Another approach that would only work on the desktop would be to use the platform text-shaping and rasterization APIs in this environment. Bypassing the DOM means that we'll need to implement styling and text layout ourselves. That is a high price to pay, but we think it will be worth it to bypass the performance overhead imposed by the DOM. ## Development process ### Experiment At this phase, this code is focused on learning. Whatever code we write should be production-quality, but we don't need to support everything at this phase. We can defer features that don't contribute substantially to learning. ### Documentation-driven development Before coding, we ask ourselves whether the code we're writing can be motivated by something that's written in the guide. The right approach here will always be a judgment call, but let's err on the side of transparency and see what happens. ### Disciplined monorepo All code related to Xray should live in this repository, but intra-repository dependencies should be expressed in a disciplined way to ensure that a one-line docs change doesn't require us to rebuild the world. Builds should be finger-printed on a per-component basis and we should aim to keep components granular. ## Contributing Interested in helping out? Welcome! Check out the [CONTRIBUTING](./CONTRIBUTING.md) guide to get started. ================================================ FILE: docs/architecture/001_client_server_protocol.md ================================================ # Xray's client/server protocol Xray is organized around a client/server architecture, with all the application logic located in a central server. User-facing components connect to this server as clients to present the user experience. ## Major application components ![Major components](../images/client_server_components.png) All application logic is controlled by a single server that listens on a domain socket located at `ATOM_SOCKET_PATH`. We connect to the server with three different types of clients: * **CLI:** When you run the `xray` binary, we will check if a socket for the server already exists and is listening. If it does, we will connect to this socket and communicate with the server directly. For example, the application may already be running, but we want to open a new workspace for a given path. To do that, we just connect to the existing socket and send it an `OpenWorkspace` message. If the CLI is unable to connect to the socket, it spawns the Electron app and waits for it to report that it is `Listening\n` on `stdout`. * **App:** The Electron app in `xray_electron` spawns the server as a child process on startup and identifies itself as the application client via the `{type: "StartApp"}` message. The server then sends the app client application-level command messages like the `OpenWindow` message, which tells the app to open a new window. * **Window:** When the server tells the app to open a window, it provides a window id, which gets passed to the Electron window in the URL. Once the window loads, it connects to the server's socket and identifies itself as a window, supplying this id. ## The window protocol ![Window protocol diagram](../images/window_protocol.png) The protocol between the window and the server is inspired by the [Flux application architecture](https://facebook.github.io/flux/), though it's probably different in some ways due to the particular needs of Xray. The state of the UI for any given window is managed entirely by the server. It creates a `Window` object for each connected window, and this `Window` object is responsible for managing a tree of views to be rendered by the connected client. Each view is associated with a unique identifier, a component name, and a plain-old JS object representing the view's state. Views can refer to *other* views via their id. When views are added and removed from the `Window` object on the server side, updates are automatically relayed to the client. The server calls `render()` on any newly added views to obtain a JSON object representing the view's state. The window also observes an `updates()` stream associated with each view, and sends a new update for a view's state if the view becomes dirty. To keep things simple, each time a view is updated, its entire state tree is sent again across the wire. For this reason, it's important to limit the size of each view's state object to avoid transmission and parsing overhead. Since the required data for a given view is naturally limited by the viewport, this should be acceptable. We may switch to protocol buffers if JSON parsing overhead becomes a bottleneck. The root view of a typical window is a `WorkspaceView` with an id of `0`. Its props refer to other views that are displayed in the workspace via their id. For example, the workspace may contain a `BufferView` (editor) with id 1, and also be presenting a `FileFinderView` with id 2 as a modal panel. When views are added to the `Window`, they are provided with a `WindowHandle` via the optional `did_mount` method that allows them to add additional sub-views to the window. When a view adds a sub-view, it receives a `ViewHandle`. When this handle is dropped, the sub-view is automatically removed from the `Window` and deleted on the client. In the render process, we maintain a `ViewRegistry` which mirrors the state of the `Window` in the server process. The `ViewRegistry` contains an imperative interface for fetching the component and props associated with a particular view id, although most code will interface with the registry declaratively via special React components. The render process communicates changes to the server via *actions*, which are plain-old JS objects that can be dispatched to a particular view id. These actions are dispatched from the `ViewRegistry` on the render process and make their way to the server, where they are routed by the `Window` object. `Window` calls `dispatch_action` on the view corresponding to the action's specified `view_id`, passing the JSON to the view for handling. Views can handle an action by updating their own state or the state of other model objects. The `Window` detects state changes via the `updates` stream of any current views, then sends these updates to the client. The server can also tell the window to *focus* a particular view by calling the `focus` method on its top-level React component. This can be accessed via the `ViewHandle::focus` method on the server side. These commands are simply relayed to the client. The server has no explicit model of focus state. ## Detecting when views need to be re-rendered Each view is associated with an `updates` stream, which is implemented with the Rust [`futures`](https://docs.rs/futures/0.2.0-alpha/futures/) crate. A full explanation of Rust futures is beyond the scope of this document, but their poll-oriented nature is relevant to this use case. The `Window` object represents the messages that need to be sent to the client as a `Stream` called `WindowUpdateStream`. This stream implements `poll`, which checks dirty sets for inserted and removed views, then calls `poll` on the updates stream of all currently-visible views. Any views returning `Async::Ready` are then rendered and added to the next update to be sent to the client. If polling every visible view on each poll of the window update stream turns out to have too much overhead, we can always employ a similar strategy to the `FuturesUnordered` object and track notifications in a more fine-grained way. However, since we're anticipating rendering far fewer than 100 discrete views at any one time, we don't think polling everything should be an issue. One cool feature of the stream-oriented approach for detecting individual view updates is that a given view's update stream could be composed from other streams in fairly complex ways. For example, the updates stream of a `BufferView` (editor) could derive from a `NotifyCell` on the view itself, plus an updates stream on the view's `Buffer`, which it could share with other `BufferView`s. ## Declarative interface on the client To consume view state on the client, we implement a `ViewRegistry` that allows you to get the component and props for any view id, and also watch those props for updates. The `ViewRegistry`'s imperative API is wrapped in a component-oriented interface. At the root of the component hierarchy is the `App` component, which injects the view registry into the component tree's [context](https://reactjs.org/docs/context.html). Beneath the `App` component, the `View` component can be used to render a view with a specific id. The `View` component takes the view's `id` as a property, then retrieves the view's component and props from the registry and renders the component as a child. It also sets up an observer on the view's props, re-rendering its child component with the new properties when they change. Finally, the `View` component passes a `dispatch` method as a property to the child component, giving it the ability to send arbitrary actions as plain JS objects back to the view's server-side representation. ================================================ FILE: docs/architecture/002_shared_workspaces.md ================================================ # Shared workspaces ## Current features An instance of `xray_server` can host one or more shared workspaces, which can be accessed by other `xray_server` instances over the network. Currently, when connecting to a remote peer, we automatically open its first shared workspace in a window on the client. The client can use the file finder to locate and open any file in the shared workspace's project. When multiple participants open a buffer for the same file, their edits are replicated to other collaborators in real time. ### Server * `xray foo/ bar/ --listen 8888` starts the Xray server listening on port 8888. * The `--headless` flag can be passed to create a server that only hosts workspaces for other clients and does not present its own UI. ### Basic client experience * `xray --connect hostname:port` opens a new window that is connected to the first workspace available on the remote host. * `cmd-t` in the new window searches through paths in the remote workspace. * Selecting a path opens it. * Running `xray --connect` from a second instance allows for collaborative editing when clients open the same buffer. ### Selecting between multiple workspaces on the client * If the host exposes multiple workspaces, `xray --connect hostname:port` opens an *Open Workspace* dialog that allows the user to select which workspace to open. * `cmd-o` in any Xray window opens the *Open Workspace* dialog listing workspaces from all connected servers. ## RPC System We implement shared workspaces on top of an RPC system that allows objects on the client to derive their state and behavior from objects that live on the server. ### Goals #### Support replicated objects The primary goal of the system is to support the construction of a replicated object-oriented domain model. In addition to supporting remote procedure calls, we also want the system to explicitly support long-lived, stateful objects that change over time. Replication support should be fairly additive, meaning that the domain model on the server side should be designed pretty much as if it weren't replicated. On the client side, interacting with representations of remote objects should be explicit but convenient. #### Capabilities-based security Secure ECMA Script and Cap'N Proto introduced me to the concept of capabilities-based security, and our system adopts the same philosophy. Objects on the server are exposed via *services*, which can be thought of as "capabilities" that grant access to a narrow slice of functionality that is dynamically defined. Starting from a single root service, remote users are granted increasing access by being provided with additional capabilities. #### Dynamic resource management Server-side services only need to live as long as they are referenced by a client. Server-side code can elect to retain a reference to a service. Otherwise, ownership is maintained by clients over the wire. If both the server and the client drop their reference-counted handle to a service, we should drop the service on the server side automatically. #### Binary messages We want to move data efficiently between the server and client, so a binary encoding scheme for messages is important. For now, we're using bincode for convenience, but we should eventually switch to Protocol Buffers to support graceful evolution of the protocol. ### Design ![Diagram](../images/rpc.png) **Services** are the fundamental abstraction of the system. In `rpc::server`, `Service` is a *trait* that can be implemented by a custom service wrapper for each domain object that makes the object accessible to remote clients. A `Service` exposes a static snapshot of the object's initial state, a stream of updates, and the ability to handle requests. The `Service` trait has various associated types for `Request`, `Response`, `Update`, and `State`. When server-side code accepts connections, it creates an `rpc::server::Connection` object for each client that takes ownership of the `Stream` of that client's incoming messages. `Connection`s must be created with a *root service*, which is sent to the client immediately. The `Connection` is itself a `Stream` of outgoing messages to be sent to the connected client. On the client side, we create a connection by passing the `Stream` of incoming messages to `rpc::client::Connection::new`, which returns a *future* for a tuple containing two objects. The first object is a `rpc::client::Service` representing the *root service* that was sent from the server. The second is an instance of `client::Connection`, which is a `Stream` of outgoing messages to send to the server. Using the root service, the client can make requests to gain access to additional services. In Xray, the root service is currently `app::AppService`, which includes a list of shared workspaces in its replicated state. After a client connects to a server, it stores a handle to its root service in a `PeerList` object. We will eventually build a `PeerListView` based on the state of the `PeerList`, which allows the user to open a remote workspace on any connected peer. For now, we automatically open the first workspace when connecting to a remote peer. When we connect to a remote workspace, we send an `app::ServiceRequest::OpenWorkspace` message to the remote `AppService`. When handling this request in the `AppService` on the server, we call `add_service` on the connection with a `WorkspaceService` for the requested workspace, which returns us a `ServiceId` integer. We send that id to the client in the response. When handling the response on the client, we call `take_service` on root service with the id to take ownership of a handle to the remote service. We can then create a `RemoteWorkspace` and pass it ownership of the service handle to the remote workspace. `RemoteWorkspace` and `LocalWorkspace` both implement the `Workspace` trait, which allows a `RemoteWorkspace` to be used in the system in all of the same ways that a `LocalWorkspace` can. We create the illusion that remote domain objects are really local through a combination of state replication and remote procedure calls. Fuzzy finding on the project file trees is addressed through replication, since the data size is typically small and the task is latency sensitive. Project-wide search is implemented via RPC, since replicating the contents of the entire remote file system would be costly, especially for the in-browser use case. Buffer replication is implemented by relaying conflict-free representations of individual edit operations, which can be correctly integrated on remote replicas due to our use of a CRDT in Xray's underlying buffer implementation. ================================================ FILE: docs/architecture/003_memo_epochs.md ================================================ The following document describes the sequence of operations that we should perform when the repository HEAD changes, both on the machine where the HEAD change occurred and at remote sites that receive the resulting epoch change. The algorithms assume that version vectors don't reset across epochs. This does raise the concern that version vectors could grow without bound over the life of the repository, but we're going to suspend that concern temporarily to make progress. ### Creating a new epoch after HEAD moves Assume we are currently at epoch A described by Tree T. - Scan all the entries from Git's database based on the new HEAD into a new Tree T'. - Synthesize and apply operations for all uncommitted changes via a `git diff`. This includes file system operations as well as uncommitted changes to file contents. - For all buffers with unsaved edits in T: - Diff the last saved contents in T against the current contents of T' using the path of the buffer in T. This diff will describe a set of regions that have been touched outside of our control. - Go through each of the unsaved operations in T and check if they intersect with any of the regions in this diff to detect a conflict. - If there is a conflict, synthesize operations by performing a diff between the contents of T' and the contents of T and apply these as unsaved operations on top of T', then mark the buffer as in conflict. - Otherwise, transform all the unsaved operations according to the initial diff and apply them to the buffer in T'. Afterward, we broadcast a new epoch B that contains the new HEAD SHA, the work tree's current version vector, a Lamport timestamp, and all synthesized operations. ### Receiving a new epoch * Check Lamport timestamp of the epoch. If it's less than the current epoch's timestamp, ignore it. Otherwise, proceed to change the active epoch as follows: * Scan all entries from Git's database based on the new epoch's HEAD SHA into a new Tree T'. * Apply operations that are associated with the new epoch to T'. What happens to buffers? * For all buffers containing edits not included in the epoch change's version vector: * If a file with the same path exists in T': * Diff the contents that are included in the version vector against the contents of T' using the path of the buffer in T. This diff will describe a set of regions that have been touched outside of our control. * Go through each of the local edits that were not part of the version vector. If they do not directly conflict with a region in the diff, synthesize a new operation with an adjusted position based on the diff and apply it to T'. * If no file with that path exists in T', we create it with initial contents from T. ================================================ FILE: docs/updates/2018_03_05.md ================================================ # Update for March 5, 2018 ## Contributions We received some great contributions from [@dirk](https://github.com/dirk) that improved error handling ([#5](https://github.com/atom/xray/pull/5)) and refined how we build our N-API bindings ([#7](https://github.com/atom/xray/pull/7), [#9](https://github.com/atom/xray/pull/9)). He also clarified our build process in the documentation and added an explicit Electron dependency now that the new beta supports N-API ([#10](https://github.com/atom/xray/pull/10)). Thanks @dirk! ## 12-week experiment Our plan is to dedicate 12 full weeks to Xray and see how far we can get with the implementation. We originally planned to start this trial period 2 weeks ago, but decided to defer it in order to spend more time doing planning around our vision for real time collaboration. So last week will count as week 1 of 12. This week is week 2 of 12. ## Text shaping We're currently rendering text with a fairly naive strategy, where we just transform code points to glyphs and position them one after another with WebGL. The great thing about this strategy is it's really fast. It takes me ~1.2 ms to render a full screen's worth of text on a late 2016 MacBook Pro. The downside of this strategy is that we don't perform correct text shaping. Last week, we explored running all of our lines through HarfBuzz compiled as a separate WebAssembly module, but in our tests, running HarfBuzz on 50 lines of 100 characters each was taking between 4.5ms and 20ms, depending on the font. Since our target for a frame is 8ms, this makes us pretty reluctant to pursue this path further. We're a code editor, not a word processor, so it's not clear that we *need* all the features that a full-on text shaping engine provides. If we don't do some sort of text shaping (and HarfBuzz seems like the only game in town), here's what we'll be missing: * No ligatures support: Text shapers combine code points with tables embedded in the font to decide when to render ligatures. We're a code editor, so this isn't a deal-breaker, but fonts like Fira Code rely on ligatures to render special characters for common programming sequences such as `<=`. * No kerning support: For fixed width fonts, we weren't able to observe any noticeable difference for a lack of kerning. For variable-width fonts like Helvetica, rendering without kerning looks a bit odd. Again, we're a code editor, so not a deal-breaker. * No support for bi-directional text. This isn't a deal-breaker in the short term, since all of the dominant programming languages are based on latin scripts. Again, it's not our ambition to become a general word processor. Long term, however, we need to support right-to-left text appearing in strings and comments in order to be usable by developers working with languages like Arabic and Hebrew. Interestingly, Sublime Text does not appear to support bidirectional text, but we'd like to do better. * No support for context-sensitive substitutions. In Arabic and Indic scripts, the same characters can render different glyphs depending on their context. Sublime also does not support this. Based on what we have learned and the above limitations, this our plan for text shaping going forward. In the near-term: * Don't support full text shaping in the general case. We want to emphasize maximal speed for the common case, which shouldn't require full text shaping. If running text shaping on every line took less than 1ms, it would be worth it, but we'd prefer not to pay what it appears to cost. At some point in the future, make the following enhancements: * Add bi-directional text support. We've run into trouble building a library that combines both Rust and C/C++ in a single WebAssembly module, so the ideal path would be to find or write an implementation of the Unicode bi-directional text algorithm in Rust and embed it in `xray_core`. One important detail is that we need to preserve the correspondence between column positions in the source and rendered text in order to render cursors and interpret mouse interactions, so just transforming the text alone will be insufficient. * Use presentational characters to render Arabic [as described in this blog post](https://blog.mapbox.com/improving-arabic-and-hebrew-text-in-map-labels-fd184cf5ebd1), again porting an existing implementation of this transformation to Rust and incorporating it into `xray_core`. Again, we'll need to maintain a mapping of how characters in the input and the output map for cursor positioning. Several of the existing implementations of this transformation are GPL-licensed, so we'll need to be careful to avoid deriving our work from one of them. * Add limited ligatures support at some point in the future to `xray_core`. This would involve loading the font and consulting the lookup table for ligatures. The goal would be support for fonts like Fira Code, and the hope is that we will be able to efficiently perform just this subset of the generalized text shaping workload within our budget of 1ms. * Render sequences of Indic characters as atomic units via canvas rather than trying to render and composite individual glyphs like we do for other scripts. This would rely on the text-shaping built into the browser to render words in these scripts. We will pay a performance cost, but since we're anticipating these characters to appear rarely as part of comments and strings, it should be acceptable and better than adding the performance and complexity of full shaping for cases where it isn't needed. Producing a lightning fast editor that runs on the web is going to involve trade-offs, and we'll need to make some tough decisions. Avoiding full text shaping is one of them. It would be great to be fast *and* perfectly correct in all cases, but we're not willing to sacrifice speed in the common case for perfect correctness at the corners. We're going to post some help-wanted issues to see if anyone is interested in helping out with some of the compromise solutions in the above plan. So in conclusion, we didn't end up merging any *code* related to text shaping, but we did learn a lot and came up with a clear plan for how to proceed. ## Anchors and selections The bulk of the week was spent adding support for selections to the editor. The first step was an introduction of a new abstraction called *anchors*. Anchors serve a similar role to markers in Atom today, but they have a much cleaner implementation due to the buffer being a CRDT. An anchor is a *value* that tracks a logical position in a buffer. You create an anchor by calling one of the following methods on the buffer: * `anchor_before_offset` * `anchor_after_offset` * `anchor_before_point` * `anchor_after_point` These return an opaque `Anchor` value, which can be converted back to a concrete offset or point in the future via the following methods: * `offset_for_anchor` * `point_for_anchor` Internally, an anchor is an enum that either represents either the `Start` or `End` of the file or some point in the `Middle` of the file via an `insertion_id`, `offset`, and `bias`. If you create an anchor at offset 10, its position will be updated by any edits that occur prior to offset 10, so that it always tracks the same logical position in the text. So if you create this anchor *before* offset 10, it will have a *left* bias and remain at that offset even if there is a subsequent insertion at its exact location. If the anchor is created *after* offset 10, it will have a *right* bias and be pushed rightward by insertions at its location. Selections are built on top of anchors. Each anchor maintains a vector of selections ordered by their start anchor, maintaining the invariant that the selection ranges are always disjoint. We use anchors for selections rather than absolute positions so that the logical intention of the user is maintained even in the face of edits to the buffer by other users or by packages. We implemented basic cursor movements and selection expansions (up, down, left, and right) as well as methods to add a selection above and below the current. We plan to render selections and cursors as additional WebGL shader passes that draw solid rectangles. We have the plumbing mostly in place to do this, but haven't finished actually populating the buffers on the GPU to tell the shaders where to draw. We're hoping to have that finished early this week, so we can move on to handling the input to actually move the selections and cursors around. That will raise the question of how we handle key bindings and commands in Xray, which could take some time to iron out. Once we can render and manipulate selections, we'll move on to handling keystrokes to perform actual edits to the buffer. The `splice` method already exists to enable edits, so it should just be a matter of calling it in a loop in reverse order of the selection ranges. Once we add some caching related to translating anchors to positions, we can measure our performance and see how many cursors we can type with within our 8ms target window. Hopefully we do well. ## The week ahead We'll be a bit short-handed this week due to @as-cii being on reactive duty for Atom and @nathansobo heading to Denver on Wednesday to give a talk at Pivotal Labs. We hope to finish selection rendering and ideally also get an initial solution in place for key bindings and commands to move those selections around. If things go really well, we'll start on editing. ================================================ FILE: docs/updates/2018_03_12.md ================================================ # Update for March 12, 2018 ## Contributions We got some help from [@apcragg](https://github.com/apcragg), who made [the build script for our N-API bindings support Linux and Windows](https://github.com/atom/xray/pull/25). Also [@maxim-lian](https://github.com/maxim-lian) helped by updating our repo to correctly use [Cargo workspaces](https://github.com/atom/xray/pull/26). Also saw a bunch of people building the project and exploring. There's not much to see yet, but we're happy that people are interested. ## Hacker News and new contributor interest Someone posted this repo to Hacker News last week, which drew a bunch of attention to the project. Overall this is obviously great, but I sort of regret reading the comments. People can be so mean on HN and it's really a drag to feel like you're the target of vitriol and derision. I'm just over here trying to build stuff. But I guess you gotta just have a thick skin and keep coding. Thanks to everyone who jumped in with an interest in contributing! The engagement was really helpful and prompted me to post [the beginnings of a contributing guide](https://github.com/atom/xray/blob/master/CONTRIBUTING.md) and some initial [help-wanted issues](https://github.com/atom/xray/labels/help%20wanted). Looking forward to engaging with someone who wants to dive in on one of those problems. ## Progress on selections We have selections and cursors rendering on a branch, and hope to merge it this week. The results are promising, and we're only exceeding our 8 ms frame budget when we reach thousands of selections in a document with thousands of edits. We think we can do better with some optimizations. We plan to merge an initial PR that lays the groundwork this week. This will include the ability to model and render selections, but we need to get a basic key bindings and commands system in place before this will mean much in the UI. We'll post some more detailed benchmarks once we get this basic implementation integrated and have a chance to look at the profile for any basic optimizations. ## Big architectural changes incoming A conversation with [@joefitzgerald](https://github.com/joefitzgerald) at Pivotal in Denver led to a big shift in how we plan to interoperate between JavaScript and Rust. Originally, we planned to embed a shared library written in Rust into the Electron render process and use Node's N-API to interoperate. Somewhere along the line, we realized that to maintain a global edit history for all files that wasn't tied to a project, we would need to unify all of the file system interaction in a single process to avoid race conditions between multiple Electron windows. This process gradually grew in our thinking to take on other responsibilities, such as connecting to other processes to facilitate real time collaboration. Eventually, we decided that we would need to route all I/O through this central process. Joe asked a simple and insightful question. If we're planning to route all I/O through an external process, then what's the point of the Node APIs available in Electron? They're all designed around I/O, but we won't be doing any. This made it click for me that we should actually move *all* of the application logic completely into the external Rust process and treat the UI as an extremely thin layer that interacts with the app via an async channel. We'll use something akin to the Flux architecture, where the UI can submit actions to the server on the channel and receive JSON payloads representing state to render. Yes, I realize that this makes us even *more* similar to Xi architecturally. This will make the UI code 100% ordinary HTML and JavaScript with no assumption of any special APIs, which really supports our goal of running on the web. For the desktop experience, we can use any solution that gives us a modern, standards-compliant web view and run the core of the application as a server process. The easiest solution on the desktop would be Electron, but we could even use an embedded WebKit view on the Mac to save on bundle size if we wanted to absorb that complexity (it's not clear it's worth it though). On the web, we'll need the server process to run inside the browser, because this whole design assumes an ultra-low-latency connection between the front end and the back end. In this scenario, we can compile some subset of the server process that runs on the desktop to WebAssembly and run it in a worker thread. This web "back end" can then establish a peer-to-peer connection with another Xray process running in the cloud or on another user's computer. This will provide a 100% compatible experience between the browser and the desktop and enable collaboration between users in either environment. Here's a somewhat complicated picture of our current thinking. Note the centrality of the "Xray Core Process". All the logic and extensibility lives there, and the view becomes much simpler. ![New architecture](../images/architecture.png) We'll be starting on these changes this week. The first step is to completely eliminate Electron and wrap `xray_core` in a server process, which we're thinking about calling `xray_server`. The front end will be `xray_client` and compile down to a simple HTML file. When you open it in the browser, it will connect to a local port based on the query parameter in the URL. Once we accomplish that, we'll need to figure out an alternate variant of the server, maybe called `xray_server_wasm`, which runs a "server" in one or more worker threads for a low-latency experience on the web. Compiling pure Rust to WebAssembly hopefully won't be a big deal, but we are a bit worried about including TreeSitter, whose runtime and grammars are written in C. We think we can figure it out though. In light of these big structural changes, we'll probably be holding off on merging any PRs this week until the dust has settled. Thanks for your patience. ## What about Xi? I've mentioned elsewhere in this repo the conversation I had with Raph Levien a few weeks ago about collaborating with Xi. In light of the fact that we're moving even closer to Xi architecturally, I can already anticipate the criticism that what we're doing here is redundant and we should just contribute to Xi. Why bother with this? First of all, I'm definitely not opposed to some sort of partnership with Xi. I have tremendous respect for the technical work they've done and really *like* Raph personally. That said, it's complicated. We have our own ideas for where we want to take this project and what it means to GitHub. At the moment, the only way I see to guarantee we can achieve those ideas is to have enough creative control to iterate and follow our own path. Xi has been around for a while now, and it would be rude and presumptuous to roll in and start telling them how they should run their project. If we want to be active participants in our own destiny without telling other people what to do, it seems like we need to take our own path, at least for now. We would like to learn as much as we can from the Xi team, and we'd be happy if we could provide value to them as well. It seems like sharing ideas would be a necessary prerequisite to sharing code. If the work we're doing proves valuable enough for the Xi team to want to invite us in as partners, then that would be great, but we're really still experimenting here with how best to achieve our particular goals. It's not clear that we have the same priorities as the Xi team, and it's not obvious that our priorities are sufficiently compatible for us share a codebase. And that's okay. There's room for more than one editor in the world. If we do end up having enough social and technical alignment to share code, then that would obviously be great for us, because we'd be working with some incredibly smart people. I want to be open minded, but I also want the freedom to create based on our own ideas without telling anyone else what to do. So it's complicated. It's not crystal clear what the right path is, but we're doing our best to make the best decisions with the experience and wisdom we've managed to acquire to this point. ================================================ FILE: docs/updates/2018_03_19.md ================================================ # Update for March 19, 2018 ## Contributions We have a couple of PRs pending ([#36](https://github.com/atom/xray/pull/36) and [#34](https://github.com/atom/xray/pull/34)), but we're holding off on merging anything until we complete some major architectural changes. Sorry for the delay [@LucaT1](https://github.com/LucaT1) and [@breezykermo](https://github.com/breezykermo). ## Selections optimizations [I merged a PR](https://github.com/atom/xray/pull/45) from [@as-cii](https://github.com/as-cii) that optimized our initial implementation of selections. While we still think there is room for more optimization, we're pretty happy with our early results. On Antonio's machine, he's moving 1k selections in a document with 10k edits in under 2ms. Based on some hacky experimentation to avoid allocations, we think we can make that even faster. At some point, with some number of selections, we're going to end up blowing our frame budget, but we think maintaining it into the thousands of selections ought to be acceptable. ## Significant progress switching to a client/server architecture [@as-cii](https://github.com/as-cii), [@maxbrunsfeld](https://github.com/maxbrunsfeld) and I have made decent progress on a PR to switch Xray to the client/server architecture I [discussed last week](./2018_03_12.md#big-architectural-changes-incoming). We're implementing an event-driven server using [Tokio](https://tokio.rs/), and have what seems like a viable approach for relaying data between the server and the window that will leave the door open to packages implementing custom views that slot in cleanly next to built-in features. Check out [#46](https://github.com/atom/xray/pull/46) for details. I've also written [a fairly detailed document](https://github.com/atom/xray/blob/198e3bdf3c284679a5520923b0e27b079cc23377/docs/architecture/001_client_server_protocol.md) explaining our architecture and the protocol that will become a permanent part of Xray's documentation once this PR is merged. ================================================ FILE: docs/updates/2018_03_26.md ================================================ # Update for March 26, 2018 ## Contributions [@matthewwithanm](https://github.com/matthewwithanm) of Facebook's Nuclide team helped us improve our React game by [avoiding the use of deprecated string refs](https://github.com/atom/xray/pull/50) and [avoiding the use of component `state` for data that is unrelated to rendering](https://github.com/atom/xray/pull/51). Thanks Matthew! ## The switch to a client/server architecture is complete We merged [#46](https://github.com/atom/xray/pull/46) last week, completing our switch to a client/server architecture. JavaScript in Xray's user interface now communicates with the Rust core over a domain socket rather than via a native V8 extension, which dramatically simplifies our build process. We connect to the server over a domain socket, which unfortunately means that Xray doesn't work on Windows for now due to the unavailability of domain sockets in the OS. If anyone is interested in adding support for named pipes on Windows to `xray_server`, we'd gladly collaborate on a pull request. If you've tried to build Xray and ran into trouble, now would be a good time to try again on non-Windows platforms after [carefully reading our build instructions](../../CONTRIBUTING.md#building). ## Updated roadmap We've adjusted our roadmap a bit to prioritize collaborative editing rather than focusing on producing WebAssembly-based editor build. A browser-compatible editor is still part of our long term plan and we're designing the system with that requirement in mind, but since we want all of Xray's features to support remote collaboration, it makes sense to get it into the architecture early. ## Fast file finding Xray is currently hard-coded to open a single buffer containing the dev build of React, which isn't very useful. To fix that, [we're adding a file finder](https://github.com/atom/xray/pull/55) that can quickly filter all files in the project that match a given subsequence query. To obtain good search performance, we're maintaining an in-memory replica of the names of all the files and directories in the project which we can brute-force scan on a background thread whenever the query changes. We represent this data as a simple tree which reflects the hierarchy of the file system. To ensure that we can respond to user input within our 50ms deadline for coarse-grained interactions, we really want to be able to run queries before we finish reading all of the entries from the file system. To enable that, we're designing our in-memory file tree to support concurrent reads and writes. We spent a decent amount of time exploring different approaches that could enable this, and ultimately we decided to protect the entry vector for each directory with a fine-grained read/write lock. When [@as-cii](https://github.com/as-cii) first suggested this approach, I was worried that it would consume too much memory, but I then discovered the [parking_lot](https://github.com/Amanieu/parking_lot) crate, whose `RwLock` implementation only consumes a word of memory per instance. The basic logic of searching will be in `xray_core` and is modeled as a `Future` to give us flexibility in how we schedule it. For `xray_server`, which runs as a standalone binary and has full threading capabilities, we can simply spawn the search on a thread pool. Until WebAssembly adds threading support, we can implement some kind of background scheduler that uses `requestIdleCallback` to break the work up into smaller chunks before yielding the thread. Rust futures are based on a polling model, where the executor repeatedly calls `poll` on the future to drive it to completion. To support granular yielding in a single threaded environment, we really need to execute the minimal amount of work each time `poll` is called on our `fs::Search` future. To enable that, we maintain a stack within the future that tracks our current position within the tree. The stack keeps an `Arc` (atomic reference-counted) pointer to the entries of each directory, along with the current index into that list of entries. Since concurrent writers could insert entries that might invalidate these indices, we treat directory entries as clone-on-write if we detect they are referenced by more than one `Arc` pointer, via the `Arc::make_mut` method. Most of the time, writes should be able to freely mutate a directory's vector of entries, but if that write might interfere with an ongoing search, we clone the vector to avoid invalidating any active indices. The work is still in progress, but we're hoping this design will enable a highly user responsive experience for file finding even in the presence of extremely large source directories. We'll report on our findings in the next update. ## Thoughts on key bindings and actions We're optimistic that we can finish up a basic (but fast) file finding UX some time next week. After that, I think it's time to tackle key bindings. Atom's key binding implementation is insanely complex and jumps through some ridiculous hoops to support a long tail of different locales and features like overlapping multi-stroke bindings, binding to key-up events, etc. Eventually, we want Xray to support all of these features as well, but in the short term, we want to keep the implementation as simple as possible. We're going to start by targeting single-stroke bindings and avoid any gymnastics to workaround browser limitations in various international locales. We'll revisit these concerns after getting some more traction in other areas of the system. Our strategy with Atom was to "embrace the web", which led us to associate key bindings with CSS selectors. This was a neat idea and served Atom reasonably well, since CSS selectors are a powerful tool for describing a specific context in the DOM. However, in the end I don't think the power was worth the complexity of full-blown CSS selectors. Their flexibility makes it extremely difficult to build a user interface for configuring bindings, and the complex rules for evaluating selector specificity can lead to a frustrating experience. With Xray, I want a system for making key bindings context-sensitive that is flexible enough to support most reasonable use cases, but not so flexible that it becomes hard to reason about. My thoughts are still evolving on this, but I'm thinking about representing the context in which we interpret a key binding as a set of simple tokens called an "action context". A custom component can be used to refine this context for a subset of the view hierarchy by adding or removing tokens. Let's use an example to explain how the system would work. This is going to be a bit contrived, but it's not totally unrealistic. Imagine you wanted to write a spell-checking extension that allowed the user to display a list of suggestions next to a misspelled word that could be navigated from the keyboard. It might look something like this: ```js class SpellingSuggestions extends React.Component { render () {
...
} } ``` In the example above, we declare a refinement to the action context via an `ActionContext` JSX tag at the root of the component, adding the `SpellingSuggestions` and `VerticalNav` tokens and removing `Insert`. We then declare three actions that this component handles via `Action` tags: `NavUp` and `NavDown`, and `Confirm`. Normally in the editor, the up and down arrows would be bound to the `MoveCursorUp` and `MoveCursorDown` actions, which move the cursor. But when your menu is displaying, you want the arrow keys to select the next or previous item in the list instead. To enable that, the up and down arrow keys could be bound to `NavUp` and `NavDown` within the `VerticalNav` context. The left and right arrow keys would continue to move the cursor, and potentially dismiss the menu if you moved out of the misspelled word. If you didn't like the menu hijacking your cursor movement, you could unbind the arrow keys in the `VerticalNav` context, or maybe leave the arrow keys bound but preserve the Emacs-style `ctrl-p` and `ctrl-n` bindings for cursor movement. Users might also bind `j` and `k` to `NavUp` and `NavDown` in any context that is not `Insert`. The text editor would introduce `Insert` to the action context because it inserts text, but the spelling suggestions menu could temporarily override that by removing `Insert` from the context. So could a Vim extension in command mode. This system is still pretty complex, but its semantics are much simpler than CSS selectors, and it seems like it could cover compositional scenarios like the one described above rather well. We could easily provide some kind of global registry of action context tokens that gives them a human-readable name and description, then use that in a user interface that makes it convenient for users to customize their bindings in specific contexts without opening a JSON file. ================================================ FILE: docs/updates/2018_04_02.md ================================================ # Update for April 2, 2018 ## Contributions [@chgibb](https://github.com/chgibb) helped us get an initial Travis build in [#48](https://github.com/atom/xray/pull/48). This partially addresses [our help-wanted issue](https://github.com/atom/xray/issues/22), but we're still going to leave it open since we want to run the minimal tests for a given change to mitigate one of the downsides of being a monorepo. Thanks to @chgibb for getting this started. ## Almost done with the file finder Our main focus last week was finishing up the file finder feature that I also [discussed in the previous update](./2018_03_26.md#fast-file-finding). The last update was all about our approach to scanning the directory tree from the file system into an in-memory representation, and the approach we described remains pretty much unchanged. We plan to merge [the pull request](https://github.com/atom/xray/pull/55) early this week. ### Leveraging prior art Last week was all about using that in-memory representation to return search results based on a "fuzzy" search query. After an initial attempt that yielded decent performance but poor ranking of the search results, we decided to investigate existing solutions. We tried two command-line fuzzy finders, [`fzy`](https://github.com/jhawthorn/fzy) (written in C) and [`fzf`](https://github.com/junegunn/fzf) (written in Go) on the Electron repository, which contains over 500,000 files when `.gitignore` is disabled. Both tools yielded excellent performance and high quality results, and since the [core matching algorithm](https://github.com/jhawthorn/fzy/blob/47609dbf73789bc28289576a12177965c04ef49b/src/match.c#L70) behind `fzy` was reasonably straightforward to read and understand, we decided to port it to Rust. You can [read more about the algorithm](https://github.com/jhawthorn/fzy/blob/master/ALGORITHM.md) in the `fzy` repository, but at a high-level, their solution is based on dynamic programming and determines the optimal match positions for a given substring by populating a matrix with cascading values. We copied their basic approach almost exactly, but we also enhanced it a bit to make use of the existing tree structure to recycle computation for common path prefixes. ### Matching and scoring Xray matches paths in two phases. First, [we scan the tree to determine which paths match the query](https://github.com/atom/xray/blob/3c25fc7a7328b0ce1f6746990689e0f80bca3009/xray_core/src/project.rs#L93), populating a hash map to mark which file system entries either match the query or contain matches to the query. Simply matching the query only requires us to perform linear character comparison and is fairly cheap to perform, and this allows us to constrain the search space for the next step. Once we determine matches, [we then walk the tree to associate each matching path with a score](https://github.com/atom/xray/blob/3c25fc7a7328b0ce1f6746990689e0f80bca3009/xray_core/src/project.rs#L154). Scoring is O(N*M), where N is the length of the query and M is the length of the path. Luckily, longer queries tend to match fewer paths, which means when it is most expensive to compute scores, we usually end up needing to compute fewer of them. ### Results Overall, we're happy with the results. The quality of the matches is extremely high thanks to the work [@jhawthorn](https://github.com/jhawthorn) put into tuning the scoring criteria. Since ranking matches is somewhat subjective, basing our results on an existing, fairly mature solution gives us a lot more confidence in the quality of the results. The performance is also pretty decent. Searching for `init` in the 151,201 files of the [`blink`](https://chromium.googlesource.com/chromium/blink/+/master) repository yields results in ~120ms on my machine. Searching for `init.py`, which is a more selective query, drops that to ~16ms. ### Future improvements These early results are good, but we think there's room for improvement. First, we're still matching on a single thread, and it seems like we might be able to use [Rayon](https://github.com/rayon-rs/rayon) to parallelize the matching over multiple CPU cores. We could also do a better job reporting progress. 20ms into the query we could check if we are more than 20% complete with ranking, and if we aren't we could display some sort of subtle progress indicator. That could help the search feel *responsive* even if it takes 100+ms to return results. That said, we're going to call this good for now and move on to other areas. The file finder *feels* fast and fluid now, even for big repositories, and we think we have a solid foundation in place for future improvements. ## Other improvements Since we're still fairly early in development, we're allowing branches to get longer and heavier than we might in a more established project. Folded into the file finder branch are a few smaller improvements that made sense to add along the way. ### Window and view API refinements We display the file finder as a modal in the workspace, and when the user selects a file or cancels the modal, we need to take action in the workspace. After pondering a couple of approaches, we ended up deciding to use a fairly traditional delegate pattern here, where the `WorkspaceView` implements the `FileFinderViewDelegate` trait and passes a weak reference of itself to the `FileFinderView`. Trouble is, how does the `WorkspaceView` obtain a weak reference to itself? Since the `Window` wraps each view in an `Rc`, we ended up deciding that it would be convenient for the window to [pass each view a `WeakViewHandle` to itself](https://github.com/atom/xray/blob/3c25fc7a7328b0ce1f6746990689e0f80bca3009/xray_core/src/window.rs#L116) in the view's `will_mount` hook. Many views can simply ignore this parameter, but if views need to perform delegation they can safely store and clone it without worrying about leaking memory, enabling them to hand itself as a delegate of child views. This is how [we connect](https://github.com/atom/xray/blob/3c25fc7a7328b0ce1f6746990689e0f80bca3009/xray_core/src/workspace.rs#L48) actions dispatched on the `FileFinderView` to state changes in the workspace. ### Focus API We also needed a way to focus the file finder when it displays, then focus the newly opened editor after a file is selected. We decided to implement this on the server side via the new `ViewHandle::focus` method. Whenever this method is called, it assigns the `focused` field on the `Window` to the focused view's id. This gets relayed to the client, which calls the `focus` method on the corresponding React component. For now, we aren't interested in replicating the focus state to the server. Server-side code can request that a view be focused, but it can't ask which view is currently focused. This is a decision we can revisit later, but focus is a very weird piece of global state that references individual DOM nodes, so it doesn't seem worth the complexity of attempting to represent it outside of the browser environment. This means that the modal panel will still need to have a bit of custom focus handling logic in order to restore focus to the previous element when cancelled, but so far this seems manageable. ### CLI improvements We've also changed the structure of the CLI's relationship with the server and Electron slightly. Previously, when we spawned Electron, we could ask it to relay a message to the server via the `XRAY_INITIAL_MESSAGE` environment variable. Now, the CLI waits for the Electron app to emit `Listening\n` on `stdout`, then attempts to connect to the server itself to send the initial message. We made this change to deal with error handling. The server may need to report an error message to the CLI over the socket, and this was going to be complicated to achieve with the previous approach of delegating the initial message send to Electron. Waiting for Electron to tell us the server has started may introduce some latency, which is why we initially preferred the delegation approach, but we'll need to actually measure this before the additional complexity is warranted in light of the need to receive a response from the server. ## The week ahead We hope to merge the file finder PR. All that's left is some basic styling and iteration on focus handling. After that, we plan to start working on shared headless workspaces. The hardest part is enabling concurrent text editing, but that's pretty much solved by our use of a CRDT as Xray's core text-storage structure. However, there's still plenty of complexity remaining in terms of how we actually connect buffer replicas together and structure the client/server interaction. We plan to explore [Cap'n Proto RPC](https://capnproto.org/rpc.html), which seems to have an actively-maintained [Rust implementation](https://github.com/capnproto/capnproto-rust). None of us has ever used it, so we'll need to see how the reality matches up to its promises, but on initial investigation it looks like it could be a good fit for Xray's needs. Cap'n Proto offers a compact yet evolvable binary representation for messages, and the RPC system seems like it makes it easy to expose any object over the network in a [secure way](https://capnproto.org/rpc.html#security) and [efficiently call its methods](https://capnproto.org/rpc.html#time-travel-promise-pipelining). As long as they're well-implemented, these features seem sufficiently general to be a foundation for network interaction between Xray instances. At this point, Xray is still too young to be usable. But we're trying to ruthlessly prioritize and zero in on the highest value and highest risk aspects of the system as soon as possible. It's unfortunate that Xray doesn't build on Windows right now, but there's honestly not that much to see or use anyway. If you're a Windows user and you're interested in helping out, getting a named-pipes- or TCP-based connectivity solution in place on Windows would be a great place to start. ================================================ FILE: docs/updates/2018_04_09.md ================================================ # Update for April 9, 2018 ## Shared workspaces We spent the entire week [laying down the foundations that will enable shared workspaces](https://github.com/atom/xray/pull/61). What are shared workspaces? The basic idea is that you'll be able to start a headless Xray instance on a remote machine, then have multiple developers connect and co-inhabit that workspace from their local machines. The fact that our buffers are CRDTs makes concurrent buffer editing relatively straightforward to implement, but we still need a solution for synchronizing state between peers and performing requests and response. After experimenting a bit with Cap'N Proto RPC and feeling a bit overwhelmed by the generated code, we decided to explore what a custom solution might look like. We're not quite done with the implementation, but after a lot of thinking and a bit of wheel-spinning, we have a reasonably solid design for a capabilities-based RPC system that will be a good fit for our use case. I've written up [a much deeper description](https://github.com/atom/xray/blob/9a1a02b7b608225a4c60fa364a1d60c1ef5f59c2/docs/architecture/002_shared_workspaces.md) that will become part of Xray's permanent documentation. Here's a *huge* diagram to get you interested: ![RPC Diagram](../images/rpc.png) ## The week ahead We hope to finish an initial take on the RPC system next week, then start using it to build out a basic demo of shared workspaces. Our goal is to make it possible to find and open paths on the client and support concurrent editing by multiple clients. That may spill into the following week, when I'll be traveling Amsterdam for some in-person full-throttle coding with [@as-cii](https://github.com/as-cii). ================================================ FILE: docs/updates/2018_04_16.md ================================================ # Update for April 16, 2018 ## Contributions [@rleungx](https://github.com/rleungx) [set up a basic benchmarking framework](https://github.com/atom/xray/pull/62) that uses [Criterion](https://github.com/japaric/criterion.rs). ## Progress on shared workspaces By the middle of last week, we had a first iteration of the RPC system that we were happy with, and started using it to build out shared workspaces. To do that, we're adding replication to Xray's model objects. The goal is to be able to use model objects without worrying about whether or not they are remote or local. We're converging on a design where most model objects are represented by a trait, with local and remote concrete implementations of this trait. For example, the project model has a `Project` trait along with `LocalProject` and `RemoteProject` implementations. We also have an `rpc::server::Service` implementation that has a shared reference to a `LocalProject` and exposes it to a remote client. On the client side, the `RemoteProject` owns a `rpc::client::Service` object. When you call a method like `open_buffer` on the client side, it's translated into a network request to a service on the remote peer, which translates the request to a method call on the corresponding `LocalProject`. We have unit tests passing for replication of file system trees and projects, along with the initial state for buffers. We still need to replicate buffer edits. We also have some work to do to refine our treatment of ownership for services on the server side. We think the best approach might be to enable both the client and the server to retain services. So if the server wants to keep a service alive and return it across multiple requests or updates, it can store off a handle to the service. Or it can drop the handle, in which case the client can take ownership over the service. Once the client drops, we'll communicate this fact across the wire and decrement the service's reference count. It's essentially an `Rc` transmitted over the network. We'll see how it goes. ## Syntax awareness This week, [@maxbrunsfeld](https://github.com/maxbrunsfeld) will be diving in on integrating the [Tree-sitter](https://github.com/tree-sitter/tree-sitter) incremental parsing system into Xray. The first step involves some adjustments to the runtime to enable syntax trees to be fully persistent and sharable across threads. Xray's buffers already support this kind of usage, so including syntax trees will enable lots of interesting computations to be pushed into the background. ## Heads-down in Amsterdam [@as-cii](https://github.com/as-cii) and I are meeting up in Amsterdam this week to write as much code as possible together in person. To that end, I'm going to keep this update short so we can get to work. ================================================ FILE: docs/updates/2018_04_23.md ================================================ # Update for April 23, 2018 ## An initial implementation of shared workspaces is complete Last week we [completed the initial milestone for shared workspaces](https://github.com/atom/xray/pull/61), which allows you to connect to a remote Xray instance over TCP and open one of its workspaces in a new window. You can then use the file-finder to locate and open any file in the remote project and collaboratively edit buffers. There is obviously a ton more work to do until we can call our implementation of shared workspaces "done". Xray isn't even really usable right now for even basic text editing due to a long tail of missing features. Regardless, we think it's really important to have this infrastructure in place early. From here on out, every feature we build will be designed to support remote collaboration, and the foundation we've laid over the last two weeks will make that possible. We're pretty excited about the potential RPC system we've built. By combining remote procedure calls with eagerly replicated state and the judicious use of conflict-free replicated data types, we think we can abstract away the physical boundaries that separate individual machines and developers. ## Browser compatibility The [four pillars of Xray](../../README.md#foundational-priorities) are performance, real-time collaboration, browser compatibility, and extensibility. 8 weeks into focused development, we're feeling confident that Xray's architecture can meet our desired performance goals, and we've validated an approach that will bake collaboration into the heart of the system. Before burning down the long list of features that make up a usable text editor, we want to take some time to put the last two pillars in place by getting Xray working in a browser and laying the foundation for extensibility. By taking care of all four of these high-level concerns early, we'll ensure that they're supported as we build out the remainder of the system. To that end, we're now turning our attention to browser compatibility. We've actually been designing Xray with this goal in mind from the beginning. Today, Xray comprises two major components: `xray_server`, which contains the core application logic, and `xray_electron`, which presents the user interface and communicates with `xray_server` over a local socket. Now we need to create versions of these two components that run inside of a web browser. As a browser-based counterpart of the `xray_server` executable, we're creating `xray_wasm`, which will be compiled to WebAssembly and run in a web worker. `xray_wasm` will share the majority of its implementation with `xray_server` via a dependency on the platform-agnostic `xray_core` crate. `xray_core` abstracts its communication with the outside world in terms of abstract traits defined by the Rust `futures` crate. Methods for connecting to remote peers and the user interface accept and return `Stream`s of binary buffers, and the application also expects to be passed `Executor` instances that can schedule futures to be executed in the foreground or background. In the browser, we'll move data via message channels and web sockets rather than using domain sockets and TCP, but these are just transport layers and are easy to abstract in terms of `Stream`s and `Sink`s so they can be passed into the platform-agnostic code. Similarly, we'll integrate with the browser's event loop by writing a custom `Executor` that uses the `Promise` API or `requestIdleCallback` to defer computation. We're using the `wasm-bindgen` crate to interoperate between Rust and JavaScript, and last Friday we managed to get asynchronous bi-directional communication working between Rust and JavaScript. This week, we plan to extract as much UI code as possible from `xray_electron` into a common library called `xray_web`. We'll then create `xray_browser`, which will package everything together into a browser-deployable bundle that runs the core application logic in a web worker and connects it to the UI running on a web page. Since browsers strongly sandbox interaction with the underlying machine, we will only support interactions with remote shared workspaces when Xray is running in a browser. We plan to add WebSockets support to Xray server so that it can accept connections from browser-based clients. We'll also add an `--http` option that exposes a simple web server from `xray_server` that serves a browser-based UI to clients. This will obviously require a security scheme to be useful in a production setting, but it seems like a good way to develop the browser-based user experience. A simple password-list based security scheme would also be pretty quick to add. ================================================ FILE: docs/updates/2018_04_30.md ================================================ # Update for April 30, 2018 ## Xray now runs in a browser Last week, we merged [#67](https://github.com/atom/xray/pull/67), which allows Xray to be run inside of a web browser. The design is different in a couple of details from what I anticipated in last week's update, but the big picture is pretty much what we expected. The main difference is that for now, we decided not to bake HTTP and WebSockets support directly into `xray_server`, but instead place them in [a simple development server](https://github.com/atom/xray/blob/92f6c1959f843059738caff889df0843836cc006/xray_browser/script/server) which is written in Node and proxies WebSocket connections to `xray_server`'s normal TCP-based connection listener. This made it easy to integrate with middleware for WebPack that recompiles our JS bundle during development. Long-term, we'd still like to host web clients directly from `xray_server`, but we want to bundle the static web assets directly into the binary so that `xray_server` can continue to work as a standalone executable. This should definitely be possible, but it doesn't feel important to address it now. ## Demo this week We plan to show off Xray's progress to some colleagues here at GitHub later this week, so to that end, we'll focus some of this week on smaller details that, while not fundamentally advancing architectural concerns, will end up making for a better demo. By the end of this week, we should be rendering the cursors and selections of remote collaborators. We also plan to add a discussion panel to the Xray workspace where collaborators can have a text-based conversation that is linked to their code. Once the demo is behind us, we plan to take a few days to burn down any technical debt we have accrued in the 10 weeks we've been actively developing the project. The biggest thing on our agenda is updating to [futures 0.2](http://aturon.github.io/2018/02/27/futures-0-2-RC/) and the [latest version of tokio](https://tokio.rs/blog/2018-03-tokio-runtime/). We also plan to take a look at our build and see if we can make our CI turnaround faster. ================================================ FILE: docs/updates/2018_05_07.md ================================================ # Update for May 7, 2018 ## Contributions [@yajamon](https://github.com/yajamon) contributed [a fix for an oversight in our build script](https://github.com/atom/xray/pull/78) where we were specifying `+nightly` even though our repository is associated with a `rust-toolchain` file. Thanks! ## First internal demo is complete As I mentioned in the last update, we focused last week on preparing for an internal demo that presented at least a tiny slice of the Xray vision in a more tangible, interactive form. We spun up a headless Xray server as a digital ocean droplet and showed off remote shared workspaces, collaborative editing, and conversations tied to the code. We also put together a few slides demonstrating Xray's performance for various tasks such as fuzzy file-finding, moving large numbers of cursors, and scrolling. The response was really positive, and we've elected to continue the experiment into the next quarter. [@as-cii](https://github.com/as-cii) and [I](https://github.com/nathansobo) will continue to focus on Xray in the coming months, and we'll get a bit of support from [@maxbrunsfeld](https://github.com/maxbrunsfeld) in order to integrate [tree-sitter](https://github.com/tree-sitter/tree-sitter) as the basis of Xray's syntax awareness. ## Into the unknown with CRDTs As [I discussed in the first update](./2018_03_05.md#anchors-and-selections), Using CRDTs in Xray's native buffer implementation allows us to create *anchors*, which are stable references to positions within a text file that maintain their logical position even after the buffer is subsequently edited. For our discussions feature, we use anchors to link each message to the range of the buffer that was selected at the time the message was sent. This allows you to select a code fragment and make a comment, then allow other participants to click on the message at some later time to jump to the code you had selected when you sent the message. For now, Xray only maintains all of this state in memory. The discussion history is lost as soon as you kill the process, and we deliberately avoid dropping buffers once they are open in order to preserve the validity of anchors. This is obviously not going to work, and to fix it, we need to figure out how to persist each buffer's operation history. If we assume that buffers are never renamed and that history only ever marches forward, this is pretty easy. But the possibility of renames and interactions with Git (or other version control systems) make it interesting. We want to track a file's identity across renames and ensure that we load the appropriate history when the user switches branches, and these concerns have a lot of overlap with some other ideas we've been pondering that can loosely be described as "real-time version control". With a proof-of-concept for shared workspaces behind us, we think it's time to explore them. Currently, we represent buffers as CRDTs. We're interested in what happens if we take that further and treat the entire *repository* as a single unified CRDT data structure that is persisted to disk. Ideally, assuming Xray is used for every edit, we will be able to maintain a keystroke-level history of every edit to every file all the way back to the moment that each file was created, sort of like an infinite conflict-free undo history. But of course, there will be many cases where files change occur outside of Xray, so we'll need to gracefully handle those situations as well. We've decided to spend the next couple weeks exploring this. We'll probably spend most of our time clarifying our thoughts in writing at first before transitioning to coding. It's unclear exactly how much gold is at the end of this particular rainbow, but it seems worth a look. ## Strike out with futures 0.2 On Friday, we spent an hour and a half upgrading `xray_core` to `futures` 0.2, only to discover that Tokio doesn't yet support that version 🙈. Luckily, it wasn't that much time wasted, but we did feel somewhat foolish for assuming that Tokio worked with it without checking first. ## Optimizations [@as-cii](https://github.com/as-cii) has been picking some low-hanging optimization fruit related to selections and editing. The [first](https://github.com/atom/xray/pull/79) is related to adding selections above and below the cursor. He's also been looking at [batching tree manipulation](https://github.com/atom/xray/tree/optimize-edit) when editing with multiple cursors, which is still in progress and is not yet associated with a PR. ================================================ FILE: docs/updates/2018_05_14.md ================================================ # Update for May 14, 2018 ## More optimizations Last week we spent a couple of days speeding up multi-cursor editing. Specifically, we wanted to take advantage of the batched nature of this operation and edit the buffer's CRDT in a single pass, as opposed to performing a splice for each range. Please, take a look at [#82](https://github.com/atom/xray/pull/82) for all the details. There is still some work to do in that area to deliver a smooth experience when editing with thousands of cursors, but we are planning to get back to it once we have fleshed out more features. ## Thoughts on further applications of CRDTs After demoing Xray to our colleagues, we got a lot of interest in how Xray's CRDT-based approach to buffers might apply to the problem of versioning generally, so we took some time to explore it last week. We were intrigued by the idea of a CRDT-based analog to Git, a kind of operation-oriented version control system that allowed for real-time synchronization among several replicas of the same working tree and persistence of all operations. After spinning our wheels quite a bit, we've concluded that we really need to get clear on the specific problems we might like to solve. They are as follows: * Replay: We'd like to allow developers to record a collaboration session and cross-reference their keystrokes to audio, so that it could be replayed later. Assuming people were willing to opt into this, it could provide deep insights into the thought processes behind a given piece of code to future developers. This use case is really all about persisting the operations, and has nothing to do with replicating the entire file tree. * Permalinks: Today we have anchors, which automatically track a logical position in a buffer even if in the presence of concurrent and subsequent edits, but these anchors are only valid for the lifetime of the buffer in memory. We'd like to be able to create an anchor that can always be mapped to a logical position at arbitrary points in the future, even thousands of commits later. Again, this has nothing to do with full replication. It's really about *indexing* the operations we persist and tracking the movement of files over time so that we can always efficiently retrieve a logical position for an anchor. * Streaming persistence and code broadcast: Today, code lives on your local machine until you save it, commit it, and push it to the cloud. We want to persist your edit history as it is typed and optionally stream it into the cloud. If your computer spontaneously combusts, your up-to-the-minute edit history is still saved on the server. If you elect for your edits to be public, colleagues or community members could watch your edit stream in real time. This would require full replication if you wanted to allow another party to make *edits* to the working tree. If the server is just storing your operations, there's really no need to deal with concurrency. It *might* be cool if someone could come along and edit the server's replica of the work tree and have their edits automatically appear in your replica, but is that actually a good user experience? Real-time collaboration requires tight coordination, so it might be jarring to receive edits from someone you didn't actively invite to your workspace. * File-system mirroring for third-party editors: We'd like to allow other editors to use Xray in headless mode as a collaboration engine. In this use case, we'd need to relay edit operations through Xray via specific APIs, but it might be helpful if Xray could mirror the state of a remote project to the local file system. That way, an exiting editor could use its ordinary mechanisms for dealing with local files to interact with the remote workspace, and wouldn't need to perform file system interactions over RPC, which would simplify integration. I wanted to think through the design implications of these various features early to determine whether any of them had an impact on Xray's core architecture, and after a lot of thinking, my conclusion is that it should be okay to defer these features for now. I had envisioned a single unified design that elegantly addressed all of these features in a single replicated structure, but now we think that that cost of building such a structure probably outweighs its benefits. For now, we've decided to defer these concerns to until the point that replay, permalinks, or streaming persistence are actually the next most important feature we want to add. Our instinct is that when that time comes, we'll be able to address these features in an additive fashion, and that it doesn't make sense to invest in adding support for them today. In retrospect, last week was a bit of a distraction. I've done more up-front design thinking for Xray than I ever have for any other project, and it's worked out pretty well overall. But after last week, I think we're approaching diminishing returns for up-front architectural design. We've validated that the current design can be performant and collaborative, and it's seeming like we've struck a nice balance between simplicity and power. Now it's time to return to a more incremental strategy and continually focus on the next-most-important feature until we have a useful editor. ## The path forward This week, we'll turn our focus to implementing save as well a simple key bindings system, which [I wrote about in a previous update](2018_03_26.md#thoughts-on-key-bindings-and-actions). We also plan to clarify our short term roadmap, and we'll post an update about that next week. ================================================ FILE: docs/updates/2018_05_28.md ================================================ # Update for May 28, 2018 ## Staying the course with CRDT-based version control In the last update, I said that we were abandoning our efforts to apply CRDTs to the entire repository, citing lack of clarity on what we were actually trying to achieve. However, after more conversations with colleagues, we've decided to proceed with that effort after all. After a lot more thinking and writing, we finally got enough clarity on our direction to start writing code last week. We still plan to continue developing Xray as a text editor, but we're adding a new top-level module to the repository called Memo, which is essentially a CRDT-based version control system that interoperates with Git. Xray will pull in Memo as a library and build directly on top of its primitives, but we also plan to make Memo available as a standalone executable in the future to support integration with other editors. Our plan is for Memo to complement Git with real-time capabilities. Like Git, Memo will support branches to track parallel streams of development, but in Memo, all replicas of a given branch will be synchronized in real-time without conflicts. For example, if you and a collaborator check out the same Memo branch, you'll be able to move a file while someones else is editing that file, and the state of the file tree will cleanly converge. Today, Git serves as a bridge between your local development environment and the cloud. When you push commits to GitHub, you're not only ensuring that your changes are safely persisted and shared with your teammates, but you're also potentially kicking off processes on one or more cloud-based services to run tests, perform analysis, or deploy to production. We want to make that feedback loop tighter, allowing you to share your changes with teammates and cloud-based services as you actively write code. With Memo, as you're editing, a CI provider like Travis could run tests across a cluster of machines and give you feedback about your changes immediately. A source code analysis service like Code Climate could literally become an extension of your IDE, giving you feedback long before you commit. Like Git, we also intend to persist each branch's history to a database, but your changes will be continuously persisted on every keystroke rather than only when you commit. After the fact, you'll be able to replay edits and identify specific points in a branch's evolution via a version vector. When we detect commits to the underlying Git repository, we'll automatically persist a snapshot of the current state of the Memo repository and map the commit SHA to a version vector. When a commit only contains a subset of the outstanding changes, we'll need a more complex representation than a pure version vector in order to account for the exact contents of the commit, since a version vector can only identify the state of the repository at a specific point in time. Last week, after getting clear on our goals, we started on a new tree implementation that we'll use to index the history of changes to the file system and text files. It's based heavily on the tree that we already use within Xray to represent the buffer CRDT, but we're modifying it to support persistence of individual nodes in an external database. This will allow us to index the entire operational history of files without needing to load that entire history into memory during active editing. Once we complete the initial implementation of this B-tree, we'll use it to build out a CRDT representing the state of the file system. ## More progress on the editor While I've been focused on getting clarity in terms of version control, [@as-cii](https://github.com/as-cii) has continued to make progress on Xray itself. Last week he merged [a PR that adds support for horizontal scrolling](https://github.com/atom/xray/pull/90) the editor, which was a bit more challenging than it might sound. To support horizontal scrolling, we need to know the width of the editor's content, which involves efficient tracking and measurement of the longest line. Previously, we maintained a vector of newline offsets as part of each chunk of inserted text to support efficient translation between 1-D and 2-D coordinates which we implemented by binary searching this vector. Antonio replaced this representation with a static binary tree, which is still stored inside a vector for efficiency. With the binary tree, we maintain the same offset information that was formerly available in the flat vector, but we also index maximal row lengths, which gives us the ability to request the longest row in an arbitrary region of the text in logarithmic time. I'll be out next week on vacation, so Antonio plans to focus primarily on more editor features until I'm back on Monday, June 4th. He'll start with rendering a gutter and line numbers, which he already got started last week. In light of my absence, there's a good chance we could go another 2 weeks before the next update. Thanks for your patience. ================================================ FILE: docs/updates/2018_07_10.md ================================================ # Update for July 10, 2018 It's been a while since the last update, and I apologize for that. Our strategic direction has felt less clear to me over the past few weeks, and that lack of clarity combined with some difficulty in my personal life overcame my motivation to post for a while. I just wanted to turn inward and write code in relative isolation. Things are clearer and I'm feeling better, and I'd like to resume posting updates on a weekly basis and ask your forgiveness for the gap in communication. ## The emergence of Eon When we demonstrated Xray for GitHub leadership in May, there was definitely interest in Xray's potential as a high-performance collaborative text editor that runs on the desktop or in the browser, but there was way *more* excitement about CRDTs and their potential to impact version control. At first, this feedback caused some cognitive dissonance for me. After working so hard on Xray, it wasn't easy to hear that what I considered to be an implementation detail was the most exciting aspect of what we had built. But the more I thought about it, the more intrigued I became with the application of CRDTs to version control. The idea had been floating around in my mind since early in the development of Teletype, but now I felt encouraged to take the idea more seriously. After a bit of indecision, we decided to dive in. We've now shifted our focus to a new project called Eon, which enables real-time, fine-grained version control. Long term, we see Eon and Xray as two components of the same overall project. Eon will be an editor-agnostic datastore for fine-grained edit history that enables real-time synchronization. It will be like Git, but it will persist and synchronize changes at the granularity of individual keystrokes. We envision Xray as Eon's native user interface and the best showcase of its capabilities. One example is the idea of "layers", which are like commits that can be freely edited at any time. Git never would have taken off if it had been trapped inside a particular editor, and so if we really want to maximize the utility of what we're building, it makes sense to be editor-agnostic at the core. That's why we've decided to focus on delivering Eon as a standalone project. It may look like we have stopped working on Xray, but since Xray will ultimately build on top of Eon, the spirit of the overall project continues. Since I was presenting Eon at Qcon NYC, we briefly decided to pull out Eon into a separate repository, but then we decided that this was actually a bad idea. For now, we will [continue to develop Eon within the Xray mono-repo](https://github.com/atom/xray/tree/eon/eon) in order to keep the community and development focused in a single location. ## Progress on Eon Previously, Xray's allowed you to invite guests into your workspace, but it was a centralized design. The workspace host owned all the files and serialized all guest requests to manipulate the file system. If the host dropped offline, the collaboration was over. With Eon, we're shooting for full decentralization. Multiple people can maintain a first-class replica of a given repository, just like Git. To achieve that, over the past few weeks, we've been working on replicating the contents of the file system in addition to individual buffers. That means that if one person moves a directory while a collaborator adds a file inside of it, both parties will eventually converge to the same view of the world. It's proven to be a surprisingly complex problem. We maintain a CRDT that represents the state of all the files and directories within the repository, but the only cross-platform way to detect file system changes is to scan the underlying directory structure and compare it to our in-memory representation. So far, we've focused only on directories, and we're caching inodes so we can detect when a directory is moved. We have yet to deal with files, which add the possibility of multiple hard links to the same file, but we're planning for them in our design. We also still need to deal with the fact that the file system might change in the middle of a scan, which might cause us to encounter a file or directory multiple times. Once we detect a local change, we update the local index and create an operation to broadcast to other replicas. We've settled on a design in which each file or directory is assigned a unique identifier and associated with one or more *parent references*, which describe where that file is located in the tree. Directories can only have one parent reference since they cannot be hard linked, but files can have multiple. Additionally, directories are associated with *child references*, each of which has a name and corresponds to a parent reference elsewhere in the tree. Each parent and child reference is a simple CRDT called a *last-writer wins register*. If a file is moved, we update its parent reference. If the same file is moved concurrently on another replica, we break the tie in a consistent way such that the file ends up in the same location in all replicas. Similarly, if two child references with the same name are created concurrently within a directory, only one of them will win across all replicas. Inspired by the [Btrfs file system](https://en.wikipedia.org/wiki/Btrfs), we're storing the state of the file system in the same copy-on-write B-tree that we use to represent the contents of buffers. Our tree is implemented generically, enabling us to reuse the same code for different kinds of items. In the case of our file system representation, each item is a member of an enumeration, which allows us to store file metadata, parent references, and child references all within the same tree. Each parent and child reference is actually represented by multiple tree items that share a *reference id*. We enforce a total order between all items in the tree, honoring the leftmost item for any register as the current value of that register. We've also enhanced Xray's original B-tree to allow nodes to be persisted in an external key-value store. This will allow us to maintain a history of how the file system has evolved, and we plan to allow interactions with our tree to filter out certain nodes based on a summary of their contents. This will enable us to avoid loading portions of the tree that contain items that aren't visible in a specific version of the tree, which will keep the memory footprint small for any single version while still allowing us to load past versions of the tree if desired. In many ways arriving at our current approach was more challenging than coming up with the CRDT for text. We spent many days doing almost nothing but thinking and not writing much code, but now we're feeling pretty good about the design. It seems simple and almost obvious, which is probably a good sign that we're on the right track. ================================================ FILE: docs/updates/2018_07_16.md ================================================ # Update for July 16, 2018 ## Breaking cycles This week, we continued our focus on a fully replicated model of the file system. We're still focusing on directories only, driving our work with an integration test that randomly mutates multiple in-memory replicas of a file system tree and tests for convergence. Mid-week, we hit a pretty major snag that we hadn't anticipated, but seems obvious in retrospect. Say you have two replicas of a tree that contains two subdirectories, `a` and `b`. At one replica, `a` is moved into `b`. Concurrently, on the other replica, `b` is moved into `a`. When we exchange operations, we end up with both directories in an orphaned cycle, with `a` referring to `b` as its parent and `b` referring to `a` as its parent, a state which we can't mirror to the underlying file system of either replica. | Time | Replica 1 State | Replica 2 State | |:-----| :-------------- | :------------------ | | 0 | `a/` `b/` | `a/` `b/` | | 1 | `a/b/` | `b/a/` | | 2 | ??? | ??? | For any set of concurrent moves, it's possible to create a cycle, and you could potentially create *multiple* different cycles that share directories in certain diabolical cases. Left untreated, these cycles end up disconnecting both directories from the root of the tree. We still have the data in the CRDT, but it can't be accessed via the file system. We need to break them. We spent the second half of this week thinking about every possible approach to breaking the cycles while also preserving convergence, and we ended up arriving at two major alternatives. The first approach is to preserve the operations that create the cycle, but find a way to break the cycle when we interpret the operations. The trouble is that cycles are always created by concurrent operations, but because this is a CRDT, it's possible for concurrent operations to arrive in different orders at different replicas. This means a decision to break a cycle is order-dependent, and may need to be reevaluated upon the arrival of a new operation. Our best idea is to create an arbitrary ordering of all operations based on Lamport timestamps and replica ids. When a new operation is inserted anywhere other than the end of this sequence, we integrate it and then reinterpret all subsequent operations based on a state of the tree that accounts for the new operation. It's definitely doable and preserves the purity of the CRDT, but it also seems complex and potentially slow. It also means that we could end up synthetically breaking a cycle only to determine later that we don't need to break the cycle due to the arrival of a concurrent operation. This could cause seemingly unrelated directories to appear out of nowhere upon the arrival of a concurrent operation, which could be pretty confusing depending on the integration delay. We'd like Eon to generalize to async use cases in addition to real-time, and these "phantom directories" seemed like a real negative for usability. The second approach, which we've decided to go with, is sort of a principled hack. Whenever we interpret a move at a given replica that introduces a cycle, we look at every move operation that contributed to the cycle and synthesize a new operation that reverts the operation with the highest Lamport timestamp. We then broadcast this new operation to other participants. Depending on the order that various concurrent operations arrive at different replicas, we may end up reverting the same move redundantly or reverting multiple moves that participate in different variations of the same cycle. We considered this approach within the first hour of our discovery of the issue, but initially discarded it because it seemed to violate the spirit of CRDTs. It seems weird that integrating an operation should require us to generate a new operation in order to put the tree in a valid state. But after fully envisioning the complexity of the pure alternative, synthesizing operations seemed a lot more appealing. Breaking cycles via operations means that once a replica observes the effects of a given cycle being broken, they'll never see it "unbroken" due to the arrival of a concurrent operation. It also completely avoids the issue of totally ordering operations and reevaluating subsequent operations every time an operation arrives. One consequence of either approach is that there could be certain combinations of operations that lead to a cycle that we never detect and break. That means that certain version vectors might yield tree states containing cycles and constrains the set of version vectors we should consider valid. This isn't a huge deal, because even without cycles, the constraints of causality already limit us to a subset of all possible version vectors if we want a valid interpretation of the tree. For example: If replica 0 creates a directory at sequence number 50 and replica 1 adds a subdirectory to it at sequence number 10, the state vector `{0: 20, 1: 10}` would contain a directory whose parent doesn't exist. If we limit ourselves to version vectors corresponding to actual states observed on a replica, we will have no problems. ## Homogenous trees As I discussed in the previous update, we currently represent the state of the file tree inside a B-tree with heterogenous elements. Each tree item is either metadata, a child reference, or a parent reference. Now I'm realizing this is probably wrong. If we separated metadata, parent references, and child references into their own homogenous trees, we could probably simplify our code, reduce memory usage, and perform way pattern matching on the various enumeration variants. We plan to try separating the trees this week. ## Conclusion For whoever is reading these updates, thanks for your interest. We're always interested in thoughts and feedback. Feel free to comment on this update's PR if there's anything you'd like to communicate. ================================================ FILE: docs/updates/2018_07_23.md ================================================ # Update for July 23, 2018 ## Contributions [@MoritzKn](https://github.com/MoritzKn) [fixed a bug](https://github.com/atom/xray/pull/115) where we were incorrectly calculating the position to place the cursor when inserting strings containing multibyte characters. Thanks! ## Convergence for replicated directory trees Late last week we were able to achieve convergence in our randomized tests of replicated directory trees. As I mentioned in the last update, the biggest challenge was the possibility of concurrent moves introducing cycles. Our proposed solution of breaking cycles via synthetic "fixup" operations worked out well, but determining exactly *which* fixup operations to generate was still a challenging problem. In certain diabolical scenarios, reverting a move to break one cycle could end up introducing a second cycle. By reverting *multiple* moves, however, it should always be possible to end up with a directory tree that is free of cycles, and so that's what we do. Whenever a cycle is detected, we continually revert the most recent move that contributes to that cycle, ignoring any moves that have already been reverted. Eventually, we're guaranteed to end up with a tree that's free of cycles. Though we don't have a formal proof to back up our intuition, [we've been unable to find a failing scenario over a million randomized trials](https://github.com/atom/xray/blob/6c49587aad45d7880449668e4b882267435ff763/eon/src/fs2.rs#L1523), and we're ready to move forward. We applied the same "fixup" strategy to recover from directory name conflicts as well. When we attempt to insert a directory entry whose name conflicts with an existing entry, we compare the entries' Lamport timestamps and replica ids to select an entry that gets to keep the existing name. For the other entry, we append `~` characters until we find a name that does not conflict and synthesize a rename operation. In a real-time scenario, this situation should almost never occur, but if it does, renaming one of the directories means we can mirror the state of the CRDT to the file system without losing data. The users can then decide how to deal with the situation by deleting one of the directories or merging their contents. ## Interacting with the file system For our convergence results to be useful outside the realm of automated tests, we need to communicate changes to and from the file system. That presents its own set of challenges, since we can't rely on our internal representation always being perfectly synchronized with the state of the disk. After confusing ourselves a bit too much trying to devise a strategy for file system synchronization that could cover every possible scenario, we've decided to focus on a few narrowly-defined situations on the critical path to a working demo. * Read a tree into a new index: When the Eon daemon starts, we will need to read the current state of the tree into our internal representation. * Write an index to a file system tree: When you want to clone a remote replica, we need to write its initial state to your local disk. * Update an index from a tree: Once the daemon is started, we want to watch the file system for changes. When we detect a change, we will scan the directory tree to determine which directories have been inserted, removed, or moved. * Write incoming operations to the disk: As operations come in, we interpret them relative to our internal index and translate them into writes to the file system. For now, we've decided to rely on the fact that files and directories get associated with unique inode numbers in order to detect moves. In our previous attempt, we were hoping to not fully rely on inodes in hopes of covering cases such as the entire repository being recursively copied or another system like Git manipulating the file tree. Now we've decided we will deal with those scenarios in a separate pass once we get the basic scenario working. Tracking the mapping between our internal file identifiers and inodes makes everything much simpler. One thing that makes it challenging (if not impossible) to mirror changes to the file system perfectly is the inability to perform file system operations atomically. When we receive a move operation from the network, we'll resolve abstract identifiers in the operation to actual paths on the local disk. If the disk's contents have changed in the meantime and we haven't heard about it, there's a potential for these paths to be wrong. To mitigate this issue, we will always confirm that the relevant paths exist and have the expected inode numbers before applying a remote operation. If we detect that our index has fallen behind the contents of the disk, we will defer handling the operation until the next update. However, even if we determine that our index is consistent with the disk, this determination isn't atomic. In the microseconds between checking for consistency and performing the write, another change might invalidate our conclusion and cause the operation to fail. Worse, a change might cause the same paths to point to different inodes, meaning the operation would succeed but apply to different paths. Luckily, we anticipate this sort of situation to be extremely rare. It could only happen if a file at a given path was replaced with another in the moment between our consistency check and actually writing the operation. It might lead to surprising results, but we don't think the consequences are catastrophic. Dealing with all of these problems and getting changes to and from the file system will be our focus for this week. ================================================ FILE: docs/updates/2018_07_31.md ================================================ # Update for July 31, 2018 ## Contributions [@Aerijo](https://github.com/Aerijo) encountered some confusion and took it upon himself to [update our contributing guide](https://github.com/atom/xray/pull/118) to ensure others wouldn't suffer the same fate. We really appreciate these kinds of improvements. ## Batched conflict resolution As I mentioned in last week's update, having achieved convergence for replicated directory trees, last week we started down the path of mirroring changes from our internal CRDT-based representation to the underlying file system. After implementing file system reads and starting on randomized tests, we quickly realized that our previous mental model was incomplete. In our previous tests of convergence, we applied operations to our in-memory representation one at a time, moving, inserting, and deleting each directory in serial. However, when scanning changes from the disk, this serial approach is impossible. We only see a snapshot of the file system's latest state, which could have been produced by a variety of different sequences of individual operations. Consider the following directory structure, with two different directories that are both named "b". We'll label them `b(1)` and `b(2)` in our example to clearly identify them: ``` a/ a/b(1) b(2)/ ``` The next time we scan the file system, we observe that the directory structure has changed to the following: ``` a/ a/b(2) b(1)/ ``` The two directories named `b` have swapped their positions. If we naively apply the operations derived from this swap one at a time on a remote replica, we'll end up creating name conflicts. As I've discussed previously, we resolve name conflicts created by concurrent operations by appending a tilde character to one of the conflicting names. But in this case, appending a tilde would be incorrect, because the final state of the tree that we are trying to produce contains no actual conflicts. To avoid spurious conflict resolutions, we moved from resolving conflicts after each operation to resolving conflicts after applying arbitrary batches of operations. It took a couple days to iron out all of the new issues and edge cases with this new approach in randomized testing, which took us until last Thursday. Finally, we managed to achieve convergence with the new approach to conflict resolution in a million randomized trials of 5 different peers applying 20 operations. ## Batched writes to the file system The batched nature of operation application presented a puzzle for file system writes as well. Previously, we had planned on applying the effects of each operation as it arrived, but now we realized that wouldn't work. We needed to apply a batch of operations to the tree, resolve conflicts, and *only then* write changes in the new state of tree to the file system. Unlike our internal representation, which can temporarily tolerate intermediate states containing conflicts and cycles, each operation applied to the file system must ensure that the tree remains acyclic and free of name conflicts. We ended up converging on the following approach: We maintain a set of the internal identifiers of all files we have inserted, moved, or removed in the course of applying a batch of operations. We then sort insertions and moves by the depth of the inserted path in the new tree and sort deletions last. By performing shallower insertions first, we ensure that the parent of any directory we are trying to insert always exists. By performing shallower moves first, we ensure that we don't accidentally create cycles while rearranging directories. We don't have a formal proof, and we may need more empirical verification to be completely confident, but the intuition is as follows: A cycle can only be created by moving a directory downward to become one of its own descendants. Because we've broken cycles in the new tree, we should never encounter a situation in which a directory has been moved to become its own descendant in the final state of the new tree. A combination of moves could end up creating a cycle momentarily, but this cycle could only be created by moving a directory deeper in the tree. If we perform upward moves first, by the time we would be attempting to move a directory into one of its own descendants, we should have already moved that descendant to an equal or shallower depth. At least that's our intuition, and evidence so far is that it works. Finally, we need to deal with temporary name conflicts that can occur when directories are shuffled around. We've opted to take an extremely simple approach. When performing a move on the file system would create a name conflict, we append tildes until we find a free name and record the fact that we have done so. When all operations have been applied, we go back and clean up, renaming directories with appended tildes back to their desired names. At this point, all of the conflicts should be resolved, and so we can do this without risk of conflict. ## Dealing with concurrency The above approach worked in randomized trials at the end of last week, but we knew we were only solving part of the problem. Our initial implementation assumed that we were the only process writing to the file system. In reality, the file system can change out from under us at any time, meaning that we could be attempting to update the file system based on an outdated understanding of the file system's state. To deal with this, before we integrate a batch of operations into our tree, we clone the tree's current state and as the `old_tree`. This represents our best guess as to the current state of the underlying file system. We then update the new tree, resolve conflicts, and start writing. For each file we need to update, we use the `old_tree` to determine the current location of the relevant directories on disk. Assuming a directory still exists at the path in question, we compare inode numbers to ensure it has the proper identity. Assuming our understanding of all the relevant paths is up to date, we can proceed with the file system write and update the `old_tree` accordingly. If anything goes wrong, such as the path not existing, the path's inode not matching, or the write operation returning some kind of error, we need to pause the entire process and update our understanding of the old tree via a file system scan. As we integrate changes to the old tree, we produce operations which need to be applied to the new tree. Moves, deletions or conflict resolutions could end up changing the nature of operations we have still yet to write, requiring us to refresh and re-sort our pending writes after the old tree is updated. At the time of writing, we have yet to achieve convergence in the presence of full concurrency with the underlying file system, but it seems like we are getting close. Hopefully we'll get there by the end of this week. ## Q3 Demo Our focus during Q2 has been figuring out how to achieve optimistic replication on the entire file system as well as persistence of all operations, and we've nearly done it. Once we achieve this abstraction, we plan to shift our focus to showcasing its capabilities in a new demo. We're still not clear on the details, but the basic idea is that you should be able to open a repository in Xray, then open a "streams" panel to view the latest state of all other working copies of that repository from other developers working in Xray, whether or not they are currently online. If a stream is being actively edited, you'll be able to collaborate. If that stream's author is offline, you'll be able to pick up where they left off. You'll also be able to fork a stream, though we probably won't finish merging before the end of the quarter. We feel confident we can achieve that basic experience, but if we have time, we'd like to restore the conversation panel now that we will be able to persist anchors over the lifetime of a repository. We'd also like to find other ways to show off our operation-level history, such as the ability to play back operations. We still plan for Eon (or whatever we end up calling it; I'm not sure if I like the name) to be a standalone tool that can integrate with other editors. But we need to drive its development with a real product experience, and the best way to do that is by producing a working demo. ## Vacation Antonio is on vacation this week and next week, and I'll also be out next week to spend some quality time with my family. Due to this, expect a 2-week communication gap. We'll come back recharged to slash through randomized test failures and produce a demo of a whole new approach to collaboration. Thanks for reading! ================================================ FILE: docs/updates/2018_08_21.md ================================================ # Update for August 21, 2018 ## *Eon* is now *Memo* I chose the name *Eon* fairly hastily and ended up kind of disliking it. I wanted to change it almost immediately, but decided to hold off until I felt sure about its replacement. *Memo* is one character longer but just sounds better to me and reflects the system's ability to record every keystroke. It's kinda silly to worry this much about a name, but I just needed to change it. Now it's done. Moving on. ## Convergence for directory trees The bigger news is that we've finally achieved convergence in our randomized tests of replicated directory trees. The problem ended up being way harder than we imagined. The final challenge was to fully simulate the possibility for the file system to change at any time, including during a directory scan. We are cautiously optimistic that the worst of the algorithmic challenges could be behind us. Weeks of wading through randomized test failures has been a bit monotonous, but hopefully we can pick up some momentum building on top of this abstraction. ## Supporting text files and evolving the high-level structure The next step is to add support for files to the directory tree, which we think should be easier. Much of what we learned dealing with pure directories can be applied to files, and since files are always leaf nodes we shouldn't need to deal with cycles. We *do* need to deal with hard links, however, which should add some complexity. Supporting files also means we need to figure out the relationship between the CRDT that maintains the state of the directory tree and the CRDTs that contain the contents of individual text files. This week seems like the right time to zoom out and get a bit more clarity on the system's higher level design. Until we had a working CRDT for directory trees that felt premature, but now it seems like understanding the big picture a bit better might inform the relationship between the directory tree and individual text files. We've gone back and forth on whether we should try to decouple them, but for now we think we're going to try a more integrated approach where the directory tree CRDT has explicit knowledge of the file CRDTs. For now, we've decided to wrap both concerns in a single type called a `Timeline`, which will represent the full state of a working tree as it evolves forward in time. A `Repository` will contain multiple timelines which can evolve in parallel, fork, and eventually merge. There's still quite a bit to figure out though. How will we route operations to and from buffers? What will the ownership structure look like? How can we ensure that performing I/O doesn't interfere with the responsiveness of the system? We'll hopefully have some conclusions about those questions and more to share in the next update. ================================================ FILE: docs/updates/2018_08_28.md ================================================ # Update for August 28, 2018 ## Convergence for files and hard links As predicted in the [last update](./2018_08_21.md), adding support for files and hard links to our directory tree CRDT went smoothly, and we achieved convergence in our randomized tests on Monday. Because hard links make it possible for the same file to appear in multiple locations, many code paths needed to be updated to work in terms of *references* rather than files. Happily, we had already anticipated hard links by allowing a file to be associated with multiple parent refs, so the path was mostly paved. Once we add support for file contents and confirm that everything works in an end-to-end test, we plan to post an in-depth write-up on the directory tree CRDT and do a documentation pass on the [timeline module](../../memo/src/timeline.rs). ## Next up, buffers The file support added last week assumes that all files are empty. To allow files to be associated with editable content, we're adapting the [`buffer`](../../xray_core/src/buffer.rs) module from `xray_core` to work with Memo's [new B-tree](../../memo/src/btree.rs). The primary difference between the previous B-tree implementation and the new one is support for storing the tree's nodes in a database. This will allow us to store a file's entire history without loading old fragments into memory, but it also means that many methods now have the potential to perform I/O with the database and encounter I/O errors. We'll need to adjust the `Buffer` APIs slightly to account for this potential. For example, we can no longer return an iterator that implements the `Iterator` trait, since `next` would need to return a `Result` type. We're also dropping some of the previous buffer's support for Xray's RPC system because we anticipate dealing with network interactions differently in Memo. We don't have complete clarity on our plans for dealing with networking just yet, but it makes sense to keep our assumptions minimal at this stage. Once we get buffers implemented against our new B-tree, we'll need to integrate them into our timeline. We plan to maintain a mapping between file ids and the buffers that contain their contents, but the details will become clearer once we get into it. Buffers will need to be integrated with up to three distinct sources of I/O: the file system for reading/saving contents, the network for collaboration, and the database for history persistence. It should be a fun design problem to give them a convenient API while addressing all of those concerns. ================================================ FILE: docs/updates/2018_09_14.md ================================================ # Update for September 14, 2018 It's been an intense couple of weeks, but we're coming out of it with more clarity than ever on the future direction of Memo. We're entering a new phase of the project where we distill the research of the last few months into a usable form. Thanks for your patience with the radio silence the last couple of weeks. ## Embracing Git Our previous vision for Memo was to store the full operational history for the repository in a global database, so that each file's full history would be available in a single structure dating back to its creation. This would essentially duplicate the role of Git as a content tracker for the repository, but with a much more fine-grained resolution. It may eventually make sense to build a global operation index to enable advanced features and analysis, but I don't think it makes sense to conceive of such an index as an independent version control system. For async collaboration, CRDTs probably won't offer enough advantages to induce people to switch away from Git. Even if we managed to build such a system, it would always need to interoperate with Git. So we may as well embrace that reality and build on top of Git. We can then focus on the area where CRDTs have their greatest strength: real-time collaboration and recording the fine-grained edits that occur *between* Git commits. Augmenting Git is definitely something I've considered in the past, but it's finally becoming clearer how we can achieve it. We will start by packaging the previous months' work into a library that is similar to `teletype-crdt`. With Teletype, you work with individual buffers. Each local edit returns one or more operations to apply on remote replicas, and applying remote operations returns a diff that describes the impact of those operations on the local replica. Memo will expand the scope of this abstraction from individual buffers to the working tree, but it won't represent the full state of this tree in the form of operations. Instead, we'll exploit the fact that Git commits provide a common synchronization point. The library will expect any data that's committed to the Git repository to be supplied separately from the operations. By making the CRDT representation sparse and leaning on Git to synchronize the bulk of the working tree, we reduce the memory footprint of the CRDT to the point where it can reasonably be kept resident in memory. This also bounds the bandwidth required to replicate the full structure, which obviates the need for complex partial data fetching schemes that we were considering previously. This in turn greatly simplifies backend infrastructure. Because a sparse representation should always be small enough to fully reconstruct from raw operations on the client, server side infrastructure shouldn't need to process operations in any way other than simply storing them based on the identifier of the patch to which they apply. ## The challenge of undo One big obstacle to making this patch-based representation work is undo. In `teletype-crdt`, we implement undo by associating every operation with a counter. If an operation's undo counter is odd, we consider the operation to be undone and therefore invisible. If the operation's undo counter is even, we consider the operation to be visible. If two users undo or redo the same operation concurrently, they'll both assign its undo count to the same number, which preserves both users' intentions of undoing the operation and avoids their actions doubling up or cancelling each other out, which could occur in some other schemes. However, implementing undo in this way comes with a cost, which is that in order for me to undo an operation that is present in my local history, I need to rely on that operation being present in the history of all of my collaborators. This approach to undo combines poorly with resetting to an empty CRDT on each commit, because it forces everyone to clear their undo stack after committing since there won't be any way to refer to prior operations in order to update their undo counters. This felt like a show-stopper to me until I had a conversation with [@jeffrafter](https://github.com/jeffrafter) about his team's experience using `teletype-crdt` in [Tiny](https://tttiny.com/). I don't have a perfect understanding of the details of their approach, but they essentially bypass Teletype's built-in undo system and maintain their own history on each client independently of the CRDT. When a user performs an undo, they simply apply its to the current CRDT and broadcast it as a new operation. When I asked about some of the more diabolical concurrency scenarios that the counters were designed to eliminate, Jeff simply replied that it's working for them in practice. Inspired by their experience, I have a hunch that we can implement undo similarly in our library. For each buffer, we can maintain two CRDTs. One will serve as a local non-linear history that allows us to understand the effects of undoing operations that aren't the most recent change to the buffer. We'll perform undos against this local history first, then apply their affects to a CRDT that starts at the most recent commit. This will generate remote operations we know can be cleanly applied by all participants. The local history can be retained across several commits and even be stored locally. By fetching operations from previous commits, we could even construct such a history for clients that are new to the collaboration. ## Stable file references We need to be able to refer to files in a universal way, but with this hybrid approach, only *new* files are assigned identifiers by operations. This stumped us for a bit, until an obvious solution occurred to us. The set of paths in the base Git commit is the same for every replica, so we can sort these paths lexicographically and assign each an identifier based on its position in this fixed order. Internally, file identifiers are a Rust enum with two possible variants, `Base`, which wraps a `u64`, and `New`, which wraps a local timestamp generated on the replica that created the file. ## The big picture By being agnostic to plumbing and building a library that operates purely in terms of operations and data, this software should be useful in a broader array of applications. We plan to distribute a WebAssembly version to enable collaboration in browser-based environments, along with a native executable that can talk to editors and synchronize our CRDT with an underlying file system like we originally envisioned. The operations can serve as a kind of common real-time collaboration protocol. As long as an application can send and receive operations and feed them into this library, it should be capable of real-time collaboration with other applications. In light of these shifts in our thinking, I've updated the [Memo README](../../README.md) to reflect the current state of the world. Some details about the implementation have been dropped, but I plan to reintroduce them over time as our implementation stabilizes. At some point soon, it may make sense to again pull Memo out into its own repository that is separate from Xray. If that happens, I'll keep everyone posted here. ================================================ FILE: docs/updates/2018_10_02.md ================================================ # Update for October 2, 2018 ## Shipped an initial light client Last week, we [shipped](https://github.com/atom/xray/pull/135) an initial version of Memo JS, a light-client implementation of Memo that can be used as a library in web-based applications. To start with, we're assuming that the file system is completely virtual and that all changes are routed directly through the library. This meant that we ended up temporarily shelving a lot of the work we did to synchronize our tree CRDT with an external file system, but we still plan to take advantage of that research in order to build the full client that's capable of observing an external repository. Shipping the light client first will hopefully let us get some feedback and iterate on other aspects of the protocol's design before introducing the complexity of interoperating with an external file system. ## Next, Git operations Currently, a Memo `WorkTree` always starts at a base commit and builds forward indefinitely with operations. We assume that application code will be responsible for tearing the work tree down and rebuilding it following a commit. The next step is pull this concern into Memo itself and to allow the base commit of a replicated work tree to *change* over time due to operations on the underlying repository such as committing, resetting, and checking out different branches. We're still in the middle of figuring this out. It's murky and our thinking is still in flux. We're focused on the light client currently, which simplifies our API and reduces complexity, but we still want a design that will work when we do eventually synchronize to the file system. It's somewhat unclear whether we should just start focusing on integrating with the file system now, or alternatively completely ignore the concerns of the file system and hope we can make adjustments later. For now though, here's what is emerging. ### Epochs We divide the evolution of the work tree into *epochs*. Each epoch begins with a specific commit from the underlying repository that gives all replicas a common frame of reference, then applies additional operations on top to represent uncommitted changes in that epoch. There is one and only one *active epoch* at any time on a given replica. All operations are tagged with an epoch id, and the local counters used to identify operations are reset to zero at the start of each epoch. Someone joining the collaboration should only need to fetch operations associated with the most recent epoch. When a user performs a local Git operation such as a commit or a reset, they broadcast the creation of a new epoch. Because users can create new epochs concurrently, we always honor the epoch creation with the most recent Lamport timestamp at every replica, which will provide an arbitrary but consistent behavior for concurrent epoch creations while also respecting causality in the sequential case. ### Resets Collaborators can reset the HEAD of the working copy to an arbitrary ref. In that case, we need to create a new epoch. Depending on the nature of the reset and the state of the file system, there may be uncommitted changes on disk. We'd also like to incorporate the concept of unsaved changes when we integrate with the file system. Both uncommitted changes and unsaved changes will need to be translated into synthetic operations that build upon the new epoch's base commit. When the epoch creation arrives at remote replicas, it seems like they will have no choice but to perform I/O in order to scan the epoch's base entries into the tree. The base state of open buffers may also need to be re-read, and some of these open buffers may be for files that no longer exist in the new epoch's base commit. This is where things start to feel pretty messy and confusing. What happens to these "untethered" buffers? Do we empty out the tree and build it back up as we perform I/O on the base entries, or do we preserve the old state until the new state is ready. How do races with the file system complicate all of this? ### Commits Commits create a new epoch whose state is derived from a previous epoch, although due to the potential for concurrent commits and resets, a commit doesn't always derive from the active epoch on a given replica. Ignoring the potential for partial staging for the moment, when a user creates a commit, we can characterize what they committed via a version vector that includes all observed operations in the current epoch. If a replica receives a commit based on the active epoch (which should be the most common case), we should be able to determine their base entries without performing I/O. This is because the state that was committed should already be available as a subset of operations they have already seen, as characterized by the version vector. This would allow us to update the tree to its new state synchronously in a very common case. On the other hand, there's no guarantee that a commit is going to based on the active epoch thanks to diabolical concurrency scenarios, and this seems to mean that we may end up needing to do I/O anyways in some scenarios. That makes us wonder whether we should focus first on the ability to reset the base commit in arbitrary ways and treat commits as a special case of that. ## Conclusion This is a hard problem. We've made it through one wave of complexity to encounter another, and presumably that will continue. Every decision seems to be entangled with everything else, and even this summary just scratches the surface of the thought process behind this problem. But despite the daunting complexity, I'm still excited by the idea of a fully-replicated Git working copy. Git operations are the next summit to climb, and I imagine there will be more wilderness before we can settle in the fertile valley of conflict free replicated paradise. ================================================ FILE: memo_core/Cargo.toml ================================================ [package] name = "memo_core" version = "0.1.0" authors = ["Antonio Scandurra ", "Nathan Sobo "] edition = "2018" [dependencies] diffs = "0.3" lazy_static = "1.0" flatbuffers = "0.5" futures = "0.1" serde = "1.0" serde_derive = "1.0" smallvec = "0.6.1" uuid = { version = "0.7", features = ["serde"] } [dev-dependencies] futures-cpupool = "0.1" rand = "0.3" uuid = { version = "0.7", features = ["serde", "u128"] } ================================================ FILE: memo_core/README.md ================================================ # Memo – Real-time collaboration for Git **This project is a work in progress. This README defines the vision, but it isn't fully implemented yet.** On its own, Git can only synchronize changes between clones of a repository after the changes are committed, which forces an asynchronous collaboration workflow. A repository may be replicated across several machines, but the working copies on each of these machines are completely independent of one another. Memo's goal is to extend Git to allow a single working copy to be replicated across multiple machines. Memo uses conflict-free replicated data types (CRDTs) to record all uncommitted changes for a working copy, allowing changes to be synchronized in real time across multiple replicas as they are actively edited. Memo also maintains an operation-based record of all changes, augmenting Git's commit graph with the fine-grained edit history behind each commit. Memo is divided into the following major components: * Protocol: Memo can be thought of as a protocol for real-time change synchronization between working copies that will eventually be open for anyone to implement. * Library: Memo provides a reference library implementation written in Rust that produces and consumes the Memo protocol messages to synchronize working trees. We plan to ship a "light client" version of the library that compiles to WebAssembly and exposes a virtual file system API, as well as a full version based on Libgit2 that synchronizes with a full replica on the local file system. The libraries could be used in web- or desktop-based editors to enable real-time collaboration on a shared working copy of a Git repository. * Executable daemon: Memo will provide an executable (also written in Rust) that runs as a daemon process on the local machine. It will synchronize with an underlying file system and expose an RPC interface to support integrations with a variety of editors for collaborative buffer editing. * Xray: Memo spun out of Xray, which was an experiment to build a collaborative text editor. After the library stabilizes, we may decide to resume development of Xray as a first-class collaborative editor that is designed with Memo in mind. For now, we view the development of the more generalized technology as more important than building a new editor. Interesting features / design priorities are as follows: * Based on Git: When it comes to async collaboration and coarse-grained change synchronization, it's hard to beat Git. Memo doesn't try. Our goal is to enable Git users to share a single working copy and relay changes in real time. We may implement the ability to "fork" the state of a working copy, but we don't plan to implement asynchronous features such as branching and merging in terms of conflict-free replicated data types. For that you will continue to use Git. We will strive not to send or store any data that can already be derived from the state of the Git repository. * Distributed: Like Git, Memo is fully distributed. This means that no replica is privileged over any other. No specific network topology will be enforced by our core algorithms and it will be possible to disseminate operations in arbitrary ways. * Covers the whole working tree: Memo will merge concurrent edits to files along with modifications of the file system tree. One person can edit a file while another person moves it to a new directory, etc. * Open and general purpose: We want Memo to feel similar to Git, a tool that can be integrated in a variety of workflows and environments. We may build more centralized experiences on top of it, but the core protocol should remain open and decentralized. * More than just the source code: One of Memo's primary use cases is real-time collaboration, but effectively collaborating on source code often requires support from the environment to compile, run tests, statically analyze, etc. We intend to extend Memo's protocol to support primitives such as streams and shared buffers, which could support log output or a shared terminal, and annotations, which could support static analysis. An ideal scenario might see two developers with full replicas collaborating with a third developer in a browser, all viewing diagnostics generated by a language server running against a replica in the cloud and viewing test output from another machine. A fundamental goal is to make the distinction between physical machines less relevant during the actual process of writing code. Today, most code is developed locally, while some code may be developed in cloud-based IDEs. It shouldn't actually matter *where* the working tree is located, and it might be replicated to multiple machines simultaneously which are all contributing something to the overall experience of the participating developers. ================================================ FILE: memo_core/rustfmt.toml ================================================ edition = "2018" ================================================ FILE: memo_core/script/compile_flatbuffers ================================================ #!/bin/bash flatc --rust -o src/serialization src/serialization/schema.fbs # Workaround for incorrect code generation by flatc echo "use flatbuffers::EndianScalar;" >> src/serialization/schema_generated.rs ================================================ FILE: memo_core/src/btree.rs ================================================ use smallvec::SmallVec; use std::cmp::Ordering; use std::fmt; use std::ops::{Add, AddAssign}; use std::sync::Arc; #[cfg(test)] const TREE_BASE: usize = 2; #[cfg(not(test))] const TREE_BASE: usize = 16; pub trait Item: Clone + Eq + fmt::Debug { type Summary: for<'a> AddAssign<&'a Self::Summary> + Default + Clone + fmt::Debug; fn summarize(&self) -> Self::Summary; } pub trait KeyedItem: Item { type Key: Dimension; fn key(&self) -> Self::Key; } pub trait Dimension: for<'a> Add<&'a Self, Output = Self> + for<'a> AddAssign<&'a Self> + Ord + Clone + fmt::Debug { fn from_summary(summary: &Summary) -> Self; fn default() -> Self { Self::from_summary(&Summary::default()).clone() } } #[derive(Debug, Clone)] pub struct Tree(Arc>); #[derive(Debug)] pub enum Node { Internal { height: u8, summary: T::Summary, child_summaries: SmallVec<[T::Summary; 2 * TREE_BASE]>, child_trees: SmallVec<[Tree; 2 * TREE_BASE]>, }, Leaf { summary: T::Summary, items: SmallVec<[T; 2 * TREE_BASE]>, }, } #[derive(Clone)] pub struct Cursor { tree: Tree, stack: SmallVec<[(Tree, usize, T::Summary); 16]>, summary: T::Summary, did_seek: bool, at_end: bool, } pub struct FilterCursor bool, T: Item> { cursor: Cursor, filter_node: F, } #[derive(Eq, PartialEq)] pub enum SeekBias { Left, Right, } #[derive(Debug)] pub enum Edit { Insert(T), Remove(T), } impl Tree { pub fn new() -> Self { Tree(Arc::new(Node::Leaf { summary: T::Summary::default(), items: SmallVec::new(), })) } pub fn from_item(item: T) -> Self { let mut tree = Self::new(); tree.push(item); tree } #[allow(dead_code)] pub fn items(&self) -> Vec { let mut items = Vec::new(); let mut cursor = self.cursor(); cursor.descend_to_first_item(self.clone(), |_| true); loop { if let Some(item) = cursor.item() { items.push(item); } else { break; } cursor.next(); } items } pub fn cursor(&self) -> Cursor { Cursor::new(self.clone()) } pub fn filter(&self, filter_node: F) -> FilterCursor where F: Fn(&T::Summary) -> bool, { FilterCursor::new(self, filter_node) } #[allow(dead_code)] pub fn first(&self) -> Option { self.leftmost_leaf().0.items().first().cloned() } pub fn last(&self) -> Option { self.rightmost_leaf().0.items().last().cloned() } pub fn extent>(&self) -> D { match self.0.as_ref() { Node::Internal { summary, .. } => D::from_summary(summary).clone(), Node::Leaf { summary, .. } => D::from_summary(summary).clone(), } } pub fn summary(&self) -> T::Summary { match self.0.as_ref() { Node::Internal { summary, .. } => summary.clone(), Node::Leaf { summary, .. } => summary.clone(), } } #[cfg(test)] pub fn is_empty(&self) -> bool { match self.0.as_ref() { Node::Internal { .. } => false, Node::Leaf { items, .. } => items.is_empty(), } } pub fn extend(&mut self, iter: I) where I: IntoIterator, { let mut leaf: Option> = None; for item in iter { if leaf.is_some() && leaf.as_ref().unwrap().items().len() == 2 * TREE_BASE { self.push_tree(Tree(Arc::new(leaf.take().unwrap()))); } if leaf.is_none() { leaf = Some(Node::Leaf:: { summary: T::Summary::default(), items: SmallVec::new(), }); } let leaf = leaf.as_mut().unwrap(); *leaf.summary_mut() += &item.summarize(); leaf.items_mut().push(item); } if leaf.is_some() { self.push_tree(Tree(Arc::new(leaf.take().unwrap()))); } } pub fn push(&mut self, item: T) { self.push_tree(Tree::from_child_trees(vec![Tree(Arc::new(Node::Leaf { summary: item.summarize(), items: SmallVec::from_vec(vec![item]), }))])) } pub fn push_tree(&mut self, other: Self) { let other_node = other.0.clone(); if !other_node.is_leaf() || other_node.items().len() > 0 { if self.0.height() < other_node.height() { for tree in other_node.child_trees() { self.push_tree(tree.clone()); } } else if let Some(split_tree) = self.push_tree_recursive(other) { *self = Self::from_child_trees(vec![self.clone(), split_tree]); } } } fn push_tree_recursive(&mut self, other: Tree) -> Option> { match Arc::make_mut(&mut self.0) { Node::Internal { height, summary, child_summaries, child_trees, .. } => { let other_node = other.0.clone(); *summary += other_node.summary(); let height_delta = *height - other_node.height(); let mut summaries_to_append = SmallVec::<[T::Summary; 2 * TREE_BASE]>::new(); let mut trees_to_append = SmallVec::<[Tree; 2 * TREE_BASE]>::new(); if height_delta == 0 { summaries_to_append.extend(other_node.child_summaries().iter().cloned()); trees_to_append.extend(other_node.child_trees().iter().cloned()); } else if height_delta == 1 && !other_node.is_underflowing() { summaries_to_append.push(other_node.summary().clone()); trees_to_append.push(other) } else { let tree_to_append = child_trees.last_mut().unwrap().push_tree_recursive(other); *child_summaries.last_mut().unwrap() = child_trees.last().unwrap().0.summary().clone(); if let Some(split_tree) = tree_to_append { summaries_to_append.push(split_tree.0.summary().clone()); trees_to_append.push(split_tree); } } let child_count = child_trees.len() + trees_to_append.len(); if child_count > 2 * TREE_BASE { let left_summaries: SmallVec<_>; let right_summaries: SmallVec<_>; let left_trees; let right_trees; let midpoint = (child_count + child_count % 2) / 2; { let mut all_summaries = child_summaries .iter() .chain(summaries_to_append.iter()) .cloned(); left_summaries = all_summaries.by_ref().take(midpoint).collect(); right_summaries = all_summaries.collect(); let mut all_trees = child_trees.iter().chain(trees_to_append.iter()).cloned(); left_trees = all_trees.by_ref().take(midpoint).collect(); right_trees = all_trees.collect(); } *summary = sum(left_summaries.iter()); *child_summaries = left_summaries; *child_trees = left_trees; Some(Tree(Arc::new(Node::Internal { height: *height, summary: sum(right_summaries.iter()), child_summaries: right_summaries, child_trees: right_trees, }))) } else { child_summaries.extend(summaries_to_append); child_trees.extend(trees_to_append); None } } Node::Leaf { summary, items, .. } => { let other_node = other.0; let child_count = items.len() + other_node.items().len(); if child_count > 2 * TREE_BASE { let left_items; let right_items: SmallVec<[T; 2 * TREE_BASE]>; let midpoint = (child_count + child_count % 2) / 2; { let mut all_items = items.iter().chain(other_node.items().iter()).cloned(); left_items = all_items.by_ref().take(midpoint).collect(); right_items = all_items.collect(); } *items = left_items; *summary = sum_owned(items.iter().map(|item| item.summarize())); Some(Tree(Arc::new(Node::Leaf { summary: sum_owned(right_items.iter().map(|item| item.summarize())), items: right_items, }))) } else { *summary += other_node.summary(); items.extend(other_node.items().iter().cloned()); None } } } } fn from_child_trees(child_trees: Vec>) -> Self { let height = child_trees[0].0.height() + 1; let mut child_summaries = SmallVec::new(); for child in &child_trees { child_summaries.push(child.0.summary().clone()); } let summary = sum(child_summaries.iter()); Tree(Arc::new(Node::Internal { height, summary, child_summaries, child_trees: SmallVec::from_vec(child_trees), })) } fn leftmost_leaf(&self) -> Tree { match *self.0 { Node::Leaf { .. } => self.clone(), Node::Internal { ref child_trees, .. } => child_trees.first().unwrap().leftmost_leaf(), } } fn rightmost_leaf(&self) -> Tree { match *self.0 { Node::Leaf { .. } => self.clone(), Node::Internal { ref child_trees, .. } => child_trees.last().unwrap().rightmost_leaf(), } } } impl Tree { pub fn insert(&mut self, item: T) { let mut cursor = self.cursor(); let mut new_tree = cursor.slice(&item.key(), SeekBias::Left); new_tree.push(item); new_tree.push_tree(cursor.suffix::()); *self = new_tree; } pub fn edit(&mut self, edits: &mut [Edit]) { if edits.is_empty() { return; } edits.sort_unstable_by_key(|item| item.key()); let mut cursor = self.cursor(); let mut new_tree = Tree::new(); let mut buffered_items = Vec::new(); cursor.seek(&T::Key::default(), SeekBias::Left); for edit in edits { let new_key = edit.key(); let mut old_item = cursor.item(); if old_item .as_ref() .map_or(false, |old_item| old_item.key() < new_key) { new_tree.extend(buffered_items.drain(..)); let slice = cursor.slice(&new_key, SeekBias::Left); new_tree.push_tree(slice); old_item = cursor.item(); } if old_item.map_or(false, |old_item| old_item.key() == new_key) { cursor.next(); } match edit { Edit::Insert(item) => { buffered_items.push(item.clone()); } Edit::Remove(_) => {} } } new_tree.extend(buffered_items); new_tree.push_tree(cursor.suffix::()); *self = new_tree; } } impl Node { fn is_leaf(&self) -> bool { match self { Node::Leaf { .. } => true, _ => false, } } fn height(&self) -> u8 { match self { Node::Internal { height, .. } => *height, Node::Leaf { .. } => 0, } } fn summary(&self) -> &T::Summary { match self { Node::Internal { summary, .. } => summary, Node::Leaf { summary, .. } => summary, } } fn child_summaries(&self) -> &[T::Summary] { match self { Node::Internal { child_summaries, .. } => child_summaries.as_slice(), Node::Leaf { .. } => panic!("Leaf nodes have no child summaries"), } } fn child_trees(&self) -> &SmallVec<[Tree; 2 * TREE_BASE]> { match self { Node::Internal { child_trees, .. } => child_trees, Node::Leaf { .. } => panic!("Leaf nodes have no child trees"), } } fn items(&self) -> &SmallVec<[T; 2 * TREE_BASE]> { match self { Node::Leaf { items, .. } => items, Node::Internal { .. } => panic!("Internal nodes have no items"), } } fn items_mut(&mut self) -> &mut SmallVec<[T; 2 * TREE_BASE]> { match self { Node::Leaf { items, .. } => items, Node::Internal { .. } => panic!("Internal nodes have no items"), } } fn summary_mut(&mut self) -> &mut T::Summary { match self { Node::Internal { summary, .. } => summary, Node::Leaf { summary, .. } => summary, } } fn is_underflowing(&self) -> bool { match self { Node::Internal { child_trees, .. } => child_trees.len() < TREE_BASE, Node::Leaf { items, .. } => items.len() < TREE_BASE, } } } impl Clone for Node { fn clone(&self) -> Self { match self { Node::Internal { height, summary, child_summaries, child_trees, .. } => Node::Internal { height: *height, summary: summary.clone(), child_summaries: child_summaries.clone(), child_trees: child_trees.clone(), }, Node::Leaf { summary, items, .. } => Node::Leaf { summary: summary.clone(), items: items.clone(), }, } } } impl Cursor { fn new(tree: Tree) -> Self { Self { tree, stack: SmallVec::new(), summary: T::Summary::default(), did_seek: false, at_end: false, } } fn reset(&mut self) { self.did_seek = false; self.at_end = false; self.stack.truncate(0); self.summary = T::Summary::default(); } pub fn start>(&self) -> D { D::from_summary(&self.summary).clone() } pub fn end>(&self) -> D { if let Some(item) = self.item() { self.start::() + &D::from_summary(&item.summarize()) } else { self.start::() } } pub fn item(&self) -> Option { assert!(self.did_seek, "Must seek before calling this method"); if let Some((subtree, index, _)) = self.stack.last() { match *subtree.0 { Node::Leaf { ref items, .. } => { if *index == items.len() { None } else { Some(items[*index].clone()) } } _ => unreachable!(), } } else { None } } pub fn prev_item(&self) -> Option { assert!(self.did_seek, "Must seek before calling this method"); if let Some((cur_leaf, index, _)) = self.stack.last() { if *index == 0 { if let Some(prev_leaf) = self.prev_leaf() { let prev_leaf = prev_leaf.0; Some(prev_leaf.items().last().unwrap().clone()) } else { None } } else { match *cur_leaf.0 { Node::Leaf { ref items, .. } => Some(items[index - 1].clone()), _ => unreachable!(), } } } else if self.at_end { self.tree.last() } else { None } } fn prev_leaf(&self) -> Option> { for (ancestor, index, _) in self.stack.iter().rev().skip(1) { if *index != 0 { match *ancestor.0 { Node::Internal { ref child_trees, .. } => return Some(child_trees[index - 1].rightmost_leaf()), Node::Leaf { .. } => unreachable!(), }; } } None } pub fn prev(&mut self) { assert!(self.did_seek, "Must seek before calling this method"); if self.at_end { self.summary = T::Summary::default(); let root = self.tree.clone(); self.descend_to_last_item(root); self.at_end = false; } else { while let Some((subtree, index, _)) = self.stack.pop() { if index > 0 { let new_index = index - 1; self.summary = self .stack .last() .map_or(T::Summary::default(), |(_, _, summary)| summary.clone()); match subtree.0.as_ref() { Node::Internal { child_trees, child_summaries, .. } => { for summary in &child_summaries[0..new_index] { self.summary += summary; } self.stack .push((subtree.clone(), new_index, self.summary.clone())); self.descend_to_last_item(child_trees[new_index].clone()); } Node::Leaf { items, .. } => { for item in &items[0..new_index] { self.summary += &item.summarize(); } self.stack .push((subtree.clone(), new_index, self.summary.clone())); } } break; } } } } pub fn next(&mut self) { self.next_internal(|_| true) } fn next_internal(&mut self, filter_node: F) where F: Fn(&T::Summary) -> bool, { assert!(self.did_seek, "Must seek before calling this method"); if self.stack.is_empty() { if !self.at_end { let root = self.tree.clone(); self.descend_to_first_item(root, filter_node); } } else { while self.stack.len() > 0 { let new_subtree = { let (subtree, index, summary) = self.stack.last_mut().unwrap(); match subtree.0.as_ref() { Node::Internal { child_trees, child_summaries, .. } => { while *index < child_summaries.len() { *summary += &child_summaries[*index]; *index += 1; if let Some(next_summary) = child_summaries.get(*index) { if filter_node(next_summary) { break; } else { self.summary += next_summary; } } } child_trees.get(*index).cloned() } Node::Leaf { items, .. } => loop { let item_summary = items[*index].summarize(); self.summary += &item_summary; *summary += &item_summary; *index += 1; if let Some(next_item) = items.get(*index) { if filter_node(&next_item.summarize()) { return; } } else { break None; } }, } }; if let Some(subtree) = new_subtree { self.descend_to_first_item(subtree, filter_node); break; } else { self.stack.pop(); } } } self.at_end = self.stack.is_empty(); } fn descend_to_first_item(&mut self, mut subtree: Tree, filter_node: F) where F: Fn(&T::Summary) -> bool, { self.did_seek = true; loop { subtree = match *subtree.0 { Node::Internal { ref child_trees, ref child_summaries, .. } => { let mut new_index = None; for (index, summary) in child_summaries.iter().enumerate() { if filter_node(summary) { new_index = Some(index); break; } self.summary += summary; } if let Some(new_index) = new_index { self.stack .push((subtree.clone(), new_index, self.summary.clone())); child_trees[new_index].clone() } else { break; } } Node::Leaf { ref items, .. } => { let mut new_index = None; for (index, item) in items.iter().enumerate() { let summary = item.summarize(); if filter_node(&summary) { new_index = Some(index); break; } self.summary += &summary; } if let Some(new_index) = new_index { self.stack .push((subtree.clone(), new_index, self.summary.clone())); } break; } } } } fn descend_to_last_item(&mut self, mut subtree: Tree) { self.did_seek = true; loop { match subtree.0.clone().as_ref() { Node::Internal { child_trees, child_summaries, .. } => { for summary in &child_summaries[0..child_summaries.len() - 1] { self.summary += summary; } self.stack .push((subtree.clone(), child_trees.len() - 1, self.summary.clone())); subtree = child_trees.last().unwrap().clone(); } Node::Leaf { items, .. } => { let last_index = items.len().saturating_sub(1); for item in &items[0..last_index] { self.summary += &item.summarize(); } self.stack .push((subtree.clone(), last_index, self.summary.clone())); break; } } } } pub fn seek(&mut self, pos: &D, bias: SeekBias) -> bool where D: Dimension, { self.reset(); self.seek_internal(pos, bias, None) } pub fn seek_forward(&mut self, pos: &D, bias: SeekBias) -> bool where D: Dimension, { self.seek_internal(pos, bias, None) } pub fn slice(&mut self, end: &D, bias: SeekBias) -> Tree where D: Dimension, { let mut slice = Tree::new(); self.seek_internal(end, bias, Some(&mut slice)); slice } pub fn suffix(&mut self) -> Tree where D: Dimension, { let extent = self.tree.extent::(); let mut slice = Tree::new(); self.seek_internal(&extent, SeekBias::Right, Some(&mut slice)); slice } fn seek_internal( &mut self, target: &D, bias: SeekBias, mut slice: Option<&mut Tree>, ) -> bool where D: Dimension, { let mut pos = D::from_summary(&self.summary).clone(); debug_assert!(target >= &pos); let mut containing_subtree = None; if self.did_seek { 'outer: while self.stack.len() > 0 { { let (parent_subtree, index, _) = self.stack.last_mut().unwrap(); match *parent_subtree.0 { Node::Internal { ref child_summaries, ref child_trees, .. } => { *index += 1; while *index < child_summaries.len() { let child_tree = &child_trees[*index]; let child_summary = &child_summaries[*index]; let mut child_end = pos; child_end += &D::from_summary(&child_summary); let comparison = target.cmp(&child_end); if comparison == Ordering::Greater || (comparison == Ordering::Equal && bias == SeekBias::Right) { self.summary += child_summary; pos = child_end; if let Some(slice) = slice.as_mut() { slice.push_tree(child_tree.clone()); } *index += 1; } else { pos = D::from_summary(&self.summary).clone(); containing_subtree = Some(child_tree.clone()); break 'outer; } } } Node::Leaf { ref items, .. } => { let mut slice_items = SmallVec::<[T; 2 * TREE_BASE]>::new(); let mut slice_items_summary = T::Summary::default(); while *index < items.len() { let item = &items[*index]; let item_summary = item.summarize(); let mut item_end = pos; item_end += &D::from_summary(&item_summary); let comparison = target.cmp(&item_end); if comparison == Ordering::Greater || (comparison == Ordering::Equal && bias == SeekBias::Right) { self.summary += &item_summary; pos = item_end; if slice.is_some() { slice_items.push(item.clone()); slice_items_summary += &item_summary; } *index += 1; } else { pos = D::from_summary(&self.summary).clone(); if let Some(slice) = slice.as_mut() { slice.push_tree(Tree(Arc::new(Node::Leaf { summary: slice_items_summary, items: slice_items, }))); } break 'outer; } } if let Some(slice) = slice.as_mut() { if slice_items.len() > 0 { slice.push_tree(Tree(Arc::new(Node::Leaf { summary: slice_items_summary, items: slice_items, }))); } } } } } self.stack.pop(); } } else { self.did_seek = true; containing_subtree = Some(self.tree.clone()); } if let Some(mut subtree) = containing_subtree { loop { let mut next_subtree = None; match *subtree.0 { Node::Internal { ref child_summaries, ref child_trees, .. } => { for (index, child_summary) in child_summaries.iter().enumerate() { let mut child_end = pos; child_end += &D::from_summary(child_summary); let comparison = target.cmp(&child_end); if comparison == Ordering::Greater || (comparison == Ordering::Equal && bias == SeekBias::Right) { self.summary += child_summary; pos = child_end; if let Some(slice) = slice.as_mut() { slice.push_tree(child_trees[index].clone()); } } else { pos = D::from_summary(&self.summary).clone(); self.stack .push((subtree.clone(), index, self.summary.clone())); next_subtree = Some(child_trees[index].clone()); break; } } } Node::Leaf { ref items, .. } => { let mut slice_items = SmallVec::<[T; 2 * TREE_BASE]>::new(); let mut slice_items_summary = T::Summary::default(); for (index, item) in items.iter().enumerate() { let item_summary = item.summarize(); let mut child_end = pos; child_end += &D::from_summary(&item_summary); let comparison = target.cmp(&child_end); if comparison == Ordering::Greater || (comparison == Ordering::Equal && bias == SeekBias::Right) { if slice.is_some() { slice_items.push(item.clone()); slice_items_summary += &item_summary; } self.summary += &item_summary; pos = child_end; } else { pos = D::from_summary(&self.summary).clone(); self.stack .push((subtree.clone(), index, self.summary.clone())); break; } } if let Some(slice) = slice.as_mut() { if slice_items.len() > 0 { slice.push_tree(Tree(Arc::new(Node::Leaf { summary: slice_items_summary, items: slice_items, }))); } } } }; if let Some(next_subtree) = next_subtree { subtree = next_subtree; } else { break; } } } self.at_end = self.stack.is_empty(); if bias == SeekBias::Left { *target == self.end::() } else { *target == self.start::() } } } impl Iterator for Cursor { type Item = T; fn next(&mut self) -> Option { if !self.did_seek { let root = self.tree.clone(); self.descend_to_first_item(root, |_| true); } if let Some(item) = self.item() { self.next(); Some(item) } else { None } } } impl bool, T: Item> FilterCursor { fn new(tree: &Tree, filter_node: F) -> Self { let mut cursor = tree.cursor(); if filter_node(&tree.summary()) { cursor.descend_to_first_item(tree.clone(), &filter_node); } else { cursor.did_seek = true; cursor.at_end = true; } Self { cursor, filter_node, } } pub fn start>(&self) -> D { self.cursor.start() } pub fn item(&self) -> Option { self.cursor.item() } pub fn next(&mut self) { self.cursor.next_internal(&self.filter_node); } } impl bool, T: Item> Iterator for FilterCursor { type Item = T; fn next(&mut self) -> Option { if let Some(item) = self.item() { self.cursor.next_internal(&self.filter_node); Some(item) } else { None } } } impl Edit { fn key(&self) -> T::Key { match self { Edit::Insert(item) | Edit::Remove(item) => item.key(), } } } fn sum<'a, T, I>(iter: I) -> T where T: 'a + Default + AddAssign<&'a T>, I: Iterator, { let mut sum = T::default(); for value in iter { sum += value; } sum } fn sum_owned(iter: I) -> T where T: Default + for<'a> AddAssign<&'a T>, I: Iterator, { let mut sum = T::default(); for value in iter { sum += &value; } sum } #[cfg(test)] mod tests { use super::*; #[test] fn test_extend_and_push_tree() { let mut tree1 = Tree::new(); tree1.extend(0..20); let mut tree2 = Tree::new(); tree2.extend(50..100); tree1.push_tree(tree2); assert_eq!(tree1.items(), (0..20).chain(50..100).collect::>()); } #[test] fn test_random() { for seed in 0..100 { use rand::{Rng, SeedableRng, StdRng}; let mut rng = StdRng::from_seed(&[seed]); let mut tree = Tree::::new(); let count = rng.gen_range(0, 10); tree.extend(rng.gen_iter().take(count)); for _ in 0..5 { let splice_end = rng.gen_range(0, tree.extent::().0 + 1); let splice_start = rng.gen_range(0, splice_end + 1); let count = rng.gen_range(0, 3); let tree_end = tree.extent::(); let new_items = rng.gen_iter().take(count).collect::>(); let mut reference_items = tree.items(); reference_items.splice(splice_start..splice_end, new_items.clone()); let mut cursor = tree.cursor(); tree = cursor.slice(&Count(splice_start), SeekBias::Right); tree.extend(new_items); cursor.seek(&Count(splice_end), SeekBias::Right); tree.push_tree(cursor.slice(&tree_end, SeekBias::Right)); assert_eq!(tree.items(), reference_items); let mut filter_cursor = tree.filter(|summary| summary.contains_even); let mut reference_filter = tree .items() .into_iter() .enumerate() .filter(|(_, item)| (item & 1) == 0); while let Some(actual_item) = filter_cursor.item() { let (reference_index, reference_item) = reference_filter.next().unwrap(); assert_eq!(actual_item, reference_item); assert_eq!(filter_cursor.start::().0, reference_index); filter_cursor.next(); } assert!(reference_filter.next().is_none()); let mut pos = rng.gen_range(0, tree.extent::().0 + 1); let mut before_start = false; let mut cursor = tree.cursor(); cursor.seek(&Count(pos), SeekBias::Right); for i in 0..10 { assert_eq!(cursor.start::().0, pos); if pos > 0 { assert_eq!(cursor.prev_item().unwrap(), reference_items[pos - 1]); } else { assert_eq!(cursor.prev_item(), None); } if pos < reference_items.len() && !before_start { assert_eq!(cursor.item().unwrap(), reference_items[pos]); } else { assert_eq!(cursor.item(), None); } if i < 5 { cursor.next(); if pos < reference_items.len() { pos += 1; before_start = false; } } else { cursor.prev(); if pos == 0 { before_start = true; } pos = pos.saturating_sub(1); } } } } } #[test] fn test_cursor() { // Empty tree let tree = Tree::::new(); let mut cursor = tree.cursor(); assert_eq!(cursor.slice(&Sum(0), SeekBias::Right).items(), vec![]); assert_eq!(cursor.item(), None); assert_eq!(cursor.prev_item(), None); assert_eq!(cursor.start::(), Count(0)); assert_eq!(cursor.start::(), Sum(0)); // Single-element tree let mut tree = Tree::::new(); tree.extend(vec![1]); let mut cursor = tree.cursor(); assert_eq!(cursor.slice(&Sum(0), SeekBias::Right).items(), vec![]); assert_eq!(cursor.item(), Some(1)); assert_eq!(cursor.prev_item(), None); assert_eq!(cursor.start::(), Count(0)); assert_eq!(cursor.start::(), Sum(0)); cursor.next(); assert_eq!(cursor.item(), None); assert_eq!(cursor.prev_item(), Some(1)); assert_eq!(cursor.start::(), Count(1)); assert_eq!(cursor.start::(), Sum(1)); cursor.prev(); assert_eq!(cursor.item(), Some(1)); assert_eq!(cursor.prev_item(), None); assert_eq!(cursor.start::(), Count(0)); assert_eq!(cursor.start::(), Sum(0)); cursor.reset(); assert_eq!(cursor.slice(&Sum(1), SeekBias::Right).items(), [1]); assert_eq!(cursor.item(), None); assert_eq!(cursor.prev_item(), Some(1)); assert_eq!(cursor.start::(), Count(1)); assert_eq!(cursor.start::(), Sum(1)); cursor.seek(&Sum(0), SeekBias::Right); assert_eq!( cursor .slice(&tree.extent::(), SeekBias::Right) .items(), [1] ); assert_eq!(cursor.item(), None); assert_eq!(cursor.prev_item(), Some(1)); assert_eq!(cursor.start::(), Count(1)); assert_eq!(cursor.start::(), Sum(1)); // Multiple-element tree let mut tree = Tree::new(); tree.extend(vec![1, 2, 3, 4, 5, 6]); let mut cursor = tree.cursor(); assert_eq!(cursor.slice(&Sum(4), SeekBias::Right).items(), [1, 2]); assert_eq!(cursor.item(), Some(3)); assert_eq!(cursor.prev_item(), Some(2)); assert_eq!(cursor.start::(), Count(2)); assert_eq!(cursor.start::(), Sum(3)); cursor.next(); assert_eq!(cursor.item(), Some(4)); assert_eq!(cursor.prev_item(), Some(3)); assert_eq!(cursor.start::(), Count(3)); assert_eq!(cursor.start::(), Sum(6)); cursor.next(); assert_eq!(cursor.item(), Some(5)); assert_eq!(cursor.prev_item(), Some(4)); assert_eq!(cursor.start::(), Count(4)); assert_eq!(cursor.start::(), Sum(10)); cursor.next(); assert_eq!(cursor.item(), Some(6)); assert_eq!(cursor.prev_item(), Some(5)); assert_eq!(cursor.start::(), Count(5)); assert_eq!(cursor.start::(), Sum(15)); cursor.next(); cursor.next(); assert_eq!(cursor.item(), None); assert_eq!(cursor.prev_item(), Some(6)); assert_eq!(cursor.start::(), Count(6)); assert_eq!(cursor.start::(), Sum(21)); cursor.prev(); assert_eq!(cursor.item(), Some(6)); assert_eq!(cursor.prev_item(), Some(5)); assert_eq!(cursor.start::(), Count(5)); assert_eq!(cursor.start::(), Sum(15)); cursor.prev(); assert_eq!(cursor.item(), Some(5)); assert_eq!(cursor.prev_item(), Some(4)); assert_eq!(cursor.start::(), Count(4)); assert_eq!(cursor.start::(), Sum(10)); cursor.prev(); assert_eq!(cursor.item(), Some(4)); assert_eq!(cursor.prev_item(), Some(3)); assert_eq!(cursor.start::(), Count(3)); assert_eq!(cursor.start::(), Sum(6)); cursor.prev(); assert_eq!(cursor.item(), Some(3)); assert_eq!(cursor.prev_item(), Some(2)); assert_eq!(cursor.start::(), Count(2)); assert_eq!(cursor.start::(), Sum(3)); cursor.prev(); assert_eq!(cursor.item(), Some(2)); assert_eq!(cursor.prev_item(), Some(1)); assert_eq!(cursor.start::(), Count(1)); assert_eq!(cursor.start::(), Sum(1)); cursor.prev(); assert_eq!(cursor.item(), Some(1)); assert_eq!(cursor.prev_item(), None); assert_eq!(cursor.start::(), Count(0)); assert_eq!(cursor.start::(), Sum(0)); cursor.prev(); assert_eq!(cursor.item(), None); assert_eq!(cursor.prev_item(), None); assert_eq!(cursor.start::(), Count(0)); assert_eq!(cursor.start::(), Sum(0)); cursor.next(); assert_eq!(cursor.item(), Some(1)); assert_eq!(cursor.prev_item(), None); assert_eq!(cursor.start::(), Count(0)); assert_eq!(cursor.start::(), Sum(0)); cursor.reset(); assert_eq!( cursor .slice(&tree.extent::(), SeekBias::Right) .items(), tree.items() ); assert_eq!(cursor.item(), None); assert_eq!(cursor.prev_item(), Some(6)); assert_eq!(cursor.start::(), Count(6)); assert_eq!(cursor.start::(), Sum(21)); cursor.seek(&Count(3), SeekBias::Right); assert_eq!( cursor .slice(&tree.extent::(), SeekBias::Right) .items(), [4, 5, 6] ); assert_eq!(cursor.item(), None); assert_eq!(cursor.prev_item(), Some(6)); assert_eq!(cursor.start::(), Count(6)); assert_eq!(cursor.start::(), Sum(21)); // Seeking can bias left or right cursor.seek(&Sum(1), SeekBias::Left); assert_eq!(cursor.item(), Some(1)); cursor.seek(&Sum(1), SeekBias::Right); assert_eq!(cursor.item(), Some(2)); // Slicing without resetting starts from where the cursor is parked at. cursor.seek(&Sum(1), SeekBias::Right); assert_eq!(cursor.slice(&Sum(6), SeekBias::Right).items(), vec![2, 3]); assert_eq!(cursor.slice(&Sum(21), SeekBias::Left).items(), vec![4, 5]); assert_eq!(cursor.slice(&Sum(21), SeekBias::Right).items(), vec![6]); } #[derive(Clone, Default, Debug)] pub struct IntegersSummary { count: Count, sum: Sum, contains_even: bool, } #[derive(Ord, PartialOrd, Default, Eq, PartialEq, Clone, Debug)] struct Count(usize); #[derive(Ord, PartialOrd, Default, Eq, PartialEq, Clone, Debug)] struct Sum(usize); impl Item for u8 { type Summary = IntegersSummary; fn summarize(&self) -> Self::Summary { IntegersSummary { count: Count(1), sum: Sum(*self as usize), contains_even: (*self & 1) == 0, } } } impl<'a> AddAssign<&'a Self> for IntegersSummary { fn add_assign(&mut self, other: &Self) { self.count += &other.count; self.sum += &other.sum; self.contains_even |= other.contains_even; } } impl Dimension for Count { fn from_summary(summary: &IntegersSummary) -> Self { summary.count.clone() } } impl<'a> AddAssign<&'a Self> for Count { fn add_assign(&mut self, other: &Self) { self.0 += other.0; } } impl<'a> Add<&'a Self> for Count { type Output = Self; fn add(mut self, other: &Self) -> Self { self.0 += other.0; self } } impl Dimension for Sum { fn from_summary(summary: &IntegersSummary) -> Self { summary.sum.clone() } } impl<'a> AddAssign<&'a Self> for Sum { fn add_assign(&mut self, other: &Self) { self.0 += other.0; } } impl<'a> Add<&'a Self> for Sum { type Output = Self; fn add(mut self, other: &Self) -> Self { self.0 += other.0; self } } } ================================================ FILE: memo_core/src/buffer.rs ================================================ use crate::btree::{self, SeekBias}; use crate::operation_queue::{self, OperationQueue}; use crate::serialization; use crate::time; use crate::{Error, ReplicaId}; use flatbuffers::{FlatBufferBuilder, WIPOffset}; use lazy_static::lazy_static; use serde_derive::{Deserialize, Serialize}; use smallvec::SmallVec; use std::cell::RefCell; use std::cmp::{self, Ordering}; use std::collections::{HashMap, HashSet}; use std::iter; use std::mem; use std::ops::{Add, AddAssign, Range, Sub}; use std::sync::Arc; use std::vec; pub type SelectionSetId = time::Lamport; pub type SelectionsVersion = usize; #[derive(Clone)] pub struct Buffer { fragments: btree::Tree, insertion_splits: HashMap>, anchor_cache: RefCell>, offset_cache: RefCell>, pub version: time::Global, last_edit: time::Local, selections: HashMap>, pub selections_last_update: SelectionsVersion, deferred_ops: OperationQueue, deferred_replicas: HashSet, } #[derive(Clone, Copy, Deserialize, Eq, PartialEq, Debug, Hash, Serialize)] pub struct Point { pub row: u32, pub column: u32, } #[derive(Clone, Eq, PartialEq, Debug, Hash)] pub enum Anchor { Start, End, Middle { insertion_id: time::Local, offset: usize, bias: AnchorBias, }, } #[derive(Clone, Eq, PartialEq, Debug, Hash)] pub enum AnchorBias { Left, Right, } #[derive(Clone, Debug, Eq, PartialEq)] pub struct Selection { pub start: Anchor, pub end: Anchor, pub reversed: bool, } pub struct Iter { fragment_cursor: btree::Cursor, fragment_offset: usize, reversed: bool, } struct ChangesIter bool> { cursor: btree::FilterCursor, since: time::Global, } #[derive(Debug, Eq, PartialEq)] pub struct Change { pub range: Range, pub code_units: Vec, new_extent: Point, } #[derive(Clone, Eq, PartialEq, Debug)] pub struct Insertion { id: time::Local, parent_id: time::Local, offset_in_parent: usize, text: Arc, lamport_timestamp: time::Lamport, } #[derive(Clone, Eq, PartialEq, Debug)] pub struct Text { code_units: Vec, nodes: Vec, } #[derive(Clone, Eq, PartialEq, Debug)] struct LineNode { len: u32, longest_row: u32, longest_row_len: u32, offset: usize, rows: u32, } struct LineNodeProbe<'a> { offset_range: &'a Range, row: u32, left_ancestor_end_offset: usize, right_ancestor_start_offset: usize, node: &'a LineNode, left_child: Option<&'a LineNode>, right_child: Option<&'a LineNode>, } #[derive(Ord, PartialOrd, Eq, PartialEq, Clone, Debug)] struct FragmentId(Arc>); #[derive(Eq, PartialEq, Clone, Debug)] struct Fragment { id: FragmentId, insertion: Insertion, start_offset: usize, end_offset: usize, deletions: HashSet, } #[derive(Eq, PartialEq, Clone, Debug)] pub struct FragmentSummary { extent: usize, extent_2d: Point, max_fragment_id: FragmentId, first_row_len: u32, longest_row: u32, longest_row_len: u32, max_version: time::Global, } #[derive(Eq, PartialEq, Clone, Debug)] struct InsertionSplit { extent: usize, fragment_id: FragmentId, } #[derive(Eq, PartialEq, Clone, Debug)] struct InsertionSplitSummary { extent: usize, } #[derive(Clone, Debug, Eq, PartialEq)] pub enum Operation { Edit { start_id: time::Local, start_offset: usize, end_id: time::Local, end_offset: usize, version_in_range: time::Global, new_text: Option>, local_timestamp: time::Local, lamport_timestamp: time::Lamport, }, UpdateSelections { set_id: SelectionSetId, selections: Option>, lamport_timestamp: time::Lamport, }, } impl Buffer { pub fn new(base_text: T) -> Self where T: Into, { let mut insertion_splits = HashMap::new(); let mut fragments = btree::Tree::new(); let base_insertion = Insertion { id: time::Local::default(), parent_id: time::Local::default(), offset_in_parent: 0, text: Arc::new(base_text.into()), lamport_timestamp: time::Lamport::default(), }; insertion_splits.insert( base_insertion.id, btree::Tree::from_item(InsertionSplit { fragment_id: FragmentId::min_value(), extent: 0, }), ); fragments.push(Fragment { id: FragmentId::min_value(), insertion: base_insertion.clone(), start_offset: 0, end_offset: 0, deletions: HashSet::new(), }); if base_insertion.text.len() > 0 { let base_fragment_id = FragmentId::between(&FragmentId::min_value(), &FragmentId::max_value()); insertion_splits .get_mut(&base_insertion.id) .unwrap() .push(InsertionSplit { fragment_id: base_fragment_id.clone(), extent: base_insertion.text.len(), }); fragments.push(Fragment { id: base_fragment_id, start_offset: 0, end_offset: base_insertion.text.len(), insertion: base_insertion, deletions: HashSet::new(), }); } Self { fragments, insertion_splits, anchor_cache: RefCell::new(HashMap::default()), offset_cache: RefCell::new(HashMap::default()), version: time::Global::new(), last_edit: time::Local::default(), selections: HashMap::default(), selections_last_update: 0, deferred_ops: OperationQueue::new(), deferred_replicas: HashSet::new(), } } pub fn is_modified(&self) -> bool { self.version != time::Global::new() } pub fn len(&self) -> usize { self.fragments.extent::() } pub fn len_for_row(&self, row: u32) -> Result { let row_start_offset = self.offset_for_point(Point::new(row, 0))?; let row_end_offset = if row >= self.max_point().row { self.len() } else { self.offset_for_point(Point::new(row + 1, 0))? - 1 }; Ok((row_end_offset - row_start_offset) as u32) } pub fn longest_row(&self) -> u32 { self.fragments.summary().longest_row } pub fn max_point(&self) -> Point { self.fragments.extent() } pub fn line(&self, row: u32) -> Result, Error> { let mut iterator = self.iter_at_point(Point::new(row, 0)).peekable(); if iterator.peek().is_none() { Err(Error::OffsetOutOfRange) } else { Ok(iterator.take_while(|c| *c != u16::from(b'\n')).collect()) } } pub fn to_u16_chars(&self) -> Vec { self.iter().collect::>() } pub fn to_string(&self) -> String { String::from_utf16_lossy(&self.to_u16_chars()) } pub fn iter(&self) -> Iter { Iter::new(self) } pub fn iter_at_point(&self, point: Point) -> Iter { Iter::at_point(self, point) } pub fn selections_changed_since(&self, since: SelectionsVersion) -> bool { self.selections_last_update != since } pub fn changes_since(&self, since: &time::Global) -> impl Iterator { let since_2 = since.clone(); let cursor = self .fragments .filter(move |summary| summary.max_version.changed_since(&since_2)); ChangesIter { cursor, since: since.clone(), } } pub fn deferred_ops_len(&self) -> usize { self.deferred_ops.len() } pub fn edit( &mut self, old_ranges: I, new_text: T, local_clock: &mut time::Local, lamport_clock: &mut time::Lamport, ) -> Vec where I: IntoIterator>, T: Into, { let new_text = new_text.into(); let new_text = if new_text.len() > 0 { Some(Arc::new(new_text)) } else { None }; self.anchor_cache.borrow_mut().clear(); self.offset_cache.borrow_mut().clear(); let ops = self.splice_fragments( old_ranges .into_iter() .filter(|old_range| new_text.is_some() || old_range.end > old_range.start), new_text.clone(), local_clock, lamport_clock, ); if let Some(op) = ops.last() { if let Operation::Edit { local_timestamp, .. } = op { self.last_edit = *local_timestamp; self.version.observe(*local_timestamp); } else { unreachable!() } } ops } pub fn edit_2d( &mut self, old_2d_ranges: I, new_text: T, local_clock: &mut time::Local, lamport_clock: &mut time::Lamport, ) -> Vec where I: IntoIterator>, T: Into, { let mut old_1d_ranges = SmallVec::<[_; 1]>::new(); for old_2d_range in old_2d_ranges { let start = self.offset_for_point(old_2d_range.start); let end = self.offset_for_point(old_2d_range.end); if start.is_ok() && end.is_ok() { old_1d_ranges.push(start.unwrap()..end.unwrap()); } } self.edit(old_1d_ranges, new_text, local_clock, lamport_clock) } pub fn add_selection_set( &mut self, ranges: I, lamport_clock: &mut time::Lamport, ) -> Result<(SelectionSetId, Operation), Error> where I: IntoIterator>, { let selections = self.selections_from_ranges(ranges)?; let lamport_timestamp = lamport_clock.tick(); self.selections .insert(lamport_timestamp, selections.clone()); self.selections_last_update += 1; Ok(( lamport_timestamp, Operation::UpdateSelections { set_id: lamport_timestamp, selections: Some(selections), lamport_timestamp, }, )) } pub fn replace_selection_set( &mut self, set_id: SelectionSetId, ranges: I, lamport_clock: &mut time::Lamport, ) -> Result where I: IntoIterator>, { self.selections .remove(&set_id) .ok_or(Error::InvalidSelectionSet(set_id))?; let mut selections = self.selections_from_ranges(ranges)?; self.merge_selections(&mut selections); self.selections.insert(set_id, selections.clone()); let lamport_timestamp = lamport_clock.tick(); self.selections_last_update += 1; Ok(Operation::UpdateSelections { set_id, selections: Some(selections), lamport_timestamp, }) } pub fn remove_selection_set( &mut self, set_id: SelectionSetId, lamport_clock: &mut time::Lamport, ) -> Result { self.selections .remove(&set_id) .ok_or(Error::InvalidSelectionSet(set_id))?; let lamport_timestamp = lamport_clock.tick(); self.selections_last_update += 1; Ok(Operation::UpdateSelections { set_id, selections: None, lamport_timestamp, }) } pub fn selection_ranges<'a>( &'a self, set_id: SelectionSetId, ) -> Result> + 'a, Error> { let selections = self .selections .get(&set_id) .ok_or(Error::InvalidSelectionSet(set_id))?; Ok(selections.iter().map(move |selection| { let start = self.point_for_anchor(&selection.start).unwrap(); let end = self.point_for_anchor(&selection.end).unwrap(); if selection.reversed { end..start } else { start..end } })) } pub fn all_selections(&self) -> impl Iterator)> { self.selections.iter() } pub fn all_selection_ranges<'a>( &'a self, ) -> impl 'a + Iterator>)> { self.selections .keys() .map(move |set_id| (*set_id, self.selection_ranges(*set_id).unwrap().collect())) } fn merge_selections(&mut self, selections: &mut Vec) { let mut new_selections = Vec::with_capacity(selections.len()); { let mut old_selections = selections.drain(..); if let Some(mut prev_selection) = old_selections.next() { for selection in old_selections { if self .cmp_anchors(&prev_selection.end, &selection.start) .unwrap() >= Ordering::Equal { if self .cmp_anchors(&selection.end, &prev_selection.end) .unwrap() > Ordering::Equal { prev_selection.end = selection.end; } } else { new_selections.push(mem::replace(&mut prev_selection, selection)); } } new_selections.push(prev_selection); } } *selections = new_selections; } fn selections_from_ranges(&self, ranges: I) -> Result, Error> where I: IntoIterator>, { let mut ranges = ranges.into_iter().collect::>(); ranges.sort_unstable_by_key(|range| range.start); let mut selections = Vec::with_capacity(ranges.len()); for range in ranges { if range.start > range.end { selections.push(Selection { start: self.anchor_before_point(range.end)?, end: self.anchor_before_point(range.start)?, reversed: true, }); } else { selections.push(Selection { start: self.anchor_before_point(range.start)?, end: self.anchor_before_point(range.end)?, reversed: false, }); } } Ok(selections) } pub fn apply_ops>( &mut self, ops: I, local_clock: &mut time::Local, lamport_clock: &mut time::Lamport, ) -> Result<(), Error> { let mut deferred_ops = Vec::new(); for op in ops { if self.can_apply_op(&op) { self.apply_op(op, local_clock, lamport_clock)?; } else { self.deferred_replicas.insert(op.replica_id()); deferred_ops.push(op); } } self.deferred_ops.insert(deferred_ops); self.flush_deferred_ops(local_clock, lamport_clock)?; Ok(()) } fn apply_op( &mut self, op: Operation, local_clock: &mut time::Local, lamport_clock: &mut time::Lamport, ) -> Result<(), Error> { match op { Operation::Edit { start_id, start_offset, end_id, end_offset, new_text, version_in_range, local_timestamp, lamport_timestamp, } => { if !self.version.observed(local_timestamp) { self.apply_edit( start_id, start_offset, end_id, end_offset, new_text.as_ref().cloned(), &version_in_range, local_timestamp, lamport_timestamp, local_clock, lamport_clock, )?; self.anchor_cache.borrow_mut().clear(); self.offset_cache.borrow_mut().clear(); self.version.observe(local_timestamp); } } Operation::UpdateSelections { set_id, selections, lamport_timestamp, } => { if let Some(selections) = selections { self.selections.insert(set_id, selections); } else { self.selections.remove(&set_id); } lamport_clock.observe(lamport_timestamp); self.selections_last_update += 1; } } Ok(()) } fn apply_edit( &mut self, start_id: time::Local, start_offset: usize, end_id: time::Local, end_offset: usize, new_text: Option>, version_in_range: &time::Global, local_timestamp: time::Local, lamport_timestamp: time::Lamport, local_clock: &mut time::Local, lamport_clock: &mut time::Lamport, ) -> Result<(), Error> { let mut new_text = new_text.as_ref().cloned(); let start_fragment_id = self.resolve_fragment_id(start_id, start_offset)?; let end_fragment_id = self.resolve_fragment_id(end_id, end_offset)?; let old_fragments = self.fragments.clone(); let mut cursor = old_fragments.cursor(); let mut new_fragments = cursor.slice(&start_fragment_id, SeekBias::Left); if start_offset == cursor.item().unwrap().end_offset { new_fragments.push(cursor.item().unwrap()); cursor.next(); } while let Some(mut fragment) = cursor.item() { if new_text.is_none() && fragment.id > end_fragment_id { break; } if fragment.id == start_fragment_id || fragment.id == end_fragment_id { let split_start = if start_fragment_id == fragment.id { start_offset } else { fragment.start_offset }; let split_end = if end_fragment_id == fragment.id { end_offset } else { fragment.end_offset }; let (before_range, within_range, after_range) = self.split_fragment( cursor.prev_item().as_ref().unwrap(), &fragment, split_start..split_end, ); let insertion = if let Some(new_text) = new_text.take() { Some( self.build_fragment_to_insert( before_range .as_ref() .or(cursor.prev_item().as_ref()) .unwrap(), within_range.as_ref().or(after_range.as_ref()), new_text, local_timestamp, lamport_timestamp, ), ) } else { None }; if let Some(fragment) = before_range { new_fragments.push(fragment); } if let Some(fragment) = insertion { new_fragments.push(fragment); } if let Some(mut fragment) = within_range { if version_in_range.observed(fragment.insertion.id) { fragment.deletions.insert(local_timestamp); } new_fragments.push(fragment); } if let Some(fragment) = after_range { new_fragments.push(fragment); } } else { if new_text.is_some() && lamport_timestamp > fragment.insertion.lamport_timestamp { new_fragments.push(self.build_fragment_to_insert( cursor.prev_item().as_ref().unwrap(), Some(&fragment), new_text.take().unwrap(), local_timestamp, lamport_timestamp, )); } if fragment.id < end_fragment_id && version_in_range.observed(fragment.insertion.id) { fragment.deletions.insert(local_timestamp); } new_fragments.push(fragment); } cursor.next(); } if let Some(new_text) = new_text { new_fragments.push(self.build_fragment_to_insert( cursor.prev_item().as_ref().unwrap(), None, new_text, local_timestamp, lamport_timestamp, )); } new_fragments.push_tree(cursor.slice(&old_fragments.extent::(), SeekBias::Right)); self.fragments = new_fragments; local_clock.observe(local_timestamp); lamport_clock.observe(lamport_timestamp); Ok(()) } fn flush_deferred_ops( &mut self, local_clock: &mut time::Local, lamport_clock: &mut time::Lamport, ) -> Result<(), Error> { self.deferred_replicas.clear(); let mut deferred_ops = Vec::new(); for op in self.deferred_ops.drain() { if self.can_apply_op(&op) { self.apply_op(op, local_clock, lamport_clock)?; } else { self.deferred_replicas.insert(op.replica_id()); deferred_ops.push(op); } } self.deferred_ops.insert(deferred_ops); Ok(()) } fn can_apply_op(&self, op: &Operation) -> bool { if self.deferred_replicas.contains(&op.replica_id()) { false } else { match op { Operation::Edit { start_id, end_id, version_in_range, .. } => { self.version.observed(*start_id) && self.version.observed(*end_id) && *version_in_range <= self.version } Operation::UpdateSelections { selections, .. } => { if let Some(selections) = selections { selections.iter().all(|selection| { let contains_start = match selection.start { Anchor::Middle { insertion_id, .. } => { self.version.observed(insertion_id) } _ => true, }; let contains_end = match selection.end { Anchor::Middle { insertion_id, .. } => { self.version.observed(insertion_id) } _ => true, }; contains_start && contains_end }) } else { true } } } } } fn resolve_fragment_id( &self, edit_id: time::Local, offset: usize, ) -> Result { let split_tree = self .insertion_splits .get(&edit_id) .ok_or(Error::InvalidOperation)?; let mut cursor = split_tree.cursor(); cursor.seek(&offset, SeekBias::Left); Ok(cursor .item() .ok_or(Error::InvalidOperation)? .fragment_id .clone()) } fn splice_fragments( &mut self, mut old_ranges: I, new_text: Option>, local_clock: &mut time::Local, lamport_clock: &mut time::Lamport, ) -> Vec where I: Iterator>, { let mut cur_range = old_ranges.next(); if cur_range.is_none() { return Vec::new(); } let mut ops = Vec::with_capacity(old_ranges.size_hint().0); let old_fragments = self.fragments.clone(); let mut cursor = old_fragments.cursor(); let mut new_fragments = btree::Tree::new(); new_fragments.push_tree(cursor.slice(&cur_range.as_ref().unwrap().start, SeekBias::Right)); let mut start_id = None; let mut start_offset = None; let mut end_id = None; let mut end_offset = None; let mut version_in_range = time::Global::new(); let mut local_timestamp = local_clock.tick(); let mut lamport_timestamp = lamport_clock.tick(); while cur_range.is_some() && cursor.item().is_some() { let mut fragment = cursor.item().unwrap(); let mut fragment_start = cursor.start::(); let mut fragment_end = fragment_start + fragment.len(); let old_split_tree = self .insertion_splits .remove(&fragment.insertion.id) .unwrap(); let mut splits_cursor = old_split_tree.cursor(); let mut new_split_tree = splits_cursor.slice(&fragment.start_offset, SeekBias::Right); // Find all splices that start or end within the current fragment. Then, split the // fragment and reassemble it in both trees accounting for the deleted and the newly // inserted text. while cur_range.as_ref().map_or(false, |r| r.start < fragment_end) { let range = cur_range.clone().unwrap(); if range.start > fragment_start { let mut prefix = fragment.clone(); prefix.end_offset = prefix.start_offset + (range.start - fragment_start); prefix.id = FragmentId::between(&new_fragments.last().unwrap().id, &fragment.id); fragment.start_offset = prefix.end_offset; new_fragments.push(prefix.clone()); new_split_tree.push(InsertionSplit { extent: prefix.end_offset - prefix.start_offset, fragment_id: prefix.id, }); fragment_start = range.start; } if range.end == fragment_start { end_id = Some(new_fragments.last().unwrap().insertion.id); end_offset = Some(new_fragments.last().unwrap().end_offset); } else if range.end == fragment_end { end_id = Some(fragment.insertion.id); end_offset = Some(fragment.end_offset); } if range.start == fragment_start { start_id = Some(new_fragments.last().unwrap().insertion.id); start_offset = Some(new_fragments.last().unwrap().end_offset); if let Some(new_text) = new_text.clone() { let new_fragment = self.build_fragment_to_insert( &new_fragments.last().unwrap(), Some(&fragment), new_text, local_timestamp, lamport_timestamp, ); new_fragments.push(new_fragment); } } if range.end < fragment_end { if range.end > fragment_start { let mut prefix = fragment.clone(); prefix.end_offset = prefix.start_offset + (range.end - fragment_start); prefix.id = FragmentId::between(&new_fragments.last().unwrap().id, &fragment.id); if fragment.is_visible() { prefix.deletions.insert(local_timestamp); } fragment.start_offset = prefix.end_offset; new_fragments.push(prefix.clone()); new_split_tree.push(InsertionSplit { extent: prefix.end_offset - prefix.start_offset, fragment_id: prefix.id, }); fragment_start = range.end; end_id = Some(fragment.insertion.id); end_offset = Some(fragment.start_offset); version_in_range.observe(fragment.insertion.id); } } else { version_in_range.observe(fragment.insertion.id); if fragment.is_visible() { fragment.deletions.insert(local_timestamp); } } // If the splice ends inside this fragment, we can advance to the next splice and // check if it also intersects the current fragment. Otherwise we break out of the // loop and find the first fragment that the splice does not contain fully. if range.end <= fragment_end { ops.push(Operation::Edit { start_id: start_id.unwrap(), start_offset: start_offset.unwrap(), end_id: end_id.unwrap(), end_offset: end_offset.unwrap(), version_in_range, new_text: new_text.clone(), local_timestamp, lamport_timestamp, }); start_id = None; start_offset = None; end_id = None; end_offset = None; version_in_range = time::Global::new(); cur_range = old_ranges.next(); if cur_range.is_some() { local_timestamp = local_clock.tick(); lamport_timestamp = lamport_clock.tick(); } } else { break; } } new_split_tree.push(InsertionSplit { extent: fragment.end_offset - fragment.start_offset, fragment_id: fragment.id.clone(), }); splits_cursor.next(); new_split_tree .push_tree(splits_cursor.slice(&old_split_tree.extent::(), SeekBias::Right)); self.insertion_splits .insert(fragment.insertion.id, new_split_tree); new_fragments.push(fragment); // Scan forward until we find a fragment that is not fully contained by the current splice. cursor.next(); if let Some(range) = cur_range.clone() { while let Some(mut fragment) = cursor.item() { fragment_start = cursor.start::(); fragment_end = fragment_start + fragment.len(); if range.start < fragment_start && range.end >= fragment_end { if fragment.is_visible() { fragment.deletions.insert(local_timestamp); } version_in_range.observe(fragment.insertion.id); new_fragments.push(fragment.clone()); cursor.next(); if range.end == fragment_end { end_id = Some(fragment.insertion.id); end_offset = Some(fragment.end_offset); ops.push(Operation::Edit { start_id: start_id.unwrap(), start_offset: start_offset.unwrap(), end_id: end_id.unwrap(), end_offset: end_offset.unwrap(), version_in_range, new_text: new_text.clone(), local_timestamp, lamport_timestamp, }); start_id = None; start_offset = None; end_id = None; end_offset = None; version_in_range = time::Global::new(); cur_range = old_ranges.next(); if cur_range.is_some() { local_timestamp = local_clock.tick(); lamport_timestamp = lamport_clock.tick(); } break; } } else { break; } } // If the splice we are currently evaluating starts after the end of the fragment // that the cursor is parked at, we should seek to the next splice's start range // and push all the fragments in between into the new tree. if cur_range.as_ref().map_or(false, |r| r.start > fragment_end) { new_fragments.push_tree( cursor.slice(&cur_range.as_ref().unwrap().start, SeekBias::Right), ); } } } // Handle range that is at the end of the buffer if it exists. There should never be // multiple because ranges must be disjoint. if cur_range.is_some() { debug_assert_eq!(old_ranges.next(), None); let last_fragment = new_fragments.last().unwrap(); ops.push(Operation::Edit { start_id: last_fragment.insertion.id, start_offset: last_fragment.end_offset, end_id: last_fragment.insertion.id, end_offset: last_fragment.end_offset, version_in_range: time::Global::new(), new_text: new_text.clone(), local_timestamp, lamport_timestamp, }); if let Some(new_text) = new_text { new_fragments.push(self.build_fragment_to_insert( &last_fragment, None, new_text, local_timestamp, lamport_timestamp, )); } } else { new_fragments .push_tree(cursor.slice(&old_fragments.extent::(), SeekBias::Right)); } self.fragments = new_fragments; ops } fn split_fragment( &mut self, prev_fragment: &Fragment, fragment: &Fragment, range: Range, ) -> (Option, Option, Option) { debug_assert!(range.start >= fragment.start_offset); debug_assert!(range.start <= fragment.end_offset); debug_assert!(range.end <= fragment.end_offset); debug_assert!(range.end >= fragment.start_offset); if range.end == fragment.start_offset { (None, None, Some(fragment.clone())) } else if range.start == fragment.end_offset { (Some(fragment.clone()), None, None) } else if range.start == fragment.start_offset && range.end == fragment.end_offset { (None, Some(fragment.clone()), None) } else { let mut prefix = fragment.clone(); let after_range = if range.end < fragment.end_offset { let mut suffix = prefix.clone(); suffix.start_offset = range.end; prefix.end_offset = range.end; prefix.id = FragmentId::between(&prev_fragment.id, &suffix.id); Some(suffix) } else { None }; let within_range = if range.start != range.end { let mut suffix = prefix.clone(); suffix.start_offset = range.start; prefix.end_offset = range.start; prefix.id = FragmentId::between(&prev_fragment.id, &suffix.id); Some(suffix) } else { None }; let before_range = if range.start > fragment.start_offset { Some(prefix) } else { None }; let old_split_tree = self .insertion_splits .remove(&fragment.insertion.id) .unwrap(); let mut cursor = old_split_tree.cursor(); let mut new_split_tree = cursor.slice(&fragment.start_offset, SeekBias::Right); if let Some(ref fragment) = before_range { new_split_tree.push(InsertionSplit { extent: range.start - fragment.start_offset, fragment_id: fragment.id.clone(), }); } if let Some(ref fragment) = within_range { new_split_tree.push(InsertionSplit { extent: range.end - range.start, fragment_id: fragment.id.clone(), }); } if let Some(ref fragment) = after_range { new_split_tree.push(InsertionSplit { extent: fragment.end_offset - range.end, fragment_id: fragment.id.clone(), }); } cursor.next(); new_split_tree .push_tree(cursor.slice(&old_split_tree.extent::(), SeekBias::Right)); self.insertion_splits .insert(fragment.insertion.id, new_split_tree); (before_range, within_range, after_range) } } fn build_fragment_to_insert( &mut self, prev_fragment: &Fragment, next_fragment: Option<&Fragment>, text: Arc, local_timestamp: time::Local, lamport_timestamp: time::Lamport, ) -> Fragment { let new_fragment_id = FragmentId::between( &prev_fragment.id, next_fragment .map(|f| &f.id) .unwrap_or(&FragmentId::max_value()), ); let mut split_tree = btree::Tree::new(); split_tree.push(InsertionSplit { extent: text.len(), fragment_id: new_fragment_id.clone(), }); self.insertion_splits.insert(local_timestamp, split_tree); Fragment::new( new_fragment_id, Insertion { id: local_timestamp, parent_id: prev_fragment.insertion.id, offset_in_parent: prev_fragment.end_offset, text, lamport_timestamp, }, ) } pub fn anchor_before_offset(&self, offset: usize) -> Result { self.anchor_for_offset(offset, AnchorBias::Left) } pub fn anchor_after_offset(&self, offset: usize) -> Result { self.anchor_for_offset(offset, AnchorBias::Right) } fn anchor_for_offset(&self, offset: usize, bias: AnchorBias) -> Result { let max_offset = self.len(); if offset > max_offset { return Err(Error::OffsetOutOfRange); } let seek_bias; match bias { AnchorBias::Left => { if offset == 0 { return Ok(Anchor::Start); } else { seek_bias = SeekBias::Left; } } AnchorBias::Right => { if offset == max_offset { return Ok(Anchor::End); } else { seek_bias = SeekBias::Right; } } }; let mut cursor = self.fragments.cursor(); cursor.seek(&offset, seek_bias); let fragment = cursor.item().unwrap(); let offset_in_fragment = offset - cursor.start::(); let offset_in_insertion = fragment.start_offset + offset_in_fragment; let point = cursor.start::() + &fragment.point_for_offset(offset_in_fragment)?; let anchor = Anchor::Middle { insertion_id: fragment.insertion.id, offset: offset_in_insertion, bias, }; self.cache_position(Some(anchor.clone()), offset, point); Ok(anchor) } pub fn anchor_before_point(&self, point: Point) -> Result { self.anchor_for_point(point, AnchorBias::Left) } pub fn anchor_after_point(&self, point: Point) -> Result { self.anchor_for_point(point, AnchorBias::Right) } fn anchor_for_point(&self, point: Point, bias: AnchorBias) -> Result { let max_point = self.max_point(); if point > max_point { return Err(Error::OffsetOutOfRange); } let seek_bias; match bias { AnchorBias::Left => { if point.is_zero() { return Ok(Anchor::Start); } else { seek_bias = SeekBias::Left; } } AnchorBias::Right => { if point == max_point { return Ok(Anchor::End); } else { seek_bias = SeekBias::Right; } } }; let mut cursor = self.fragments.cursor(); cursor.seek(&point, seek_bias); let fragment = cursor.item().unwrap(); let offset_in_fragment = fragment.offset_for_point(point - &cursor.start::())?; let offset_in_insertion = fragment.start_offset + offset_in_fragment; let anchor = Anchor::Middle { insertion_id: fragment.insertion.id, offset: offset_in_insertion, bias, }; let offset = cursor.start::() + offset_in_fragment; self.cache_position(Some(anchor.clone()), offset, point); Ok(anchor) } pub fn offset_for_anchor(&self, anchor: &Anchor) -> Result { Ok(self.position_for_anchor(anchor)?.0) } pub fn point_for_anchor(&self, anchor: &Anchor) -> Result { Ok(self.position_for_anchor(anchor)?.1) } fn position_for_anchor(&self, anchor: &Anchor) -> Result<(usize, Point), Error> { match anchor { Anchor::Start => Ok((0, Point { row: 0, column: 0 })), Anchor::End => Ok((self.len(), self.fragments.extent())), Anchor::Middle { ref insertion_id, offset, ref bias, } => { let cached_position = { let anchor_cache = self.anchor_cache.try_borrow().ok(); anchor_cache .as_ref() .and_then(|cache| cache.get(anchor).cloned()) }; if let Some(cached_position) = cached_position { Ok(cached_position) } else { let seek_bias = match bias { AnchorBias::Left => SeekBias::Left, AnchorBias::Right => SeekBias::Right, }; let splits = self.insertion_splits .get(&insertion_id) .ok_or(Error::InvalidAnchor( "split does not exist for insertion id".into(), ))?; let mut splits_cursor = splits.cursor(); splits_cursor.seek(offset, seek_bias); splits_cursor .item() .ok_or(Error::InvalidAnchor("split offset is out of range".into())) .and_then(|split| { let mut fragments_cursor = self.fragments.cursor(); fragments_cursor.seek(&split.fragment_id, SeekBias::Left); fragments_cursor .item() .ok_or(Error::InvalidAnchor("fragment id does not exist".into())) .and_then(|fragment| { let overshoot = if fragment.is_visible() { offset - fragment.start_offset } else { 0 }; let offset = fragments_cursor.start::() + overshoot; let point = fragments_cursor.start::() + &fragment.point_for_offset(overshoot)?; self.cache_position(Some(anchor.clone()), offset, point); Ok((offset, point)) }) }) } } } } fn offset_for_point(&self, point: Point) -> Result { let cached_offset = { let offset_cache = self.offset_cache.try_borrow().ok(); offset_cache .as_ref() .and_then(|cache| cache.get(&point).cloned()) }; if let Some(cached_offset) = cached_offset { Ok(cached_offset) } else { let mut fragments_cursor = self.fragments.cursor(); fragments_cursor.seek(&point, SeekBias::Left); fragments_cursor .item() .ok_or(Error::OffsetOutOfRange) .map(|fragment| { let overshoot = fragment .offset_for_point(point - &fragments_cursor.start::()) .unwrap(); let offset = &fragments_cursor.start::() + &overshoot; self.cache_position(None, offset, point); offset }) } } pub fn cmp_anchors(&self, a: &Anchor, b: &Anchor) -> Result { let a_offset = self.offset_for_anchor(a)?; let b_offset = self.offset_for_anchor(b)?; Ok(a_offset.cmp(&b_offset)) } fn cache_position(&self, anchor: Option, offset: usize, point: Point) { anchor.map(|anchor| { if let Ok(mut anchor_cache) = self.anchor_cache.try_borrow_mut() { anchor_cache.insert(anchor, (offset, point)); } }); if let Ok(mut offset_cache) = self.offset_cache.try_borrow_mut() { offset_cache.insert(point, offset); } } } impl Point { pub fn new(row: u32, column: u32) -> Self { Point { row, column } } pub fn zero() -> Self { Point::new(0, 0) } pub fn is_zero(&self) -> bool { self.row == 0 && self.column == 0 } } impl btree::Dimension for Point { fn from_summary(summary: &FragmentSummary) -> Self { summary.extent_2d } } impl<'a> Add<&'a Self> for Point { type Output = Point; fn add(self, other: &'a Self) -> Self::Output { if other.row == 0 { Point::new(self.row, self.column + other.column) } else { Point::new(self.row + other.row, other.column) } } } impl<'a> Sub<&'a Self> for Point { type Output = Point; fn sub(self, other: &'a Self) -> Self::Output { debug_assert!(*other <= self); if self.row == other.row { Point::new(0, self.column - other.column) } else { Point::new(self.row - other.row, self.column) } } } impl<'a> AddAssign<&'a Self> for Point { fn add_assign(&mut self, other: &'a Self) { if other.row == 0 { self.column += other.column; } else { self.row += other.row; self.column = other.column; } } } impl PartialOrd for Point { fn partial_cmp(&self, other: &Point) -> Option { Some(self.cmp(other)) } } impl Ord for Point { #[cfg(target_pointer_width = "64")] fn cmp(&self, other: &Point) -> Ordering { let a = (self.row as usize) << 32 | self.column as usize; let b = (other.row as usize) << 32 | other.column as usize; a.cmp(&b) } #[cfg(target_pointer_width = "32")] fn cmp(&self, other: &Point) -> Ordering { match self.row.cmp(&other.row) { Ordering::Equal => self.column.cmp(&other.column), comparison @ _ => comparison, } } } impl Anchor { fn to_flatbuf<'fbb>( &self, builder: &mut FlatBufferBuilder<'fbb>, ) -> WIPOffset> { match self { Anchor::Start => serialization::buffer::Anchor::create( builder, &serialization::buffer::AnchorArgs { variant: serialization::buffer::AnchorVariant::Start, ..serialization::buffer::AnchorArgs::default() }, ), Anchor::End => serialization::buffer::Anchor::create( builder, &serialization::buffer::AnchorArgs { variant: serialization::buffer::AnchorVariant::End, ..serialization::buffer::AnchorArgs::default() }, ), Anchor::Middle { insertion_id, offset, bias, } => serialization::buffer::Anchor::create( builder, &serialization::buffer::AnchorArgs { variant: serialization::buffer::AnchorVariant::Middle, insertion_id: Some(&insertion_id.to_flatbuf()), offset: *offset as u64, bias: bias.to_flatbuf(), }, ), } } fn from_flatbuf<'fbb>( message: &serialization::buffer::Anchor<'fbb>, ) -> Result { match message.variant() { serialization::buffer::AnchorVariant::Start => Ok(Anchor::Start), serialization::buffer::AnchorVariant::End => Ok(Anchor::End), serialization::buffer::AnchorVariant::Middle => Ok(Anchor::Middle { insertion_id: time::Local::from_flatbuf( message .insertion_id() .ok_or(crate::Error::DeserializeError)?, ), offset: message.offset() as usize, bias: AnchorBias::from_flatbuf(message.bias()), }), } } } impl AnchorBias { fn to_flatbuf(&self) -> serialization::buffer::AnchorBias { match self { AnchorBias::Left => serialization::buffer::AnchorBias::Left, AnchorBias::Right => serialization::buffer::AnchorBias::Right, } } fn from_flatbuf(message: serialization::buffer::AnchorBias) -> Self { match message { serialization::buffer::AnchorBias::Left => AnchorBias::Left, serialization::buffer::AnchorBias::Right => AnchorBias::Right, } } } impl Iter { fn new(buffer: &Buffer) -> Self { let mut fragment_cursor = buffer.fragments.cursor(); fragment_cursor.seek(&0, SeekBias::Right); Self { fragment_cursor, fragment_offset: 0, reversed: false, } } fn at_point(buffer: &Buffer, point: Point) -> Self { let mut fragment_cursor = buffer.fragments.cursor(); fragment_cursor.seek(&point, SeekBias::Right); let fragment_offset = if let Some(fragment) = fragment_cursor.item() { let point_in_fragment = point - &fragment_cursor.start::(); fragment.offset_for_point(point_in_fragment).unwrap() } else { 0 }; Self { fragment_cursor, fragment_offset, reversed: false, } } pub fn rev(mut self) -> Iter { self.reversed = true; self } pub fn into_string(self) -> String { String::from_utf16_lossy(&self.collect::>()) } } impl Iterator for Iter { type Item = u16; fn next(&mut self) -> Option { if self.reversed { if let Some(fragment) = self.fragment_cursor.item() { if self.fragment_offset > 0 { self.fragment_offset -= 1; if let Some(c) = fragment.code_unit(self.fragment_offset) { return Some(c); } } } loop { self.fragment_cursor.prev(); if let Some(fragment) = self.fragment_cursor.item() { if fragment.len() > 0 { self.fragment_offset = fragment.len() - 1; return fragment.code_unit(self.fragment_offset); } } else { break; } } None } else { if let Some(fragment) = self.fragment_cursor.item() { if let Some(c) = fragment.code_unit(self.fragment_offset) { self.fragment_offset += 1; return Some(c); } } loop { self.fragment_cursor.next(); if let Some(fragment) = self.fragment_cursor.item() { if let Some(c) = fragment.code_unit(0) { self.fragment_offset = 1; return Some(c); } } else { break; } } None } } } impl bool> Iterator for ChangesIter { type Item = Change; fn next(&mut self) -> Option { let mut change: Option = None; while let Some(fragment) = self.cursor.item() { let position = self.cursor.start(); if !fragment.was_visible(&self.since) && fragment.is_visible() { if let Some(ref mut change) = change { if change.range.start + &change.new_extent == position { change.code_units.extend(fragment.code_units()); change.new_extent += &fragment.extent_2d(); } else { break; } } else { change = Some(Change { range: position..position, code_units: Vec::from(fragment.code_units()), new_extent: fragment.extent_2d(), }); } } else if fragment.was_visible(&self.since) && !fragment.is_visible() { if let Some(ref mut change) = change { if change.range.start + &change.new_extent == position { change.range.end += &fragment.extent_2d(); } else { break; } } else { change = Some(Change { range: position..position + &fragment.extent_2d(), code_units: Vec::new(), new_extent: Point::zero(), }); } } self.cursor.next(); } change } } pub fn diff(a: &[u16], b: &[u16]) -> Vec { struct ChangeCollector<'a> { a: &'a [u16], b: &'a [u16], position: Point, changes: Vec, } impl<'a> diffs::Diff for ChangeCollector<'a> { type Error = (); fn equal(&mut self, old: usize, _: usize, len: usize) -> Result<(), ()> { self.position += &Text::extent(&self.a[old..old + len]); Ok(()) } fn delete(&mut self, old: usize, len: usize) -> Result<(), ()> { self.changes.push(Change { range: self.position..self.position + &Text::extent(&self.a[old..old + len]), code_units: Vec::new(), new_extent: Point::zero(), }); Ok(()) } fn insert(&mut self, _: usize, new: usize, new_len: usize) -> Result<(), ()> { let new_extent = Text::extent(&self.b[new..new + new_len]); self.changes.push(Change { range: self.position..self.position, code_units: Vec::from(&self.b[new..new + new_len]), new_extent, }); self.position += &new_extent; Ok(()) } fn replace( &mut self, old: usize, old_len: usize, new: usize, new_len: usize, ) -> Result<(), ()> { let old_extent = Text::extent(&self.a[old..old + old_len]); let new_extent = Text::extent(&self.b[new..new + new_len]); self.changes.push(Change { range: self.position..self.position + &old_extent, code_units: Vec::from(&self.b[new..new + new_len]), new_extent, }); self.position += &new_extent; Ok(()) } } let mut collector = diffs::Replace::new(ChangeCollector { a, b, position: Point::zero(), changes: Vec::new(), }); diffs::myers::diff(&mut collector, a, 0, a.len(), b, 0, b.len()).unwrap(); collector.into_inner().changes } impl Selection { pub fn head(&self) -> &Anchor { if self.reversed { &self.start } else { &self.end } } pub fn set_head(&mut self, buffer: &Buffer, cursor: Anchor) { if buffer.cmp_anchors(&cursor, self.tail()).unwrap() < Ordering::Equal { if !self.reversed { mem::swap(&mut self.start, &mut self.end); self.reversed = true; } self.start = cursor; } else { if self.reversed { mem::swap(&mut self.start, &mut self.end); self.reversed = false; } self.end = cursor; } } pub fn tail(&self) -> &Anchor { if self.reversed { &self.end } else { &self.start } } pub fn is_empty(&self, buffer: &Buffer) -> bool { buffer.cmp_anchors(&self.start, &self.end).unwrap() == Ordering::Equal } pub fn anchor_range(&self) -> Range { self.start.clone()..self.end.clone() } fn to_flatbuf<'fbb>( &self, builder: &mut FlatBufferBuilder<'fbb>, ) -> WIPOffset> { let start = Some(self.start.to_flatbuf(builder)); let end = Some(self.end.to_flatbuf(builder)); serialization::buffer::Selection::create( builder, &serialization::buffer::SelectionArgs { start, end, reversed: self.reversed, }, ) } fn from_flatbuf<'fbb>( message: serialization::buffer::Selection<'fbb>, ) -> Result { Ok(Self { start: Anchor::from_flatbuf(&message.start().ok_or(crate::Error::DeserializeError)?)?, end: Anchor::from_flatbuf(&message.end().ok_or(crate::Error::DeserializeError)?)?, reversed: message.reversed(), }) } } impl Text { pub fn new(code_units: Vec) -> Self { fn build_tree(index: usize, line_lengths: &[u32], mut tree: &mut [LineNode]) { if line_lengths.is_empty() { return; } let mid = if line_lengths.len() == 1 { 0 } else { let depth = log2_fast(line_lengths.len()); let max_elements = (1 << (depth)) - 1; let right_subtree_elements = 1 << (depth - 1); cmp::min(line_lengths.len() - right_subtree_elements, max_elements) }; let len = line_lengths[mid]; let lower = &line_lengths[0..mid]; let upper = &line_lengths[mid + 1..]; let left_child_index = index * 2 + 1; let right_child_index = index * 2 + 2; build_tree(left_child_index, lower, &mut tree); build_tree(right_child_index, upper, &mut tree); tree[index] = { let mut left_child_longest_row = 0; let mut left_child_longest_row_len = 0; let mut left_child_offset = 0; let mut left_child_rows = 0; if let Some(left_child) = tree.get(left_child_index) { left_child_longest_row = left_child.longest_row; left_child_longest_row_len = left_child.longest_row_len; left_child_offset = left_child.offset; left_child_rows = left_child.rows; } let mut right_child_longest_row = 0; let mut right_child_longest_row_len = 0; let mut right_child_offset = 0; let mut right_child_rows = 0; if let Some(right_child) = tree.get(right_child_index) { right_child_longest_row = right_child.longest_row; right_child_longest_row_len = right_child.longest_row_len; right_child_offset = right_child.offset; right_child_rows = right_child.rows; } let mut longest_row = 0; let mut longest_row_len = 0; if left_child_longest_row_len > longest_row_len { longest_row = left_child_longest_row; longest_row_len = left_child_longest_row_len; } if len > longest_row_len { longest_row = left_child_rows; longest_row_len = len; } if right_child_longest_row_len > longest_row_len { longest_row = left_child_rows + right_child_longest_row + 1; longest_row_len = right_child_longest_row_len; } LineNode { len, longest_row, longest_row_len, offset: left_child_offset + len as usize + right_child_offset + 1, rows: left_child_rows + right_child_rows + 1, } }; } let mut line_lengths = Vec::new(); let mut prev_offset = 0; for (offset, code_unit) in code_units.iter().enumerate() { if code_unit == &u16::from(b'\n') { line_lengths.push((offset - prev_offset) as u32); prev_offset = offset + 1; } } line_lengths.push((code_units.len() - prev_offset) as u32); let mut nodes = Vec::new(); nodes.resize( line_lengths.len(), LineNode { len: 0, longest_row_len: 0, longest_row: 0, offset: 0, rows: 0, }, ); build_tree(0, &line_lengths, &mut nodes); Self { code_units, nodes } } fn extent(code_units: &[u16]) -> Point { let mut rows = 0; let mut last_row_len = 0; for ch in code_units { if *ch == b'\n' as u16 { rows += 1; last_row_len = 0; } else { last_row_len += 1; } } Point::new(rows, last_row_len) } fn len(&self) -> usize { self.code_units.len() } fn longest_row_in_range(&self, target_range: Range) -> Result<(u32, u32), Error> { let mut longest_row = 0; let mut longest_row_len = 0; self.search(|probe| { if target_range.start <= probe.offset_range.end && probe.right_ancestor_start_offset <= target_range.end { if let Some(right_child) = probe.right_child { if right_child.longest_row_len >= longest_row_len { longest_row = probe.row + 1 + right_child.longest_row; longest_row_len = right_child.longest_row_len; } } } if target_range.start < probe.offset_range.start { if probe.offset_range.end < target_range.end && probe.node.len >= longest_row_len { longest_row = probe.row; longest_row_len = probe.node.len; } Ordering::Less } else if target_range.start > probe.offset_range.end { Ordering::Greater } else { let node_end = cmp::min(probe.offset_range.end, target_range.end); let node_len = (node_end - target_range.start) as u32; if node_len >= longest_row_len { longest_row = probe.row; longest_row_len = node_len; } Ordering::Equal } }) .ok_or(Error::OffsetOutOfRange)?; self.search(|probe| { if target_range.end >= probe.offset_range.start && probe.left_ancestor_end_offset >= target_range.start { if let Some(left_child) = probe.left_child { if left_child.longest_row_len > longest_row_len { let left_ancestor_row = probe.row - left_child.rows; longest_row = left_ancestor_row + left_child.longest_row; longest_row_len = left_child.longest_row_len; } } } if target_range.end < probe.offset_range.start { Ordering::Less } else if target_range.end > probe.offset_range.end { if target_range.start < probe.offset_range.start && probe.node.len > longest_row_len { longest_row = probe.row; longest_row_len = probe.node.len; } Ordering::Greater } else { let node_start = cmp::max(target_range.start, probe.offset_range.start); let node_len = (target_range.end - node_start) as u32; if node_len > longest_row_len { longest_row = probe.row; longest_row_len = node_len; } Ordering::Equal } }) .ok_or(Error::OffsetOutOfRange)?; Ok((longest_row, longest_row_len)) } fn point_for_offset(&self, offset: usize) -> Result { let search_result = self.search(|probe| { if offset < probe.offset_range.start { Ordering::Less } else if offset > probe.offset_range.end { Ordering::Greater } else { Ordering::Equal } }); if let Some((offset_range, row, _)) = search_result { Ok(Point::new(row, (offset - offset_range.start) as u32)) } else { Err(Error::OffsetOutOfRange) } } fn offset_for_point(&self, point: Point) -> Result { if let Some((offset_range, _, node)) = self.search(|probe| point.row.cmp(&probe.row)) { if point.column <= node.len { Ok(offset_range.start + point.column as usize) } else { Err(Error::OffsetOutOfRange) } } else { Err(Error::OffsetOutOfRange) } } fn search(&self, mut f: F) -> Option<(Range, u32, &LineNode)> where F: FnMut(LineNodeProbe) -> Ordering, { let mut left_ancestor_end_offset = 0; let mut left_ancestor_row = 0; let mut right_ancestor_start_offset = self.nodes[0].offset; let mut cur_node_index = 0; while let Some(cur_node) = self.nodes.get(cur_node_index) { let left_child = self.nodes.get(cur_node_index * 2 + 1); let right_child = self.nodes.get(cur_node_index * 2 + 2); let cur_offset_range = { let start = left_ancestor_end_offset + left_child.map_or(0, |node| node.offset); let end = start + cur_node.len as usize; start..end }; let cur_row = left_ancestor_row + left_child.map_or(0, |node| node.rows); match f(LineNodeProbe { offset_range: &cur_offset_range, row: cur_row, left_ancestor_end_offset, right_ancestor_start_offset, node: cur_node, left_child, right_child, }) { Ordering::Less => { cur_node_index = cur_node_index * 2 + 1; right_ancestor_start_offset = cur_offset_range.start; } Ordering::Equal => return Some((cur_offset_range, cur_row, cur_node)), Ordering::Greater => { cur_node_index = cur_node_index * 2 + 2; left_ancestor_end_offset = cur_offset_range.end + 1; left_ancestor_row = cur_row + 1; } } } None } } impl<'a> From<&'a str> for Text { fn from(s: &'a str) -> Self { Self::new(s.encode_utf16().collect()) } } impl From for Text { fn from(s: String) -> Self { Self::from(s.as_str()) } } impl<'a> From> for Text { fn from(s: Vec) -> Self { Self::new(s) } } #[inline(always)] fn log2_fast(x: usize) -> usize { 8 * mem::size_of::() - (x.leading_zeros() as usize) - 1 } lazy_static! { static ref FRAGMENT_ID_MIN_VALUE: FragmentId = FragmentId(Arc::new(vec![0 as u16])); static ref FRAGMENT_ID_MAX_VALUE: FragmentId = FragmentId(Arc::new(vec![u16::max_value()])); } impl FragmentId { fn min_value() -> Self { FRAGMENT_ID_MIN_VALUE.clone() } fn max_value() -> Self { FRAGMENT_ID_MAX_VALUE.clone() } fn between(left: &Self, right: &Self) -> Self { Self::between_with_max(left, right, u16::max_value()) } fn between_with_max(left: &Self, right: &Self, max_value: u16) -> Self { let mut new_entries = Vec::new(); let left_entries = left.0.iter().cloned().chain(iter::repeat(0)); let right_entries = right.0.iter().cloned().chain(iter::repeat(max_value)); for (l, r) in left_entries.zip(right_entries) { let interval = r - l; if interval > 1 { new_entries.push(l + interval / 2); break; } else { new_entries.push(l); } } FragmentId(Arc::new(new_entries)) } } impl btree::Dimension for FragmentId { fn from_summary(summary: &FragmentSummary) -> Self { summary.max_fragment_id.clone() } } impl<'a> Add<&'a Self> for FragmentId { type Output = FragmentId; fn add(self, other: &'a Self) -> Self::Output { debug_assert!(self <= *other); other.clone() } } impl<'a> AddAssign<&'a Self> for FragmentId { fn add_assign(&mut self, other: &'a Self) { debug_assert!(*self <= *other); *self = other.clone(); } } impl Fragment { fn new(id: FragmentId, insertion: Insertion) -> Self { let end_offset = insertion.text.len(); Self { id, insertion, start_offset: 0, end_offset, deletions: HashSet::new(), } } fn code_unit(&self, offset: usize) -> Option { if offset < self.len() { Some(self.insertion.text.code_units[self.start_offset + offset].clone()) } else { None } } fn code_units(&self) -> &[u16] { &self.insertion.text.code_units[self.start_offset..self.end_offset] } fn len(&self) -> usize { if self.is_visible() { self.extent() } else { 0 } } fn extent(&self) -> usize { self.end_offset - self.start_offset } fn extent_2d(&self) -> Point { self.point_for_offset(self.extent()).unwrap() } fn is_visible(&self) -> bool { self.deletions.is_empty() } fn was_visible(&self, version: &time::Global) -> bool { version.observed(self.insertion.id) && self.deletions.iter().all(|d| !version.observed(*d)) } fn point_for_offset(&self, offset: usize) -> Result { let text = &self.insertion.text; let offset_in_insertion = self.start_offset + offset; Ok(text.point_for_offset(offset_in_insertion)? - &text.point_for_offset(self.start_offset)?) } fn offset_for_point(&self, point: Point) -> Result { let text = &self.insertion.text; let point_in_insertion = text.point_for_offset(self.start_offset)? + &point; Ok(text.offset_for_point(point_in_insertion)? - self.start_offset) } } impl btree::Item for Fragment { type Summary = FragmentSummary; fn summarize(&self) -> Self::Summary { let mut max_version = time::Global::new(); max_version.observe(self.insertion.id); for deletion in &self.deletions { max_version.observe(*deletion); } if self.is_visible() { let fragment_2d_start = self .insertion .text .point_for_offset(self.start_offset) .unwrap(); let fragment_2d_end = self .insertion .text .point_for_offset(self.end_offset) .unwrap(); let first_row_len = if fragment_2d_start.row == fragment_2d_end.row { self.extent() as u32 } else { self.offset_for_point(Point::new(1, 0)).unwrap() as u32 - 1 }; let (longest_row, longest_row_len) = self .insertion .text .longest_row_in_range(self.start_offset as usize..self.end_offset as usize) .unwrap(); FragmentSummary { extent: self.len(), extent_2d: fragment_2d_end - &fragment_2d_start, max_fragment_id: self.id.clone(), first_row_len, longest_row: longest_row - fragment_2d_start.row, longest_row_len, max_version, } } else { FragmentSummary { extent: 0, extent_2d: Point { row: 0, column: 0 }, max_fragment_id: self.id.clone(), first_row_len: 0, longest_row: 0, longest_row_len: 0, max_version, } } } } impl<'a> AddAssign<&'a FragmentSummary> for FragmentSummary { fn add_assign(&mut self, other: &Self) { let last_row_len = self.extent_2d.column + other.first_row_len; if last_row_len > self.longest_row_len { self.longest_row = self.extent_2d.row; self.longest_row_len = last_row_len; } if other.longest_row_len > self.longest_row_len { self.longest_row = self.extent_2d.row + other.longest_row; self.longest_row_len = other.longest_row_len; } if self.extent_2d.row == 0 { self.first_row_len += other.first_row_len; } self.extent += other.extent; self.extent_2d += &other.extent_2d; debug_assert!(self.max_fragment_id <= other.max_fragment_id); self.max_fragment_id = other.max_fragment_id.clone(); self.max_version.observe_all(&other.max_version); } } impl Default for FragmentSummary { fn default() -> Self { FragmentSummary { extent: 0, extent_2d: Point { row: 0, column: 0 }, max_fragment_id: FragmentId::min_value(), first_row_len: 0, longest_row: 0, longest_row_len: 0, max_version: time::Global::new(), } } } impl btree::Dimension for usize { fn from_summary(summary: &FragmentSummary) -> Self { summary.extent } } impl btree::Item for InsertionSplit { type Summary = InsertionSplitSummary; fn summarize(&self) -> Self::Summary { InsertionSplitSummary { extent: self.extent, } } } impl<'a> AddAssign<&'a InsertionSplitSummary> for InsertionSplitSummary { fn add_assign(&mut self, other: &Self) { self.extent += other.extent; } } impl Default for InsertionSplitSummary { fn default() -> Self { InsertionSplitSummary { extent: 0 } } } impl btree::Dimension for usize { fn from_summary(summary: &InsertionSplitSummary) -> Self { summary.extent } } impl Operation { fn replica_id(&self) -> ReplicaId { self.lamport_timestamp().replica_id } fn lamport_timestamp(&self) -> time::Lamport { match self { Operation::Edit { lamport_timestamp, .. } => *lamport_timestamp, Operation::UpdateSelections { lamport_timestamp, .. } => *lamport_timestamp, } } pub fn is_edit(&self) -> bool { match self { Operation::Edit { .. } => true, _ => false, } } pub fn to_flatbuf<'fbb>( &self, builder: &mut FlatBufferBuilder<'fbb>, ) -> WIPOffset> { let variant_type; let variant; match self { Operation::Edit { start_id, start_offset, end_id, end_offset, version_in_range, new_text, local_timestamp, lamport_timestamp, } => { let new_text = new_text.as_ref().map(|new_text| { builder.create_string(String::from_utf16_lossy(&new_text.code_units).as_str()) }); let version_in_range = Some(version_in_range.to_flatbuf(builder)); variant_type = serialization::buffer::OperationVariant::Edit; variant = serialization::buffer::Edit::create( builder, &serialization::buffer::EditArgs { start_id: Some(&start_id.to_flatbuf()), start_offset: *start_offset as u64, end_id: Some(&end_id.to_flatbuf()), end_offset: *end_offset as u64, version_in_range, new_text, local_timestamp: Some(&local_timestamp.to_flatbuf()), lamport_timestamp: Some(&lamport_timestamp.to_flatbuf()), }, ) .as_union_value(); } Operation::UpdateSelections { set_id, selections, lamport_timestamp, } => { variant_type = serialization::buffer::OperationVariant::UpdateSelections; let selections = selections.as_ref().map(|selections| { let selection_flatbufs = &selections .iter() .map(|s| s.to_flatbuf(builder)) .collect::>(); builder.create_vector(selection_flatbufs) }); variant = serialization::buffer::UpdateSelections::create( builder, &serialization::buffer::UpdateSelectionsArgs { set_id: Some(&set_id.to_flatbuf()), selections, lamport_timestamp: Some(&lamport_timestamp.to_flatbuf()), }, ) .as_union_value(); } } serialization::buffer::Operation::create( builder, &serialization::buffer::OperationArgs { variant_type, variant: Some(variant), }, ) } pub fn from_flatbuf<'fbb>( message: &serialization::buffer::Operation<'fbb>, ) -> Result, crate::Error> { match message.variant_type() { serialization::buffer::OperationVariant::Edit => { let message = serialization::buffer::Edit::init_from_table( message.variant().ok_or(crate::Error::DeserializeError)?, ); Ok(Some(Operation::Edit { start_id: time::Local::from_flatbuf( message.start_id().ok_or(crate::Error::DeserializeError)?, ), start_offset: message.start_offset() as usize, end_id: time::Local::from_flatbuf( message.end_id().ok_or(crate::Error::DeserializeError)?, ), end_offset: message.end_offset() as usize, version_in_range: time::Global::from_flatbuf( message .version_in_range() .ok_or(crate::Error::DeserializeError)?, )?, new_text: message.new_text().map(|new_text| Arc::new(new_text.into())), local_timestamp: time::Local::from_flatbuf( message .local_timestamp() .ok_or(crate::Error::DeserializeError)?, ), lamport_timestamp: time::Lamport::from_flatbuf( message .lamport_timestamp() .ok_or(crate::Error::DeserializeError)?, ), })) } serialization::buffer::OperationVariant::UpdateSelections => { let message = serialization::buffer::UpdateSelections::init_from_table( message.variant().ok_or(crate::Error::DeserializeError)?, ); let selections = if let Some(flatbufs) = message.selections() { let mut selections = Vec::with_capacity(flatbufs.len()); for i in 0..flatbufs.len() { selections.push(Selection::from_flatbuf(flatbufs.get(i))?); } Some(selections) } else { None }; Ok(Some(Operation::UpdateSelections { set_id: time::Lamport::from_flatbuf( message.set_id().ok_or(crate::Error::DeserializeError)?, ), selections, lamport_timestamp: time::Lamport::from_flatbuf( message .lamport_timestamp() .ok_or(crate::Error::DeserializeError)?, ), })) } serialization::buffer::OperationVariant::NONE => Ok(None), } } } impl operation_queue::Operation for Operation { fn timestamp(&self) -> time::Lamport { self.lamport_timestamp() } } #[cfg(test)] mod tests { use super::*; use rand::{Rng, SeedableRng, StdRng}; use uuid::Uuid; #[test] fn test_edit() { let replica_id = Uuid::from_u128(1); let mut local_clock = time::Local::new(replica_id); let mut lamport_clock = time::Lamport::new(replica_id); let mut buffer = Buffer::new("abc"); assert_eq!(buffer.to_string(), "abc"); buffer.edit(vec![3..3], "def", &mut local_clock, &mut lamport_clock); assert_eq!(buffer.to_string(), "abcdef"); buffer.edit(vec![0..0], "ghi", &mut local_clock, &mut lamport_clock); assert_eq!(buffer.to_string(), "ghiabcdef"); buffer.edit(vec![5..5], "jkl", &mut local_clock, &mut lamport_clock); assert_eq!(buffer.to_string(), "ghiabjklcdef"); buffer.edit(vec![6..7], "", &mut local_clock, &mut lamport_clock); assert_eq!(buffer.to_string(), "ghiabjlcdef"); buffer.edit(vec![4..9], "mno", &mut local_clock, &mut lamport_clock); assert_eq!(buffer.to_string(), "ghiamnoef"); } #[test] fn test_random_edits() { for seed in 0..100 { println!("{:?}", seed); let mut rng = StdRng::from_seed(&[seed]); let mut reference_string = RandomCharIter(rng) .take(rng.gen_range(0, 10)) .collect::(); let mut buffer = Buffer::new(reference_string.as_str()); let mut buffer_versions = Vec::new(); let replica_id = Uuid::from_u128(1); let mut local_clock = time::Local::new(replica_id); let mut lamport_clock = time::Lamport::new(replica_id); for _i in 0..10 { let (old_ranges, new_text, _) = buffer.randomly_mutate(&mut rng, &mut local_clock, &mut lamport_clock); for old_range in old_ranges.iter().rev() { reference_string = [ &reference_string[0..old_range.start], new_text.as_str(), &reference_string[old_range.end..], ] .concat(); } assert_eq!(buffer.to_string(), reference_string); if rng.gen_weighted_bool(3) { buffer_versions.push(buffer.clone()); } } for mut old_buffer in buffer_versions { for change in buffer.changes_since(&old_buffer.version) { old_buffer.edit_2d( Some(change.range), Text::new(change.code_units), &mut local_clock, &mut lamport_clock, ); } assert_eq!(old_buffer.to_string(), buffer.to_string()); } } } #[test] fn test_len_for_row() { let mut buffer = Buffer::new(""); let replica_id = Uuid::from_u128(1); let mut local_clock = time::Local::new(replica_id); let mut lamport_clock = time::Lamport::new(replica_id); buffer.edit( vec![0..0], "abcd\nefg\nhij", &mut local_clock, &mut lamport_clock, ); buffer.edit( vec![12..12], "kl\nmno", &mut local_clock, &mut lamport_clock, ); buffer.edit( vec![18..18], "\npqrs\n", &mut local_clock, &mut lamport_clock, ); buffer.edit(vec![18..21], "\nPQ", &mut local_clock, &mut lamport_clock); assert_eq!(buffer.len_for_row(0), Ok(4)); assert_eq!(buffer.len_for_row(1), Ok(3)); assert_eq!(buffer.len_for_row(2), Ok(5)); assert_eq!(buffer.len_for_row(3), Ok(3)); assert_eq!(buffer.len_for_row(4), Ok(4)); assert_eq!(buffer.len_for_row(5), Ok(0)); assert_eq!(buffer.len_for_row(6), Err(Error::OffsetOutOfRange)); } #[test] fn test_longest_row() { let mut buffer = Buffer::new(""); let replica_id = Uuid::from_u128(1); let mut local_clock = time::Local::new(replica_id); let mut lamport_clock = time::Lamport::new(replica_id); assert_eq!(buffer.longest_row(), 0); buffer.edit( vec![0..0], "abcd\nefg\nhij", &mut local_clock, &mut lamport_clock, ); assert_eq!(buffer.longest_row(), 0); buffer.edit( vec![12..12], "kl\nmno", &mut local_clock, &mut lamport_clock, ); assert_eq!(buffer.longest_row(), 2); buffer.edit(vec![18..18], "\npqrs", &mut local_clock, &mut lamport_clock); assert_eq!(buffer.longest_row(), 2); buffer.edit(vec![10..12], "", &mut local_clock, &mut lamport_clock); assert_eq!(buffer.longest_row(), 0); buffer.edit(vec![24..24], "tuv", &mut local_clock, &mut lamport_clock); assert_eq!(buffer.longest_row(), 4); } #[test] fn test_iter_starting_at_point() { let mut buffer = Buffer::new(""); let replica_id = Uuid::from_u128(1); let mut local_clock = time::Local::new(replica_id); let mut lamport_clock = time::Lamport::new(replica_id); buffer.edit( vec![0..0], "abcd\nefgh\nij", &mut local_clock, &mut lamport_clock, ); buffer.edit( vec![12..12], "kl\nmno", &mut local_clock, &mut lamport_clock, ); buffer.edit(vec![18..18], "\npqrs", &mut local_clock, &mut lamport_clock); buffer.edit(vec![18..21], "\nPQ", &mut local_clock, &mut lamport_clock); let cursor = buffer.iter_at_point(Point::new(0, 0)); assert_eq!(cursor.into_string(), "abcd\nefgh\nijkl\nmno\nPQrs"); let cursor = buffer.iter_at_point(Point::new(1, 0)); assert_eq!(cursor.into_string(), "efgh\nijkl\nmno\nPQrs"); let cursor = buffer.iter_at_point(Point::new(2, 0)); assert_eq!(cursor.into_string(), "ijkl\nmno\nPQrs"); let cursor = buffer.iter_at_point(Point::new(3, 0)); assert_eq!(cursor.into_string(), "mno\nPQrs"); let cursor = buffer.iter_at_point(Point::new(4, 0)); assert_eq!(cursor.into_string(), "PQrs"); let cursor = buffer.iter_at_point(Point::new(5, 0)); assert_eq!(cursor.into_string(), ""); let cursor = buffer.iter_at_point(Point::new(0, 0)).rev(); assert_eq!(cursor.into_string(), ""); let cursor = buffer.iter_at_point(Point::new(0, 3)).rev(); assert_eq!(cursor.into_string(), "cba"); let cursor = buffer.iter_at_point(Point::new(1, 4)).rev(); assert_eq!(cursor.into_string(), "hgfe\ndcba"); let cursor = buffer.iter_at_point(Point::new(3, 2)).rev(); assert_eq!(cursor.into_string(), "nm\nlkji\nhgfe\ndcba"); let cursor = buffer.iter_at_point(Point::new(4, 4)).rev(); assert_eq!(cursor.into_string(), "srQP\nonm\nlkji\nhgfe\ndcba"); let cursor = buffer.iter_at_point(Point::new(5, 0)).rev(); assert_eq!(cursor.into_string(), "srQP\nonm\nlkji\nhgfe\ndcba"); // Regression test: let mut buffer = Buffer::new(""); let replica_id = Uuid::from_u128(1); let mut local_clock = time::Local::new(replica_id); let mut lamport_clock = time::Lamport::new(replica_id); buffer.edit(vec![0..0], "[workspace]\nmembers = [\n \"xray_core\",\n \"xray_server\",\n \"xray_cli\",\n \"xray_wasm\",\n]\n", &mut local_clock, &mut lamport_clock); buffer.edit(vec![60..60], "\n", &mut local_clock, &mut lamport_clock); let cursor = buffer.iter_at_point(Point::new(6, 0)); assert_eq!(cursor.into_string(), " \"xray_wasm\",\n]\n"); } #[test] fn test_point_for_offset() { let text = Text::from("abc\ndefgh\nijklm\nopq"); assert_eq!(text.point_for_offset(0), Ok(Point { row: 0, column: 0 })); assert_eq!(text.point_for_offset(1), Ok(Point { row: 0, column: 1 })); assert_eq!(text.point_for_offset(2), Ok(Point { row: 0, column: 2 })); assert_eq!(text.point_for_offset(3), Ok(Point { row: 0, column: 3 })); assert_eq!(text.point_for_offset(4), Ok(Point { row: 1, column: 0 })); assert_eq!(text.point_for_offset(5), Ok(Point { row: 1, column: 1 })); assert_eq!(text.point_for_offset(9), Ok(Point { row: 1, column: 5 })); assert_eq!(text.point_for_offset(10), Ok(Point { row: 2, column: 0 })); assert_eq!(text.point_for_offset(14), Ok(Point { row: 2, column: 4 })); assert_eq!(text.point_for_offset(15), Ok(Point { row: 2, column: 5 })); assert_eq!(text.point_for_offset(16), Ok(Point { row: 3, column: 0 })); assert_eq!(text.point_for_offset(17), Ok(Point { row: 3, column: 1 })); assert_eq!(text.point_for_offset(19), Ok(Point { row: 3, column: 3 })); assert_eq!(text.point_for_offset(20), Err(Error::OffsetOutOfRange)); let text = Text::from("abc"); assert_eq!(text.point_for_offset(0), Ok(Point { row: 0, column: 0 })); assert_eq!(text.point_for_offset(1), Ok(Point { row: 0, column: 1 })); assert_eq!(text.point_for_offset(2), Ok(Point { row: 0, column: 2 })); assert_eq!(text.point_for_offset(3), Ok(Point { row: 0, column: 3 })); assert_eq!(text.point_for_offset(4), Err(Error::OffsetOutOfRange)); } #[test] fn test_offset_for_point() { let text = Text::from("abc\ndefgh"); assert_eq!(text.offset_for_point(Point { row: 0, column: 0 }), Ok(0)); assert_eq!(text.offset_for_point(Point { row: 0, column: 1 }), Ok(1)); assert_eq!(text.offset_for_point(Point { row: 0, column: 2 }), Ok(2)); assert_eq!(text.offset_for_point(Point { row: 0, column: 3 }), Ok(3)); assert_eq!( text.offset_for_point(Point { row: 0, column: 4 }), Err(Error::OffsetOutOfRange) ); assert_eq!(text.offset_for_point(Point { row: 1, column: 0 }), Ok(4)); assert_eq!(text.offset_for_point(Point { row: 1, column: 1 }), Ok(5)); assert_eq!(text.offset_for_point(Point { row: 1, column: 5 }), Ok(9)); assert_eq!( text.offset_for_point(Point { row: 1, column: 6 }), Err(Error::OffsetOutOfRange) ); let text = Text::from("abc"); assert_eq!(text.offset_for_point(Point { row: 0, column: 0 }), Ok(0)); assert_eq!(text.offset_for_point(Point { row: 0, column: 1 }), Ok(1)); assert_eq!(text.offset_for_point(Point { row: 0, column: 2 }), Ok(2)); assert_eq!(text.offset_for_point(Point { row: 0, column: 3 }), Ok(3)); assert_eq!( text.offset_for_point(Point { row: 0, column: 4 }), Err(Error::OffsetOutOfRange) ); } #[test] fn test_longest_row_in_range() { for seed in 0..100 { println!("{:?}", seed); let mut rng = StdRng::from_seed(&[seed]); let string = RandomCharIter(rng) .take(rng.gen_range(1, 10)) .collect::(); let text = Text::from(string.as_ref()); for _i in 0..10 { let end = rng.gen_range(1, string.len() + 1); let start = rng.gen_range(0, end); let mut cur_row = string[0..start].chars().filter(|c| *c == '\n').count() as u32; let mut cur_row_len = 0; let mut expected_longest_row = cur_row; let mut expected_longest_row_len = cur_row_len; for ch in string[start..end].chars() { if ch == '\n' { if cur_row_len > expected_longest_row_len { expected_longest_row = cur_row; expected_longest_row_len = cur_row_len; } cur_row += 1; cur_row_len = 0; } else { cur_row_len += 1; } } if cur_row_len > expected_longest_row_len { expected_longest_row = cur_row; expected_longest_row_len = cur_row_len; } assert_eq!( text.longest_row_in_range(start..end), Ok((expected_longest_row, expected_longest_row_len)) ); } } } #[test] fn test_fragment_ids() { for seed in 0..10 { use rand::{Rng, SeedableRng, StdRng}; let mut rng = StdRng::from_seed(&[seed]); let mut ids = vec![FragmentId(Arc::new(vec![0])), FragmentId(Arc::new(vec![4]))]; for _i in 0..100 { let index = rng.gen_range::(1, ids.len()); let left = ids[index - 1].clone(); let right = ids[index].clone(); ids.insert(index, FragmentId::between_with_max(&left, &right, 4)); let mut sorted_ids = ids.clone(); sorted_ids.sort(); assert_eq!(ids, sorted_ids); } } } #[test] fn test_anchors() { let mut buffer = Buffer::new(""); let replica_id = Uuid::from_u128(1); let mut local_clock = time::Local::new(replica_id); let mut lamport_clock = time::Lamport::new(replica_id); buffer.edit(vec![0..0], "abc", &mut local_clock, &mut lamport_clock); let left_anchor = buffer.anchor_before_offset(2).unwrap(); let right_anchor = buffer.anchor_after_offset(2).unwrap(); buffer.edit(vec![1..1], "def\n", &mut local_clock, &mut lamport_clock); assert_eq!(buffer.to_string(), "adef\nbc"); assert_eq!(buffer.offset_for_anchor(&left_anchor).unwrap(), 6); assert_eq!(buffer.offset_for_anchor(&right_anchor).unwrap(), 6); assert_eq!( buffer.point_for_anchor(&left_anchor).unwrap(), Point { row: 1, column: 1 } ); assert_eq!( buffer.point_for_anchor(&right_anchor).unwrap(), Point { row: 1, column: 1 } ); buffer.edit(vec![2..3], "", &mut local_clock, &mut lamport_clock); assert_eq!(buffer.to_string(), "adf\nbc"); assert_eq!(buffer.offset_for_anchor(&left_anchor).unwrap(), 5); assert_eq!(buffer.offset_for_anchor(&right_anchor).unwrap(), 5); assert_eq!( buffer.point_for_anchor(&left_anchor).unwrap(), Point { row: 1, column: 1 } ); assert_eq!( buffer.point_for_anchor(&right_anchor).unwrap(), Point { row: 1, column: 1 } ); buffer.edit(vec![5..5], "ghi\n", &mut local_clock, &mut lamport_clock); assert_eq!(buffer.to_string(), "adf\nbghi\nc"); assert_eq!(buffer.offset_for_anchor(&left_anchor).unwrap(), 5); assert_eq!(buffer.offset_for_anchor(&right_anchor).unwrap(), 9); assert_eq!( buffer.point_for_anchor(&left_anchor).unwrap(), Point { row: 1, column: 1 } ); assert_eq!( buffer.point_for_anchor(&right_anchor).unwrap(), Point { row: 2, column: 0 } ); buffer.edit(vec![7..9], "", &mut local_clock, &mut lamport_clock); assert_eq!(buffer.to_string(), "adf\nbghc"); assert_eq!(buffer.offset_for_anchor(&left_anchor).unwrap(), 5); assert_eq!(buffer.offset_for_anchor(&right_anchor).unwrap(), 7); assert_eq!( buffer.point_for_anchor(&left_anchor).unwrap(), Point { row: 1, column: 1 } ); assert_eq!( buffer.point_for_anchor(&right_anchor).unwrap(), Point { row: 1, column: 3 } ); // Ensure anchoring to a point is equivalent to anchoring to an offset. assert_eq!( buffer.anchor_before_point(Point { row: 0, column: 0 }), buffer.anchor_before_offset(0) ); assert_eq!( buffer.anchor_before_point(Point { row: 0, column: 1 }), buffer.anchor_before_offset(1) ); assert_eq!( buffer.anchor_before_point(Point { row: 0, column: 2 }), buffer.anchor_before_offset(2) ); assert_eq!( buffer.anchor_before_point(Point { row: 0, column: 3 }), buffer.anchor_before_offset(3) ); assert_eq!( buffer.anchor_before_point(Point { row: 1, column: 0 }), buffer.anchor_before_offset(4) ); assert_eq!( buffer.anchor_before_point(Point { row: 1, column: 1 }), buffer.anchor_before_offset(5) ); assert_eq!( buffer.anchor_before_point(Point { row: 1, column: 2 }), buffer.anchor_before_offset(6) ); assert_eq!( buffer.anchor_before_point(Point { row: 1, column: 3 }), buffer.anchor_before_offset(7) ); assert_eq!( buffer.anchor_before_point(Point { row: 1, column: 4 }), buffer.anchor_before_offset(8) ); // Comparison between anchors. let anchor_at_offset_0 = buffer.anchor_before_offset(0).unwrap(); let anchor_at_offset_1 = buffer.anchor_before_offset(1).unwrap(); let anchor_at_offset_2 = buffer.anchor_before_offset(2).unwrap(); assert_eq!( buffer.cmp_anchors(&anchor_at_offset_0, &anchor_at_offset_0), Ok(Ordering::Equal) ); assert_eq!( buffer.cmp_anchors(&anchor_at_offset_1, &anchor_at_offset_1), Ok(Ordering::Equal) ); assert_eq!( buffer.cmp_anchors(&anchor_at_offset_2, &anchor_at_offset_2), Ok(Ordering::Equal) ); assert_eq!( buffer.cmp_anchors(&anchor_at_offset_0, &anchor_at_offset_1), Ok(Ordering::Less) ); assert_eq!( buffer.cmp_anchors(&anchor_at_offset_1, &anchor_at_offset_2), Ok(Ordering::Less) ); assert_eq!( buffer.cmp_anchors(&anchor_at_offset_0, &anchor_at_offset_2), Ok(Ordering::Less) ); assert_eq!( buffer.cmp_anchors(&anchor_at_offset_1, &anchor_at_offset_0), Ok(Ordering::Greater) ); assert_eq!( buffer.cmp_anchors(&anchor_at_offset_2, &anchor_at_offset_1), Ok(Ordering::Greater) ); assert_eq!( buffer.cmp_anchors(&anchor_at_offset_2, &anchor_at_offset_0), Ok(Ordering::Greater) ); } #[test] fn test_anchors_at_start_and_end() { let mut buffer = Buffer::new(""); let replica_id = Uuid::from_u128(1); let mut local_clock = time::Local::new(replica_id); let mut lamport_clock = time::Lamport::new(replica_id); let before_start_anchor = buffer.anchor_before_offset(0).unwrap(); let after_end_anchor = buffer.anchor_after_offset(0).unwrap(); buffer.edit(vec![0..0], "abc", &mut local_clock, &mut lamport_clock); assert_eq!(buffer.to_string(), "abc"); assert_eq!(buffer.offset_for_anchor(&before_start_anchor).unwrap(), 0); assert_eq!(buffer.offset_for_anchor(&after_end_anchor).unwrap(), 3); let after_start_anchor = buffer.anchor_after_offset(0).unwrap(); let before_end_anchor = buffer.anchor_before_offset(3).unwrap(); buffer.edit(vec![3..3], "def", &mut local_clock, &mut lamport_clock); buffer.edit(vec![0..0], "ghi", &mut local_clock, &mut lamport_clock); assert_eq!(buffer.to_string(), "ghiabcdef"); assert_eq!(buffer.offset_for_anchor(&before_start_anchor).unwrap(), 0); assert_eq!(buffer.offset_for_anchor(&after_start_anchor).unwrap(), 3); assert_eq!(buffer.offset_for_anchor(&before_end_anchor).unwrap(), 6); assert_eq!(buffer.offset_for_anchor(&after_end_anchor).unwrap(), 9); } #[test] fn test_is_modified() { let mut buffer = Buffer::new("abc"); let replica_id = Uuid::from_u128(1); let mut local_clock = time::Local::new(replica_id); let mut lamport_clock = time::Lamport::new(replica_id); assert!(!buffer.is_modified()); buffer.edit(vec![1..2], "", &mut local_clock, &mut lamport_clock); assert!(buffer.is_modified()); } #[test] fn test_random_concurrent_edits() { use crate::tests::Network; const PEERS: usize = 3; for seed in 0..50 { println!("{:?}", seed); let mut rng = StdRng::from_seed(&[seed]); let base_text = RandomCharIter(rng) .take(rng.gen_range(0, 10)) .collect::(); let mut replica_ids = Vec::new(); let mut buffers = Vec::new(); let mut local_clocks = Vec::new(); let mut lamport_clocks = Vec::new(); let mut network = Network::new(); for i in 0..PEERS { let buffer = Buffer::new(base_text.as_str()); buffers.push(buffer); let replica_id = Uuid::from_u128((i + 1) as u128); replica_ids.push(replica_id); local_clocks.push(time::Local::new(replica_id)); lamport_clocks.push(time::Lamport::new(replica_id)); network.add_peer(replica_id); } let mut mutation_count = 10; loop { let replica_index = rng.gen_range(0, PEERS); let replica_id = replica_ids[replica_index]; let buffer = &mut buffers[replica_index]; let local_clock = &mut local_clocks[replica_index]; let lamport_clock = &mut lamport_clocks[replica_index]; if mutation_count > 0 && rng.gen() { let (_, _, ops) = buffer.randomly_mutate(&mut rng, local_clock, lamport_clock); network.broadcast(replica_id, ops, &mut rng); mutation_count -= 1; } else if network.has_unreceived(replica_id) { buffer .apply_ops( network.receive(replica_id, &mut rng), local_clock, lamport_clock, ) .unwrap(); } if mutation_count == 0 && network.is_idle() { break; } } for buffer in &buffers[1..] { assert_eq!(buffer.to_string(), buffers[0].to_string()); assert_eq!( buffer.all_selections().collect::>(), buffers[0].all_selections().collect::>() ); assert_eq!( buffer.all_selection_ranges().collect::>(), buffers[0].all_selection_ranges().collect::>() ); } } } struct RandomCharIter(T); impl Iterator for RandomCharIter { type Item = char; fn next(&mut self) -> Option { if self.0.gen_weighted_bool(5) { Some('\n') } else { Some(self.0.gen_range(b'a', b'z' + 1).into()) } } } impl Buffer { pub fn randomly_mutate( &mut self, rng: &mut T, local_clock: &mut time::Local, lamport_clock: &mut time::Lamport, ) -> (Vec>, String, Vec) where T: Rng, { // Randomly mutate text. let mut old_ranges: Vec> = Vec::new(); for _ in 0..5 { let last_end = old_ranges.last().map_or(0, |last_range| last_range.end + 1); if last_end > self.len() { break; } let end = rng.gen_range::(last_end, self.len() + 1); let start = rng.gen_range::(last_end, end + 1); old_ranges.push(start..end); } let new_text_len = rng.gen_range(0, 10); let new_text: String = RandomCharIter(&mut *rng).take(new_text_len).collect(); if rng.gen_weighted_bool(5) { local_clock.tick(); } let mut operations = self.edit( old_ranges.iter().cloned(), new_text.as_str(), local_clock, lamport_clock, ); // Randomly add, remove or mutate selection sets. let replica_selection_sets = &self .all_selections() .map(|(set_id, _)| *set_id) .filter(|set_id| local_clock.replica_id == set_id.replica_id) .collect::>(); let set_id = rng.choose(&replica_selection_sets); if set_id.is_some() && rng.gen_weighted_bool(6) { let op = self .remove_selection_set(*set_id.unwrap(), lamport_clock) .unwrap(); operations.push(op); } else { let mut ranges = Vec::new(); for _ in 0..5 { let start = rng.gen_range(0, self.len() + 1); let start_point = self.point_for_offset(start).unwrap(); let end = rng.gen_range(0, self.len() + 1); let end_point = self.point_for_offset(end).unwrap(); ranges.push(start_point..end_point); } let op = if set_id.is_none() || rng.gen_weighted_bool(5) { self.add_selection_set(ranges, lamport_clock).unwrap().1 } else { self.replace_selection_set(*set_id.unwrap(), ranges, lamport_clock) .unwrap() }; operations.push(op); } (old_ranges, new_text, operations) } fn point_for_offset(&self, offset: usize) -> Result { let mut fragments_cursor = self.fragments.cursor(); fragments_cursor.seek(&offset, SeekBias::Left); fragments_cursor .item() .ok_or(Error::OffsetOutOfRange) .map(|fragment| { let overshoot = fragment .point_for_offset(offset - &fragments_cursor.start::()) .unwrap(); fragments_cursor.start::() + &overshoot }) } } } ================================================ FILE: memo_core/src/epoch.rs ================================================ use crate::btree::{self, SeekBias}; use crate::buffer::{self, Buffer, Point, Selection, SelectionSetId, Text}; use crate::operation_queue::{self, OperationQueue}; use crate::serialization; use crate::time; use crate::Error; use crate::Oid; use crate::ReplicaId; use flatbuffers::{FlatBufferBuilder, UnionWIPOffset, WIPOffset}; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use serde_derive::{Deserialize, Serialize}; use smallvec::SmallVec; use std::cmp::Ordering; use std::collections::{HashMap, HashSet}; use std::ffi::{OsStr, OsString}; use std::ops::{Add, AddAssign, Range}; use std::path::{Component, Path, PathBuf}; use std::sync::Arc; pub const ROOT_FILE_ID: FileId = FileId::Base(0); pub type Id = time::Lamport; #[derive(Clone)] pub struct Epoch { pub id: Id, pub head: Option, base_entries_next_id: u64, base_entries_stack: Vec, metadata: btree::Tree, parent_refs: btree::Tree, child_refs: btree::Tree, replica_locations: HashMap, version: time::Global, local_clock: time::Local, text_files: HashMap, deferred_ops: OperationQueue, } pub struct Cursor<'a> { epoch: &'a Epoch, metadata_cursor: btree::Cursor, parent_ref_cursor: btree::Cursor, child_ref_cursor: btree::Cursor, stack: Vec, path: PathBuf, } struct CursorStackEntry { cursor: btree::Cursor, visible: bool, } #[derive(Debug, Eq, PartialEq)] pub struct CursorEntry { pub file_id: FileId, pub file_type: FileType, pub depth: usize, pub name: Arc, pub status: FileStatus, pub visible: bool, } #[derive(Clone, Debug, Eq, Deserialize, PartialEq, Serialize)] pub struct DirEntry { pub depth: usize, #[serde( serialize_with = "serialize_os_string", deserialize_with = "deserialize_os_string" )] pub name: OsString, #[serde(rename = "type")] pub file_type: FileType, } #[derive(Clone, Debug, Eq, PartialEq)] pub enum Operation { InsertMetadata { file_id: FileId, file_type: FileType, parent: Option<(FileId, Arc)>, local_timestamp: time::Local, lamport_timestamp: time::Lamport, }, UpdateParent { child_id: FileId, new_parent: Option<(FileId, Arc)>, local_timestamp: time::Local, lamport_timestamp: time::Lamport, }, BufferOperation { file_id: FileId, operations: Vec, local_timestamp: time::Local, lamport_timestamp: time::Lamport, }, UpdateActiveLocation { file_id: Option, lamport_timestamp: time::Lamport, }, } #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] pub enum FileId { Base(u64), New(time::Local), } #[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)] pub enum FileStatus { New, Renamed, Removed, Modified, RenamedAndModified, Unchanged, } #[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)] pub enum FileType { Directory, Text, } #[derive(Clone, Debug, Eq, PartialEq)] struct Metadata { file_id: FileId, file_type: FileType, } #[derive(Clone, Debug, Eq, PartialEq)] struct ParentRefValue { child_id: FileId, timestamp: time::Lamport, parent: Option<(FileId, Arc)>, } #[derive(Clone, Debug, Default, Eq, PartialEq)] struct ParentRefValueKey { child_id: FileId, timestamp: time::Lamport, } #[derive(Clone, Debug, Eq, PartialEq)] struct ChildRefValue { parent_id: FileId, name: Arc, timestamp: time::Lamport, child_id: FileId, visible: bool, } #[derive(Clone, Debug, Default, Eq, PartialEq)] pub struct ChildRefValueSummary { parent_id: FileId, name: Arc, visible: bool, timestamp: time::Lamport, visible_count: usize, } #[derive(Clone, Debug, Default, Eq, PartialEq)] struct ChildRefValueKey { parent_id: FileId, name: Arc, visible: bool, timestamp: time::Lamport, } #[derive(Clone, Debug, Default, Ord, Eq, PartialEq, PartialOrd)] pub struct ChildRefKey { parent_id: FileId, name: Arc, } #[derive(Clone)] struct ReplicaLocation { file_id: Option, lamport_timestamp: time::Lamport, } #[derive(Clone)] enum TextFile { Deferred(Vec), Buffered(Buffer), } impl Epoch { pub fn new(replica_id: ReplicaId, id: Id, head: Option) -> Self { Self { id, head, base_entries_next_id: 1, base_entries_stack: Vec::new(), metadata: btree::Tree::new(), parent_refs: btree::Tree::new(), child_refs: btree::Tree::new(), replica_locations: HashMap::new(), version: time::Global::new(), local_clock: time::Local::new(replica_id), text_files: HashMap::new(), deferred_ops: OperationQueue::new(), } } pub fn buffer_version(&self, file_id: FileId) -> Result { if let Some(TextFile::Buffered(buffer)) = self.text_files.get(&file_id) { Ok(buffer.version.clone()) } else { Err(Error::InvalidFileId("file has not been opened".into())) } } pub fn buffer_selections_last_update( &self, file_id: FileId, ) -> Result { if let Some(TextFile::Buffered(buffer)) = self.text_files.get(&file_id) { Ok(buffer.selections_last_update) } else { Err(Error::InvalidFileId("file has not been opened".into())) } } pub fn version(&self) -> time::Global { self.version.clone() } pub fn cursor(&self) -> Option { let metadata_cursor = self.metadata.cursor(); let parent_ref_cursor = self.parent_refs.cursor(); let child_ref_cursor = self.child_refs.cursor(); let mut cursor = Cursor { epoch: &self, metadata_cursor, parent_ref_cursor, child_ref_cursor, stack: Vec::new(), path: PathBuf::new(), }; if cursor.descend_into(true, ROOT_FILE_ID) { Some(cursor) } else { None } } pub fn append_base_entries( &mut self, entries: I, lamport_clock: &mut time::Lamport, ) -> Result, Error> where I: IntoIterator, { let mut metadata_edits = Vec::new(); let mut parent_ref_edits = Vec::new(); let mut child_ref_edits = Vec::new(); let mut child_ref_cursor = self.child_refs.cursor(); let mut name_conflicts = HashSet::new(); for entry in entries { let stack_depth = self.base_entries_stack.len(); if entry.depth == 0 || entry.depth > stack_depth + 1 { return Err(Error::InvalidDirEntry); } self.base_entries_stack.truncate(entry.depth - 1); let parent_id = self .base_entries_stack .last() .cloned() .unwrap_or(ROOT_FILE_ID); let name = Arc::new(entry.name); let file_id = FileId::Base(self.base_entries_next_id); metadata_edits.push(btree::Edit::Insert(Metadata { file_id, file_type: entry.file_type, })); parent_ref_edits.push(btree::Edit::Insert(ParentRefValue { child_id: file_id, timestamp: time::Lamport::default(), parent: Some((parent_id, name.clone())), })); child_ref_edits.push(btree::Edit::Insert(ChildRefValue { parent_id, name: name.clone(), timestamp: time::Lamport::default(), child_id: file_id, visible: true, })); // In the rare case we already have a child ref with this name, remember to fix the // name conflict later. if child_ref_cursor.seek(&ChildRefKey { parent_id, name }, SeekBias::Left) { name_conflicts.insert(file_id); } self.base_entries_next_id += 1; if entry.file_type == FileType::Directory { self.base_entries_stack.push(file_id); } } self.metadata.edit(&mut metadata_edits); self.parent_refs.edit(&mut parent_ref_edits); self.child_refs.edit(&mut child_ref_edits); let mut fixup_ops = Vec::new(); for file_id in name_conflicts { fixup_ops.extend(self.fix_name_conflicts(file_id, lamport_clock)); } let deferred_ops = self.deferred_ops.drain(); fixup_ops.extend(self.apply_ops_internal(deferred_ops, lamport_clock)?); Ok(fixup_ops) } pub fn apply_ops( &mut self, ops: I, lamport_clock: &mut time::Lamport, ) -> Result, Error> where I: IntoIterator, { let mut fixup_ops = Vec::new(); fixup_ops.extend(self.apply_ops_internal(ops, lamport_clock)?); let deferred_ops = self.deferred_ops.drain(); fixup_ops.extend(self.apply_ops_internal(deferred_ops, lamport_clock)?); Ok(fixup_ops) } fn apply_ops_internal( &mut self, ops: I, lamport_clock: &mut time::Lamport, ) -> Result, Error> where I: IntoIterator, { let mut ops = ops.into_iter().peekable(); if ops.peek().is_none() { return Ok(Vec::new()); } let mut new_epoch = self.clone(); let mut deferred_ops = Vec::new(); let mut potential_conflicts = HashSet::new(); for op in ops { if new_epoch.can_apply_op(&op) { match &op { Operation::InsertMetadata { file_id, parent, .. } => { if parent.is_some() { potential_conflicts.insert(*file_id); } } Operation::UpdateParent { child_id, .. } => { potential_conflicts.insert(*child_id); } _ => {} } new_epoch.apply_op(op, lamport_clock)?; } else { deferred_ops.push(op); } } new_epoch.deferred_ops.insert(deferred_ops); let mut fixup_ops = Vec::new(); for file_id in &potential_conflicts { fixup_ops.extend(new_epoch.fix_conflicts(*file_id, lamport_clock)); } *self = new_epoch; Ok(fixup_ops) } pub fn apply_op( &mut self, op: Operation, lamport_clock: &mut time::Lamport, ) -> Result<(), Error> { if let Some(local_timestamp) = op.local_timestamp() { self.version.observe(local_timestamp); self.local_clock.observe(local_timestamp); } lamport_clock.observe(op.lamport_timestamp()); match op { Operation::InsertMetadata { file_id, file_type, parent, lamport_timestamp, .. } => { if !self.metadata.cursor().seek(&file_id, SeekBias::Left) { self.metadata.insert(Metadata { file_id, file_type }); if let Some((parent_id, name)) = parent { self.parent_refs.insert(ParentRefValue { child_id: file_id, parent: Some((parent_id, name.clone())), timestamp: lamport_timestamp, }); self.child_refs.insert(ChildRefValue { parent_id, name, timestamp: lamport_timestamp, child_id: file_id, visible: true, }); } } } Operation::UpdateParent { child_id, new_parent, lamport_timestamp, .. } => { let mut child_ref_edits: SmallVec<[_; 3]> = SmallVec::new(); let mut parent_ref_cursor = self.parent_refs.cursor(); if parent_ref_cursor.seek(&child_id, SeekBias::Left) { let latest_parent_ref = parent_ref_cursor.item().unwrap(); let mut latest_visible_parent_ref = None; while let Some(parent_ref) = parent_ref_cursor.item() { if parent_ref.child_id != child_id { break; } else if parent_ref.parent.is_some() { latest_visible_parent_ref = Some(parent_ref); break; } else { parent_ref_cursor.next(); } } let mut child_ref = None; if let Some(ref latest_visible_parent_ref) = latest_visible_parent_ref { let mut child_ref_cursor = self.child_refs.cursor(); let (parent_id, name) = latest_visible_parent_ref.parent.clone().unwrap(); child_ref_cursor.seek( &ChildRefValueKey { parent_id, name, visible: latest_parent_ref.parent.is_some(), timestamp: latest_visible_parent_ref.timestamp, }, SeekBias::Left, ); child_ref = child_ref_cursor.item(); } if lamport_timestamp > latest_parent_ref.timestamp { if let Some(ref child_ref) = child_ref { child_ref_edits.push(btree::Edit::Remove(child_ref.clone())); } if let Some((parent_id, name)) = new_parent.clone() { child_ref_edits.push(btree::Edit::Insert(ChildRefValue { parent_id, name, timestamp: lamport_timestamp, child_id, visible: true, })); } else if let Some(mut child_ref) = child_ref { child_ref.visible = false; child_ref_edits.push(btree::Edit::Insert(child_ref)); } } else if latest_visible_parent_ref .map_or(true, |r| lamport_timestamp > r.timestamp) && latest_parent_ref.parent.is_none() && new_parent.is_some() { let (parent_id, name) = new_parent.clone().unwrap(); if let Some(child_ref) = child_ref { child_ref_edits.push(btree::Edit::Remove(child_ref.clone())); } child_ref_edits.push(btree::Edit::Insert(ChildRefValue { parent_id, name, timestamp: lamport_timestamp, child_id, visible: false, })); } } else if let Some((parent_id, name)) = new_parent.clone() { child_ref_edits.push(btree::Edit::Insert(ChildRefValue { parent_id, name, timestamp: lamport_timestamp, child_id, visible: true, })); } self.parent_refs .edit(&mut [btree::Edit::Insert(ParentRefValue { child_id, timestamp: lamport_timestamp, parent: new_parent, })]); self.child_refs.edit(&mut child_ref_edits); } Operation::BufferOperation { file_id, operations, .. } => match self .text_files .entry(file_id) .or_insert_with(|| TextFile::Deferred(Vec::new())) { TextFile::Deferred(deferred_operations) => { deferred_operations.extend(operations); } TextFile::Buffered(buffer) => { buffer .apply_ops(operations, &mut self.local_clock, lamport_clock) .map_err(|_| Error::InvalidOperation)?; } }, Operation::UpdateActiveLocation { file_id, lamport_timestamp, .. } => { self.replica_locations .entry(lamport_timestamp.replica_id) .and_modify(|location| { if lamport_timestamp > location.lamport_timestamp { location.file_id = file_id; location.lamport_timestamp = lamport_timestamp; } }) .or_insert(ReplicaLocation { file_id, lamport_timestamp, }); } } Ok(()) } fn can_apply_op(&self, op: &Operation) -> bool { match op { Operation::InsertMetadata { .. } => true, Operation::UpdateParent { child_id, .. } => self.metadata(*child_id).is_ok(), Operation::BufferOperation { file_id, .. } => self.metadata(*file_id).is_ok(), Operation::UpdateActiveLocation { file_id, .. } => { file_id.map_or(true, |file_id| self.metadata(file_id).is_ok()) } } } pub fn create_file( &mut self, parent_id: FileId, name: N, file_type: FileType, lamport_clock: &mut time::Lamport, ) -> Result where N: AsRef, { self.check_file_id(parent_id, Some(FileType::Directory))?; let mut new_lamport_clock = *lamport_clock; let mut new_epoch = self.clone(); let file_id = FileId::New(new_epoch.local_clock.tick()); let operation = Operation::InsertMetadata { file_id, file_type, parent: Some((parent_id, Arc::new(name.as_ref().into()))), local_timestamp: new_epoch.local_clock.tick(), lamport_timestamp: new_lamport_clock.tick(), }; let fixup_ops = new_epoch .apply_ops_internal(Some(operation.clone()), &mut new_lamport_clock) .unwrap(); if fixup_ops.is_empty() { *lamport_clock = new_lamport_clock; *self = new_epoch; Ok(operation) } else { Err(Error::InvalidOperation) } } pub fn new_text_file(&mut self, lamport_clock: &mut time::Lamport) -> (FileId, Operation) { let file_id = FileId::New(self.local_clock.tick()); let operation = Operation::InsertMetadata { file_id, file_type: FileType::Text, parent: None, local_timestamp: self.local_clock.tick(), lamport_timestamp: lamport_clock.tick(), }; self.apply_op(operation.clone(), lamport_clock).unwrap(); (file_id, operation) } pub fn open_text_file( &mut self, file_id: FileId, base_text: T, lamport_clock: &mut time::Lamport, ) -> Result<(), Error> where T: Into, { self.check_file_id(file_id, Some(FileType::Text))?; match self.text_files.remove(&file_id) { Some(TextFile::Deferred(operations)) => { let mut buffer = Buffer::new(base_text); buffer .apply_ops(operations, &mut self.local_clock, lamport_clock) .map_err(|_| Error::InvalidOperation)?; self.text_files.insert(file_id, TextFile::Buffered(buffer)); } Some(text_file) => { self.text_files.insert(file_id, text_file); } None => { self.text_files .insert(file_id, TextFile::Buffered(Buffer::new(base_text))); } } Ok(()) } pub fn rename( &mut self, file_id: FileId, new_parent_id: FileId, new_name: N, lamport_clock: &mut time::Lamport, ) -> Result where N: AsRef, { self.check_file_id(file_id, None)?; self.check_file_id(new_parent_id, Some(FileType::Directory))?; let mut new_lamport_clock = *lamport_clock; let mut new_epoch = self.clone(); let operation = Operation::UpdateParent { child_id: file_id, new_parent: Some((new_parent_id, Arc::new(new_name.as_ref().into()))), local_timestamp: new_epoch.local_clock.tick(), lamport_timestamp: new_lamport_clock.tick(), }; let fixup_ops = new_epoch .apply_ops_internal(Some(operation.clone()), &mut new_lamport_clock) .unwrap(); if fixup_ops.is_empty() { *lamport_clock = new_lamport_clock; *self = new_epoch; Ok(operation) } else { Err(Error::InvalidOperation) } } pub fn remove( &mut self, file_id: FileId, lamport_clock: &mut time::Lamport, ) -> Result { self.check_file_id(file_id, None)?; let operation = Operation::UpdateParent { child_id: file_id, new_parent: None, local_timestamp: self.local_clock.tick(), lamport_timestamp: lamport_clock.tick(), }; self.apply_op(operation.clone(), lamport_clock).unwrap(); Ok(operation) } pub fn set_active_location( &mut self, file_id: Option, lamport_clock: &mut time::Lamport, ) -> Result { if let Some(file_id) = file_id { self.check_file_id(file_id, Some(FileType::Text))?; } let lamport_timestamp = lamport_clock.tick(); self.replica_locations.insert( lamport_timestamp.replica_id, ReplicaLocation { lamport_timestamp, file_id, }, ); Ok(Operation::UpdateActiveLocation { file_id, lamport_timestamp, }) } pub fn replica_location(&self, replica_id: ReplicaId) -> Option { self.replica_locations .get(&replica_id) .and_then(|location| location.file_id) } pub fn replica_locations<'a>(&'a self) -> impl Iterator + 'a { self.replica_locations .iter() .filter_map(|(replica_id, location)| { location.file_id.map(|file_id| (*replica_id, file_id)) }) } pub fn edit( &mut self, file_id: FileId, old_ranges: I, new_text: T, lamport_clock: &mut time::Lamport, ) -> Result where I: IntoIterator>, T: Into, { self.mutate_buffer( file_id, lamport_clock, |buffer, local_clock, lamport_clock| { Ok(buffer.edit(old_ranges, new_text, local_clock, lamport_clock)) }, ) } pub fn edit_2d( &mut self, file_id: FileId, old_ranges: I, new_text: T, lamport_clock: &mut time::Lamport, ) -> Result where I: IntoIterator>, T: Into, { self.mutate_buffer( file_id, lamport_clock, |buffer, local_clock, lamport_clock| { Ok(buffer.edit_2d(old_ranges, new_text, local_clock, lamport_clock)) }, ) } pub fn add_selection_set( &mut self, file_id: FileId, ranges: I, lamport_clock: &mut time::Lamport, ) -> Result<(SelectionSetId, Operation), Error> where I: IntoIterator>, { let mut new_set_id = None; let operation = self.mutate_buffer( file_id, lamport_clock, |buffer, _local_clock, lamport_clock| { let (set_id, operation) = buffer.add_selection_set(ranges, lamport_clock)?; new_set_id = Some(set_id); Ok(vec![operation]) }, )?; Ok((new_set_id.unwrap(), operation)) } pub fn replace_selection_set( &mut self, file_id: FileId, set_id: SelectionSetId, ranges: I, lamport_clock: &mut time::Lamport, ) -> Result where I: IntoIterator>, { self.mutate_buffer( file_id, lamport_clock, |buffer, _local_clock, lamport_clock| { let operation = buffer.replace_selection_set(set_id, ranges, lamport_clock)?; Ok(vec![operation]) }, ) } pub fn remove_selection_set( &mut self, file_id: FileId, set_id: SelectionSetId, lamport_clock: &mut time::Lamport, ) -> Result { self.mutate_buffer( file_id, lamport_clock, |buffer, _local_clock, lamport_clock| { let operation = buffer.remove_selection_set(set_id, lamport_clock)?; Ok(vec![operation]) }, ) } pub fn all_selections( &self, file_id: FileId, ) -> Result)>, Error> { if let Some(TextFile::Buffered(buffer)) = self.text_files.get(&file_id) { Ok(buffer .all_selections() .map(|(set_id, selections)| (*set_id, selections.clone())) .collect()) } else { Err(Error::InvalidFileId("file has not been opened".into())) } } pub fn selection_ranges<'a>( &'a self, file_id: FileId, set_id: SelectionSetId, ) -> Result> + 'a, Error> { if let Some(TextFile::Buffered(buffer)) = self.text_files.get(&file_id) { buffer.selection_ranges(set_id) } else { Err(Error::InvalidFileId("file has not been opened".into())) } } pub fn all_selection_ranges<'a>( &'a self, file_id: FileId, ) -> Result>)> + 'a, Error> { if let Some(TextFile::Buffered(buffer)) = self.text_files.get(&file_id) { Ok(buffer.all_selection_ranges()) } else { Err(Error::InvalidFileId("file has not been opened".into())) } } fn mutate_buffer( &mut self, file_id: FileId, lamport_clock: &mut time::Lamport, mutate: F, ) -> Result where F: FnOnce( &mut Buffer, &mut time::Local, &mut time::Lamport, ) -> Result, Error>, { if let Some(TextFile::Buffered(buffer)) = self.text_files.get_mut(&file_id) { let operations = mutate(buffer, &mut self.local_clock, lamport_clock)?; let local_timestamp = self.local_clock.tick(); self.version.observe(local_timestamp); Ok(Operation::BufferOperation { file_id, operations, local_timestamp, lamport_timestamp: lamport_clock.tick(), }) } else { Err(Error::InvalidFileId("file has not been opened".into())) } } pub fn file_id

(&self, path: P) -> Result where P: AsRef, { let path = path.as_ref(); let mut cursor = self.child_refs.cursor(); let mut parent_id = ROOT_FILE_ID; for component in path.components() { match component { Component::Normal(name) => { let name = Arc::new(name.into()); if cursor.seek(&ChildRefKey { parent_id, name }, SeekBias::Left) { let child_ref = cursor.item().unwrap(); if child_ref.visible { parent_id = child_ref.child_id; } else { return Err(Error::InvalidPath( format!("file not found for path {:?}", path).into(), )); } } else { return Err(Error::InvalidPath( format!("file not found for path {:?}", path).into(), )); } } _ => { return Err(Error::InvalidPath( format!("path {:?} contains unrecognized components", path).into(), )); } } } Ok(parent_id) } pub fn base_path(&self, mut file_id: FileId) -> Option { let mut cursor = self.parent_refs.cursor(); let mut path_components = Vec::new(); loop { if file_id == ROOT_FILE_ID { break; } else if file_id.is_base() { cursor.seek( &ParentRefValueKey { child_id: file_id, timestamp: time::Lamport::default(), }, SeekBias::Left, ); let (parent_id, name) = cursor.item().unwrap().parent.unwrap(); file_id = parent_id; path_components.push(name); } else { return None; } } let mut path = PathBuf::new(); for component in path_components.into_iter().rev() { path.push(component.as_ref()); } Some(path) } pub fn path(&self, file_id: FileId) -> Option { let mut path_components = Vec::new(); if self.visit_ancestors(file_id, |name| path_components.push(name)) { let mut path = PathBuf::new(); for component in path_components.into_iter().rev() { path.push(component.as_ref()); } Some(path) } else { None } } pub fn text(&self, file_id: FileId) -> Result { if let Some(TextFile::Buffered(buffer)) = self.text_files.get(&file_id) { Ok(buffer.iter()) } else { Err(Error::InvalidFileId("file has not been opened".into())) } } pub fn selections_changed_since( &self, file_id: FileId, last_selection_update: buffer::SelectionsVersion, ) -> Result { if let Some(TextFile::Buffered(buffer)) = self.text_files.get(&file_id) { Ok(buffer.selections_changed_since(last_selection_update)) } else { Err(Error::InvalidFileId("file has not been opened".into())) } } pub fn changes_since( &self, file_id: FileId, version: &time::Global, ) -> Result, Error> { if let Some(TextFile::Buffered(buffer)) = self.text_files.get(&file_id) { Ok(buffer.changes_since(version)) } else { Err(Error::InvalidFileId("file has not been opened".into())) } } pub fn buffer_deferred_ops_len(&self, file_id: FileId) -> Result { if let Some(TextFile::Buffered(buffer)) = self.text_files.get(&file_id) { Ok(buffer.deferred_ops_len()) } else { Err(Error::InvalidFileId("file has not been opened".into())) } } pub fn file_type(&self, file_id: FileId) -> Result { Ok(self.metadata(file_id)?.file_type) } fn metadata(&self, file_id: FileId) -> Result { if file_id == ROOT_FILE_ID { Ok(Metadata { file_id: ROOT_FILE_ID, file_type: FileType::Directory, }) } else { let mut cursor = self.metadata.cursor(); if cursor.seek(&file_id, SeekBias::Left) { Ok(cursor.item().unwrap()) } else { Err(Error::InvalidFileId("file does not exist".into())) } } } fn check_file_id(&self, file_id: FileId, expected_type: Option) -> Result<(), Error> { let metadata = self.metadata(file_id)?; if expected_type.map_or(true, |expected_type| expected_type == metadata.file_type) { Ok(()) } else { Err(Error::InvalidFileId( format!("expected file to have type {:?}", expected_type).into(), )) } } fn visit_ancestors(&self, file_id: FileId, mut f: F) -> bool where F: FnMut(Arc), { let mut visited = HashSet::new(); let mut cursor = self.parent_refs.cursor(); if file_id == ROOT_FILE_ID { true } else if cursor.seek(&file_id, SeekBias::Left) { loop { if let Some((parent_id, name)) = cursor.item().and_then(|r| r.parent) { // TODO: Only check for cycles in debug mode if visited.contains(&parent_id) { panic!("Cycle detected when visiting ancestors"); } else { visited.insert(parent_id); } f(name); if parent_id == ROOT_FILE_ID { break; } else if !cursor.seek(&parent_id, SeekBias::Left) { return false; } } else { return false; } } true } else { false } } fn fix_conflicts( &mut self, file_id: FileId, lamport_clock: &mut time::Lamport, ) -> Vec { use crate::btree::KeyedItem; let mut fixup_ops = Vec::new(); let mut reverted_moves: HashMap = HashMap::new(); // TODO: Only check for cycles if the child was moved and is a directory. let mut visited = HashSet::new(); let mut latest_move: Option = None; let mut cursor = self.parent_refs.cursor(); cursor.seek(&file_id, SeekBias::Left); loop { let mut parent_ref = cursor.item().unwrap(); if visited.contains(&parent_ref.child_id) { // Cycle detected. Revert the most recent move contributing to the cycle. cursor.seek(&latest_move.as_ref().unwrap().key(), SeekBias::Right); // Find the previous value for this parent ref that isn't a deletion and store // its timestamp in our reverted_moves map. loop { let parent_ref = cursor.item().unwrap(); if parent_ref.parent.is_some() { reverted_moves.insert(parent_ref.child_id, parent_ref.timestamp); break; } else { cursor.next(); } } // Reverting this move may not have been enough to break the cycle. We clear // the visited set but continue looping, potentially reverting multiple moves. latest_move = None; visited.clear(); } else { visited.insert(parent_ref.child_id); // If we have already reverted this parent ref to a previous value, interpret // it as having the value we reverted to. if let Some(prev_timestamp) = reverted_moves.get(&parent_ref.child_id) { while parent_ref.timestamp > *prev_timestamp { cursor.next(); parent_ref = cursor.item().unwrap(); } } // Check if this parent ref is a move and has the latest timestamp of any move // we have seen so far. If so, it is a candidate to be reverted. if latest_move .as_ref() .map_or(true, |m| parent_ref.timestamp > m.timestamp) { cursor.next(); if cursor.item().map_or(false, |next_parent_ref| { next_parent_ref.child_id == parent_ref.child_id }) { latest_move = Some(parent_ref.clone()); } } // Walk up to the next parent or break if none exists or the parent is the root if let Some((parent_id, _)) = parent_ref.parent { if parent_id == ROOT_FILE_ID { break; } else { if !cursor.seek(&parent_id, SeekBias::Left) { break; } } } else { break; } } } // Convert the reverted moves into new move operations. let mut moved_file_ids = Vec::new(); for (child_id, timestamp) in &reverted_moves { cursor.seek( &ParentRefValueKey { child_id: *child_id, timestamp: *timestamp, }, SeekBias::Left, ); fixup_ops.push(Operation::UpdateParent { child_id: *child_id, new_parent: cursor.item().unwrap().parent, local_timestamp: self.local_clock.tick(), lamport_timestamp: lamport_clock.tick(), }); moved_file_ids.push(*child_id); } for op in &fixup_ops { self.apply_op(op.clone(), lamport_clock).unwrap(); } for file_id in moved_file_ids { fixup_ops.extend(self.fix_name_conflicts(file_id, lamport_clock)); } if !reverted_moves.contains_key(&file_id) { fixup_ops.extend(self.fix_name_conflicts(file_id, lamport_clock)); } fixup_ops } fn fix_name_conflicts( &mut self, file_id: FileId, lamport_clock: &mut time::Lamport, ) -> Vec { let mut fixup_ops = Vec::new(); let mut parent_ref_cursor = self.parent_refs.cursor(); parent_ref_cursor.seek(&file_id, SeekBias::Left); if let Some((parent_id, name)) = parent_ref_cursor.item().unwrap().parent { let mut cursor_1 = self.child_refs.cursor(); cursor_1.seek( &ChildRefKey { parent_id, name: name.clone(), }, SeekBias::Left, ); cursor_1.next(); let mut cursor_2 = cursor_1.clone(); let mut unique_name = name.clone(); while let Some(child_ref) = cursor_1.item() { if child_ref.visible && child_ref.parent_id == parent_id && child_ref.name == name { loop { Arc::make_mut(&mut unique_name).push("~"); cursor_2.seek_forward( &ChildRefKey { parent_id, name: unique_name.clone(), }, SeekBias::Left, ); if let Some(conflicting_child_ref) = cursor_2.item() { if !conflicting_child_ref.visible || conflicting_child_ref.parent_id != parent_id || conflicting_child_ref.name != unique_name { break; } } else { break; } } let fixup_op = Operation::UpdateParent { child_id: file_id, new_parent: Some((parent_id, unique_name.clone())), local_timestamp: self.local_clock.tick(), lamport_timestamp: lamport_clock.tick(), }; self.apply_op(fixup_op.clone(), lamport_clock).unwrap(); fixup_ops.push(fixup_op); let visible_index = cursor_1.end::(); cursor_1.seek_forward(&visible_index, SeekBias::Right); } else { break; } } } fixup_ops } } impl<'a> Cursor<'a> { pub fn next(&mut self, can_descend: bool) -> bool { if !self.stack.is_empty() { let entry = self.entry().unwrap(); if !can_descend || entry.file_type != FileType::Directory || !self.descend_into(entry.visible, entry.file_id) { while !self.stack.is_empty() && !self.next_sibling() { self.stack.pop(); self.path.pop(); } } } !self.stack.is_empty() } pub fn entry(&self) -> Result { let CursorStackEntry { cursor: child_ref_cursor, visible: parent_visible, } = self.stack.last().ok_or(Error::CursorExhausted)?; let metadata = self.metadata_cursor.item().unwrap(); let child_ref = child_ref_cursor.item().unwrap(); let mut parent_ref_cursor = self.parent_ref_cursor.clone(); parent_ref_cursor.seek(&metadata.file_id, SeekBias::Left); let newest_parent_ref_value = parent_ref_cursor.item().unwrap(); parent_ref_cursor.seek(&metadata.file_id, SeekBias::Right); parent_ref_cursor.prev(); let oldest_parent_ref_value = parent_ref_cursor.item().unwrap(); let (status, visible) = match metadata.file_id { FileId::Base(_) => { if newest_parent_ref_value.parent == oldest_parent_ref_value.parent { if self.is_modified_file(metadata.file_id) { (FileStatus::Modified, true) } else { (FileStatus::Unchanged, true) } } else if newest_parent_ref_value.parent.is_some() { if self.is_modified_file(metadata.file_id) { (FileStatus::RenamedAndModified, true) } else { (FileStatus::Renamed, true) } } else { (FileStatus::Removed, false) } } FileId::New(_) => (FileStatus::New, newest_parent_ref_value.parent.is_some()), }; Ok(CursorEntry { file_id: metadata.file_id, file_type: metadata.file_type, name: child_ref.name, depth: self.stack.len(), status, visible: *parent_visible && visible, }) } pub fn path(&self) -> Result<&Path, Error> { if self.stack.is_empty() { Err(Error::CursorExhausted) } else { Ok(&self.path) } } pub fn base_path(&self) -> Result, Error> { let metadata = self.metadata_cursor.item().ok_or(Error::CursorExhausted)?; Ok(self.epoch.base_path(metadata.file_id)) } fn descend_into(&mut self, parent_visible: bool, dir_id: FileId) -> bool { let mut child_ref_cursor = self.child_ref_cursor.clone(); child_ref_cursor.seek(&dir_id, SeekBias::Left); if let Some(child_ref) = child_ref_cursor.item() { if child_ref.parent_id == dir_id { self.stack.push(CursorStackEntry { cursor: child_ref_cursor, visible: parent_visible, }); self.path.push(child_ref.name.as_ref()); self.metadata_cursor .seek(&child_ref.child_id, SeekBias::Left); true } else { false } } else { false } } pub fn next_sibling(&mut self) -> bool { let CursorStackEntry { cursor, .. } = self.stack.last_mut().unwrap(); let parent_id = cursor.item().unwrap().parent_id; cursor.next(); if let Some(child_ref) = cursor.item() { if child_ref.parent_id == parent_id { self.metadata_cursor .seek(&child_ref.child_id, SeekBias::Left); self.path.pop(); self.path.push(child_ref.name.as_ref()); return true; } } false } fn is_modified_file(&self, file_id: FileId) -> bool { self.epoch .text_files .get(&file_id) .map_or(false, |f| f.is_modified()) } } impl Operation { fn local_timestamp(&self) -> Option { match self { Operation::InsertMetadata { local_timestamp, .. } => Some(*local_timestamp), Operation::UpdateParent { local_timestamp, .. } => Some(*local_timestamp), Operation::BufferOperation { local_timestamp, .. } => Some(*local_timestamp), Operation::UpdateActiveLocation { .. } => None, } } pub fn lamport_timestamp(&self) -> time::Lamport { match self { Operation::InsertMetadata { lamport_timestamp, .. } => *lamport_timestamp, Operation::UpdateParent { lamport_timestamp, .. } => *lamport_timestamp, Operation::BufferOperation { lamport_timestamp, .. } => *lamport_timestamp, Operation::UpdateActiveLocation { lamport_timestamp, .. } => *lamport_timestamp, } } pub fn to_flatbuf<'fbb>( &self, builder: &mut FlatBufferBuilder<'fbb>, ) -> (serialization::epoch::Operation, WIPOffset) { use crate::serialization::epoch::{ BufferOperation, BufferOperationArgs, FileId as FileIdType, InsertMetadata, InsertMetadataArgs, Operation as OperationType, UpdateActiveLocation, UpdateActiveLocationArgs, UpdateParent, UpdateParentArgs, }; fn parent_to_flatbuf<'a, 'fbb>( parent: &'a Option<(FileId, Arc)>, builder: &mut FlatBufferBuilder<'fbb>, ) -> ( FileIdType, Option>, Option>, ) { if let Some((file_id, name)) = parent.as_ref() { let (file_id_type, file_id) = file_id.to_flatbuf(builder); ( file_id_type, Some(file_id), Some(builder.create_string(name.to_string_lossy().as_ref())), ) } else { (FileIdType::NONE, None, None) } } match self { Operation::InsertMetadata { file_id, file_type, parent, local_timestamp, lamport_timestamp, } => { let (file_id_type, file_id) = file_id.to_flatbuf(builder); let (parent_id_type, parent_id, name_in_parent) = parent_to_flatbuf(parent, builder); ( OperationType::InsertMetadata, InsertMetadata::create( builder, &InsertMetadataArgs { file_id_type, file_id: Some(file_id), file_type: file_type.to_flatbuf(), parent_id_type, parent_id, name_in_parent, local_timestamp: Some(&local_timestamp.to_flatbuf()), lamport_timestamp: Some(&lamport_timestamp.to_flatbuf()), }, ) .as_union_value(), ) } Operation::UpdateParent { child_id, new_parent, local_timestamp, lamport_timestamp, } => { let (child_id_type, child_id) = child_id.to_flatbuf(builder); let (new_parent_id_type, new_parent_id, new_name_in_parent) = parent_to_flatbuf(new_parent, builder); ( OperationType::UpdateParent, UpdateParent::create( builder, &UpdateParentArgs { child_id_type, child_id: Some(child_id), new_parent_id_type, new_parent_id, new_name_in_parent, local_timestamp: Some(&local_timestamp.to_flatbuf()), lamport_timestamp: Some(&lamport_timestamp.to_flatbuf()), }, ) .as_union_value(), ) } Operation::BufferOperation { file_id, operations, local_timestamp, lamport_timestamp, } => { let (file_id_type, file_id) = file_id.to_flatbuf(builder); let op_flatbufs = &operations .iter() .map(|e| e.to_flatbuf(builder)) .collect::>(); let operations = builder.create_vector(op_flatbufs); ( OperationType::BufferOperation, BufferOperation::create( builder, &BufferOperationArgs { file_id_type, file_id: Some(file_id), operations: Some(operations), local_timestamp: Some(&local_timestamp.to_flatbuf()), lamport_timestamp: Some(&lamport_timestamp.to_flatbuf()), }, ) .as_union_value(), ) } Operation::UpdateActiveLocation { file_id, lamport_timestamp, } => { let file_id_type; let file_id_buf; if let Some(file_id) = file_id { let file_id_flatbuf = file_id.to_flatbuf(builder); file_id_type = file_id_flatbuf.0; file_id_buf = Some(file_id_flatbuf.1); } else { file_id_type = FileIdType::NONE; file_id_buf = None; } ( OperationType::UpdateActiveLocation, UpdateActiveLocation::create( builder, &UpdateActiveLocationArgs { file_id_type, file_id: file_id_buf, lamport_timestamp: Some(&lamport_timestamp.to_flatbuf()), }, ) .as_union_value(), ) } } } pub fn from_flatbuf<'a>( operation_type: serialization::epoch::Operation, message: flatbuffers::Table<'a>, ) -> Result, Error> { fn parent_from_flatbuf<'a>( parent_id_type: serialization::epoch::FileId, parent_id_message: Option>, name: Option<&'a str>, ) -> Option<(FileId, Arc)> { parent_id_message.map(|parent_id_message| { let file_id = FileId::from_flatbuf(parent_id_type, parent_id_message); let name = Arc::new(OsString::from(name.unwrap())); (file_id, name) }) } match operation_type { serialization::epoch::Operation::InsertMetadata => { let message = serialization::epoch::InsertMetadata::init_from_table(message); Ok(Some(Operation::InsertMetadata { file_id: FileId::from_flatbuf( message.file_id_type(), message.file_id().ok_or(Error::DeserializeError)?, ), file_type: FileType::from_flatbuf(&message.file_type()), parent: parent_from_flatbuf( message.parent_id_type(), message.parent_id(), message.name_in_parent(), ), local_timestamp: time::Local::from_flatbuf(&message.local_timestamp().unwrap()), lamport_timestamp: time::Lamport::from_flatbuf( message.lamport_timestamp().ok_or(Error::DeserializeError)?, ), })) } serialization::epoch::Operation::UpdateParent => { let message = serialization::epoch::UpdateParent::init_from_table(message); Ok(Some(Operation::UpdateParent { child_id: FileId::from_flatbuf( message.child_id_type(), message.child_id().ok_or(Error::DeserializeError)?, ), new_parent: parent_from_flatbuf( message.new_parent_id_type(), message.new_parent_id(), message.new_name_in_parent(), ), local_timestamp: time::Local::from_flatbuf( message.local_timestamp().ok_or(Error::DeserializeError)?, ), lamport_timestamp: time::Lamport::from_flatbuf( message.lamport_timestamp().ok_or(Error::DeserializeError)?, ), })) } serialization::epoch::Operation::BufferOperation => { let message = serialization::epoch::BufferOperation::init_from_table(message); let op_messages = message.operations().ok_or(Error::DeserializeError)?; let mut operations = Vec::with_capacity(op_messages.len()); for i in 0..op_messages.len() { if let Some(op) = buffer::Operation::from_flatbuf(&op_messages.get(i))? { operations.push(op); } } Ok(Some(Operation::BufferOperation { file_id: FileId::from_flatbuf( message.file_id_type(), message.file_id().ok_or(Error::DeserializeError)?, ), operations, local_timestamp: time::Local::from_flatbuf( message.local_timestamp().ok_or(Error::DeserializeError)?, ), lamport_timestamp: time::Lamport::from_flatbuf( message.lamport_timestamp().ok_or(Error::DeserializeError)?, ), })) } serialization::epoch::Operation::UpdateActiveLocation => { let message = serialization::epoch::UpdateActiveLocation::init_from_table(message); let file_id = if let Some(file_id) = message.file_id() { Some(FileId::from_flatbuf(message.file_id_type(), file_id)) } else { None }; Ok(Some(Operation::UpdateActiveLocation { file_id, lamport_timestamp: time::Lamport::from_flatbuf( message.lamport_timestamp().ok_or(Error::DeserializeError)?, ), })) } serialization::epoch::Operation::NONE => Ok(None), } } } impl operation_queue::Operation for Operation { fn timestamp(&self) -> time::Lamport { self.lamport_timestamp() } } impl FileId { pub fn is_base(&self) -> bool { if let FileId::Base(_) = self { true } else { false } } fn to_flatbuf<'fbb>( &self, builder: &mut FlatBufferBuilder<'fbb>, ) -> (serialization::epoch::FileId, WIPOffset) { use crate::serialization::epoch::{ BaseFileId, BaseFileIdArgs, FileId as FileIdType, NewFileId, NewFileIdArgs, }; match self { FileId::Base(index) => ( FileIdType::BaseFileId, BaseFileId::create(builder, &BaseFileIdArgs { index: *index }).as_union_value(), ), FileId::New(id) => ( FileIdType::NewFileId, NewFileId::create( builder, &NewFileIdArgs { id: Some(&id.to_flatbuf()), }, ) .as_union_value(), ), } } fn from_flatbuf<'a>( file_id_type: serialization::epoch::FileId, message: flatbuffers::Table<'a>, ) -> Self { match file_id_type { serialization::epoch::FileId::BaseFileId => { let message = serialization::epoch::BaseFileId::init_from_table(message); FileId::Base(message.index()) } serialization::epoch::FileId::NewFileId => { let message = serialization::epoch::NewFileId::init_from_table(message); FileId::New(time::Local::from_flatbuf(&message.id().unwrap())) } serialization::epoch::FileId::NONE => unreachable!(), } } } impl FileType { fn to_flatbuf(&self) -> serialization::epoch::FileType { match self { FileType::Directory => serialization::epoch::FileType::Directory, FileType::Text => serialization::epoch::FileType::Text, } } fn from_flatbuf(message: &serialization::epoch::FileType) -> Self { match message { serialization::epoch::FileType::Directory => FileType::Directory, serialization::epoch::FileType::Text => FileType::Text, } } } impl btree::Dimension for FileId { fn from_summary(summary: &Self) -> Self { *summary } } impl Default for FileId { fn default() -> Self { FileId::Base(0) } } impl<'a> AddAssign<&'a Self> for FileId { fn add_assign(&mut self, other: &Self) { assert!(*self <= *other); *self = other.clone(); } } impl<'a> Add<&'a Self> for FileId { type Output = Self; fn add(self, other: &Self) -> Self { assert!(self <= *other); other.clone() } } impl btree::Item for Metadata { type Summary = FileId; fn summarize(&self) -> Self::Summary { use crate::btree::KeyedItem; self.key() } } impl btree::KeyedItem for Metadata { type Key = FileId; fn key(&self) -> Self::Key { self.file_id } } impl btree::Item for ParentRefValue { type Summary = ParentRefValueKey; fn summarize(&self) -> Self::Summary { use crate::btree::KeyedItem; self.key() } } impl btree::KeyedItem for ParentRefValue { type Key = ParentRefValueKey; fn key(&self) -> Self::Key { ParentRefValueKey { child_id: self.child_id, timestamp: self.timestamp, } } } impl btree::Dimension for ParentRefValueKey { fn from_summary(summary: &ParentRefValueKey) -> ParentRefValueKey { summary.clone() } } impl Ord for ParentRefValueKey { fn cmp(&self, other: &Self) -> Ordering { self.child_id .cmp(&other.child_id) .then_with(|| self.timestamp.cmp(&other.timestamp).reverse()) } } impl PartialOrd for ParentRefValueKey { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } } impl<'a> AddAssign<&'a Self> for ParentRefValueKey { fn add_assign(&mut self, other: &Self) { assert!(*self < *other); *self = other.clone(); } } impl<'a> Add<&'a Self> for ParentRefValueKey { type Output = Self; fn add(self, other: &Self) -> Self { assert!(self < *other); other.clone() } } impl btree::Dimension for FileId { fn from_summary(summary: &ParentRefValueKey) -> Self { summary.child_id } } impl btree::Item for ChildRefValue { type Summary = ChildRefValueSummary; fn summarize(&self) -> Self::Summary { ChildRefValueSummary { parent_id: self.parent_id, name: self.name.clone(), visible: self.visible, timestamp: self.timestamp, visible_count: if self.visible { 1 } else { 0 }, } } } impl btree::KeyedItem for ChildRefValue { type Key = ChildRefValueKey; fn key(&self) -> Self::Key { ChildRefValueKey { parent_id: self.parent_id, name: self.name.clone(), visible: self.visible, timestamp: self.timestamp, } } } impl Ord for ChildRefValueSummary { fn cmp(&self, other: &Self) -> Ordering { self.parent_id .cmp(&other.parent_id) .then_with(|| self.name.cmp(&other.name)) .then_with(|| self.visible.cmp(&other.visible).reverse()) .then_with(|| self.timestamp.cmp(&other.timestamp).reverse()) } } impl PartialOrd for ChildRefValueSummary { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } } impl<'a> AddAssign<&'a Self> for ChildRefValueSummary { fn add_assign(&mut self, other: &Self) { assert!(*self < *other, "{:?} < {:?}", self, other); self.parent_id = other.parent_id; self.name = other.name.clone(); self.visible = other.visible; self.timestamp = other.timestamp; self.visible_count += other.visible_count; } } impl btree::Dimension for FileId { fn from_summary(summary: &ChildRefValueSummary) -> Self { summary.parent_id } } impl btree::Dimension for ChildRefValueKey { fn from_summary(summary: &ChildRefValueSummary) -> ChildRefValueKey { ChildRefValueKey { parent_id: summary.parent_id, name: summary.name.clone(), visible: summary.visible, timestamp: summary.timestamp, } } } impl Ord for ChildRefValueKey { fn cmp(&self, other: &Self) -> Ordering { self.parent_id .cmp(&other.parent_id) .then_with(|| self.name.cmp(&other.name)) .then_with(|| self.visible.cmp(&other.visible).reverse()) .then_with(|| self.timestamp.cmp(&other.timestamp).reverse()) } } impl PartialOrd for ChildRefValueKey { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } } impl<'a> AddAssign<&'a Self> for ChildRefValueKey { fn add_assign(&mut self, other: &Self) { assert!(*self < *other); *self = other.clone(); } } impl<'a> Add<&'a Self> for ChildRefValueKey { type Output = Self; fn add(self, other: &Self) -> Self { assert!(self < *other); other.clone() } } impl btree::Dimension for ChildRefKey { fn from_summary(summary: &ChildRefValueSummary) -> Self { ChildRefKey { parent_id: summary.parent_id, name: summary.name.clone(), } } } impl<'a> AddAssign<&'a Self> for ChildRefKey { fn add_assign(&mut self, other: &Self) { assert!(*self <= *other); *self = other.clone(); } } impl<'a> Add<&'a Self> for ChildRefKey { type Output = Self; fn add(self, other: &Self) -> Self { assert!(self <= *other); other.clone() } } impl btree::Dimension for usize { fn from_summary(summary: &ChildRefValueSummary) -> Self { summary.visible_count } } impl TextFile { fn is_modified(&self) -> bool { match self { TextFile::Deferred(ops) => ops.iter().any(|op| op.is_edit()), TextFile::Buffered(buffer) => buffer.is_modified(), } } #[cfg(test)] fn is_buffered(&self) -> bool { match self { TextFile::Buffered(_) => true, _ => false, } } } fn serialize_os_string(os_string: &OsString, serializer: S) -> Result where S: Serializer, { os_string.to_string_lossy().serialize(serializer) } fn deserialize_os_string<'de, D>(deserializer: D) -> Result where D: Deserializer<'de>, { Ok(OsString::from(String::deserialize(deserializer)?)) } #[cfg(test)] mod tests { use super::*; use crate::buffer::Point; use rand::{Rng, SeedableRng, StdRng}; use uuid::Uuid; #[test] fn test_append_base_entries() { let replica_id = Uuid::nil(); let mut epoch = Epoch::with_replica_id(replica_id); let mut lamport_clock = time::Lamport::new(replica_id); assert!(epoch.paths().is_empty()); let fixup_ops = epoch .append_base_entries( vec![ DirEntry { depth: 1, name: OsString::from("a"), file_type: FileType::Directory, }, DirEntry { depth: 2, name: OsString::from("b"), file_type: FileType::Directory, }, DirEntry { depth: 3, name: OsString::from("c"), file_type: FileType::Text, }, DirEntry { depth: 2, name: OsString::from("d"), file_type: FileType::Directory, }, ], &mut lamport_clock, ) .unwrap(); assert_eq!(epoch.paths(), vec!["a", "a/b", "a/b/c", "a/d"]); assert_eq!(fixup_ops.len(), 0); let a = epoch.file_id("a").unwrap(); let (file_1, _) = epoch.new_text_file(&mut lamport_clock); epoch.rename(file_1, a, "e", &mut lamport_clock).unwrap(); epoch .create_file(a, "z", FileType::Directory, &mut lamport_clock) .unwrap(); let fixup_ops = epoch .append_base_entries( vec![ DirEntry { depth: 2, name: OsString::from("e"), file_type: FileType::Directory, }, DirEntry { depth: 1, name: OsString::from("f"), file_type: FileType::Text, }, ], &mut lamport_clock, ) .unwrap(); assert_eq!( epoch.paths(), vec!["a", "a/b", "a/b/c", "a/d", "a/e", "a/e~", "a/z", "f"] ); assert_eq!(fixup_ops.len(), 1); } #[test] fn test_cursor() { let replica_id = Uuid::nil(); let mut epoch = Epoch::with_replica_id(replica_id); let mut lamport_clock = time::Lamport::new(replica_id); epoch .append_base_entries( vec![ DirEntry { depth: 1, name: OsString::from("a"), file_type: FileType::Directory, }, DirEntry { depth: 2, name: OsString::from("b"), file_type: FileType::Directory, }, DirEntry { depth: 3, name: OsString::from("c"), file_type: FileType::Text, }, DirEntry { depth: 2, name: OsString::from("d"), file_type: FileType::Directory, }, DirEntry { depth: 2, name: OsString::from("e"), file_type: FileType::Directory, }, DirEntry { depth: 1, name: OsString::from("f"), file_type: FileType::Directory, }, DirEntry { depth: 2, name: OsString::from("g"), file_type: FileType::Text, }, ], &mut lamport_clock, ) .unwrap(); let a = epoch.file_id("a").unwrap(); let b = epoch.file_id("a/b").unwrap(); let c = epoch.file_id("a/b/c").unwrap(); let d = epoch.file_id("a/d").unwrap(); let e = epoch.file_id("a/e").unwrap(); let f = epoch.file_id("f").unwrap(); let g = epoch.file_id("f/g").unwrap(); epoch.remove(b, &mut lamport_clock).unwrap(); let (new_file, _) = epoch.new_text_file(&mut lamport_clock); epoch.rename(new_file, a, "x", &mut lamport_clock).unwrap(); let (new_file_that_got_removed, _) = epoch.new_text_file(&mut lamport_clock); epoch .rename(new_file_that_got_removed, e, "y", &mut lamport_clock) .unwrap(); epoch .remove(new_file_that_got_removed, &mut lamport_clock) .unwrap(); epoch.rename(e, a, "z", &mut lamport_clock).unwrap(); epoch.open_text_file(c, "123", &mut lamport_clock).unwrap(); epoch.edit(c, Some(0..0), "x", &mut lamport_clock).unwrap(); epoch .rename(g, ROOT_FILE_ID, "g", &mut lamport_clock) .unwrap(); epoch.open_text_file(g, "456", &mut lamport_clock).unwrap(); epoch.edit(g, Some(0..0), "y", &mut lamport_clock).unwrap(); let mut cursor = epoch.cursor().unwrap(); assert_eq!( cursor.entry().unwrap(), CursorEntry { file_id: a, file_type: FileType::Directory, depth: 1, name: Arc::new(OsString::from("a")), status: FileStatus::Unchanged, visible: true, } ); assert!(cursor.next(true)); assert_eq!( cursor.entry().unwrap(), CursorEntry { file_id: b, file_type: FileType::Directory, depth: 2, name: Arc::new(OsString::from("b")), status: FileStatus::Removed, visible: false, } ); assert!(cursor.next(true)); assert_eq!( cursor.entry().unwrap(), CursorEntry { file_id: c, file_type: FileType::Text, depth: 3, name: Arc::new(OsString::from("c")), status: FileStatus::Modified, visible: false, } ); assert!(cursor.next(true)); assert_eq!( cursor.entry().unwrap(), CursorEntry { file_id: d, file_type: FileType::Directory, depth: 2, name: Arc::new(OsString::from("d")), status: FileStatus::Unchanged, visible: true, } ); assert!(cursor.next(true)); assert_eq!( cursor.entry().unwrap(), CursorEntry { file_id: new_file, file_type: FileType::Text, depth: 2, name: Arc::new(OsString::from("x")), status: FileStatus::New, visible: true, } ); assert!(cursor.next(true)); assert_eq!( cursor.entry().unwrap(), CursorEntry { file_id: e, file_type: FileType::Directory, depth: 2, name: Arc::new(OsString::from("z")), status: FileStatus::Renamed, visible: true, } ); assert!(cursor.next(true)); assert_eq!( cursor.entry().unwrap(), CursorEntry { file_id: new_file_that_got_removed, file_type: FileType::Text, depth: 3, name: Arc::new(OsString::from("y")), status: FileStatus::New, visible: false, } ); assert!(cursor.next(true)); assert_eq!( cursor.entry().unwrap(), CursorEntry { file_id: f, file_type: FileType::Directory, depth: 1, name: Arc::new(OsString::from("f")), status: FileStatus::Unchanged, visible: true, } ); assert!(cursor.next(true)); assert_eq!( cursor.entry().unwrap(), CursorEntry { file_id: g, file_type: FileType::Text, depth: 1, name: Arc::new(OsString::from("g")), status: FileStatus::RenamedAndModified, visible: true, } ); assert!(!cursor.next(true)); assert!(cursor.entry().is_err()); } #[test] fn test_buffers() { let base_entries = vec![ DirEntry { depth: 1, name: OsString::from("dir"), file_type: FileType::Directory, }, DirEntry { depth: 1, name: OsString::from("file"), file_type: FileType::Text, }, ]; let base_text = Text::from("abc"); let replica_id_1 = Uuid::from_u128(1); let mut epoch_1 = Epoch::with_replica_id(replica_id_1); let mut lamport_clock_1 = time::Lamport::new(replica_id_1); epoch_1 .append_base_entries(base_entries.clone(), &mut lamport_clock_1) .unwrap(); let replica_id_2 = Uuid::from_u128(2); let mut epoch_2 = Epoch::with_replica_id(replica_id_2); let mut lamport_clock_2 = time::Lamport::new(replica_id_2); epoch_2 .append_base_entries(base_entries, &mut lamport_clock_2) .unwrap(); let file_id = epoch_1.file_id("file").unwrap(); epoch_2 .open_text_file(file_id, base_text.clone(), &mut lamport_clock_2) .unwrap(); let ops = epoch_2.edit(file_id, vec![1..2, 3..3], "x", &mut lamport_clock_2); epoch_1.apply_ops(ops, &mut lamport_clock_1).unwrap(); // Must call open_text_file on any given replica first before interacting with a buffer. assert!(epoch_1.text(file_id).is_err()); epoch_1 .open_text_file(file_id, base_text, &mut lamport_clock_1) .unwrap(); assert_eq!(epoch_1.text(file_id).unwrap().into_string(), "axcx"); assert_eq!(epoch_2.text(file_id).unwrap().into_string(), "axcx"); let ops = epoch_1.edit(file_id, vec![1..2, 4..4], "y", &mut lamport_clock_1); let base_version = epoch_2.version(); epoch_2.apply_ops(ops, &mut lamport_clock_2).unwrap(); assert_eq!(epoch_1.text(file_id).unwrap().into_string(), "aycxy"); assert_eq!(epoch_2.text(file_id).unwrap().into_string(), "aycxy"); let changes = epoch_2 .changes_since(file_id, &base_version) .unwrap() .collect::>(); assert_eq!(changes.len(), 2); assert_eq!(changes[0].range, Point::new(0, 1)..Point::new(0, 2)); assert_eq!(changes[0].code_units, [b'y' as u16]); assert_eq!(changes[1].range, Point::new(0, 4)..Point::new(0, 4)); assert_eq!(changes[1].code_units, [b'y' as u16]); let dir_id = epoch_1.file_id("dir").unwrap(); assert!(epoch_1 .open_text_file(dir_id, Text::from(""), &mut lamport_clock_1) .is_err()); } #[test] fn test_buffer_deferred_ops_len() -> Result<(), Error> { let replica_1_id = Uuid::from_u128(1); let mut epoch_1 = Epoch::with_replica_id(replica_1_id); let mut clock_1 = time::Lamport::new(replica_1_id); let (file_id, new_file_op) = epoch_1.new_text_file(&mut clock_1); epoch_1.open_text_file(file_id, "", &mut clock_1).unwrap(); let edit_1_op = epoch_1.edit(file_id, Some(0..0), "135", &mut clock_1)?; let edit_2_op = epoch_1.edit(file_id, Some(1..1), "2", &mut clock_1)?; let edit_3_op = epoch_1.edit(file_id, Some(3..3), "4", &mut clock_1)?; let replica_2_id = Uuid::from_u128(2); let mut epoch_2 = Epoch::with_replica_id(replica_2_id); let mut clock_2 = time::Lamport::new(replica_2_id); epoch_2.apply_ops(Some(new_file_op.clone()), &mut clock_2)?; epoch_2.open_text_file(file_id, "", &mut clock_2)?; assert_eq!(epoch_2.buffer_deferred_ops_len(file_id)?, 0); epoch_2.apply_ops(Some(edit_3_op.clone()), &mut clock_2)?; assert_eq!(epoch_2.buffer_deferred_ops_len(file_id)?, 1); epoch_2.apply_ops(Some(edit_2_op.clone()), &mut clock_2)?; assert_eq!(epoch_2.buffer_deferred_ops_len(file_id)?, 2); epoch_2.apply_ops(Some(edit_1_op.clone()), &mut clock_2)?; assert_eq!(epoch_2.buffer_deferred_ops_len(file_id)?, 0); // If the buffer has never been opened, we can't determine how many operations are deferred. let replica_3_id = Uuid::from_u128(3); let mut epoch_3 = Epoch::with_replica_id(replica_3_id); let mut clock_3 = time::Lamport::new(replica_3_id); epoch_3.apply_ops(Some(new_file_op), &mut clock_3)?; epoch_3.apply_ops(Some(edit_3_op), &mut clock_3)?; assert!(epoch_3.buffer_deferred_ops_len(file_id).is_err()); Ok(()) } #[test] fn test_replication_random() { use crate::tests::Network; const PEERS: usize = 5; for seed in 0..100 { println!("SEED: {:?}", seed); let mut rng = StdRng::from_seed(&[seed]); let mut base_epoch = Epoch::with_replica_id(Uuid::nil()); base_epoch.randomly_mutate(&mut rng, &mut time::Lamport::new(Uuid::nil()), 20); let base_entries = base_epoch.entries(); let base_entries = base_entries .iter() .filter(|entry| entry.visible) .map(|entry| DirEntry { depth: entry.depth, name: entry.name.as_ref().clone(), file_type: entry.file_type, }) .collect::>(); let mut base_epoch = Epoch::with_replica_id(Uuid::nil()); base_epoch .append_base_entries(base_entries.clone(), &mut time::Lamport::new(Uuid::nil())) .unwrap(); let mut replica_ids = Vec::new(); let mut epochs = Vec::new(); let mut lamport_clocks = Vec::new(); let mut base_entries_to_append = Vec::new(); let mut network = Network::new(); for i in 0..PEERS { let replica_id = Uuid::from_u128((i + 1) as u128); replica_ids.push(replica_id); epochs.push(Epoch::with_replica_id(replica_id)); lamport_clocks.push(time::Lamport::new(replica_id)); base_entries_to_append.push(base_entries.clone()); network.add_peer(replica_id); } // Generate and deliver random mutations for _ in 0..10 { let k = rng.gen_range(0, 10); let replica_index = rng.gen_range(0, PEERS); let replica_id = replica_ids[replica_index]; let epoch = &mut epochs[replica_index]; let lamport_clock = &mut lamport_clocks[replica_index]; let base_entries_to_append = &mut base_entries_to_append[replica_index]; if k < 3 && !base_entries_to_append.is_empty() { let count = rng.gen_range(0, base_entries_to_append.len()); let fixup_ops = epoch .append_base_entries(base_entries_to_append.drain(0..count), lamport_clock) .unwrap(); network.broadcast(replica_id, fixup_ops, &mut rng); } else if k < 6 && network.has_unreceived(replica_id) { let fixup_ops = epoch .apply_ops(network.receive(replica_id, &mut rng), lamport_clock) .unwrap(); network.broadcast(replica_id, fixup_ops, &mut rng); } else if k < 7 && !network.all_messages().is_empty() { network.clear_unreceived(replica_id); *base_entries_to_append = base_entries.clone(); *epoch = Epoch::with_replica_id(epoch.local_clock.replica_id); let fixup_ops = epoch .apply_ops(network.all_messages().iter().cloned(), lamport_clock) .unwrap(); network.broadcast(replica_id, fixup_ops, &mut rng); } else { let ops = epoch.randomly_mutate(&mut rng, lamport_clock, 5); network.broadcast(replica_id, ops, &mut rng); } } // Allow system to quiesce loop { let mut done = true; for replica_index in 0..PEERS { let replica_id = replica_ids[replica_index]; let epoch = &mut epochs[replica_index]; let lamport_clock = &mut lamport_clocks[replica_index]; let base_entries_to_append = &mut base_entries_to_append[replica_index]; if !base_entries_to_append.is_empty() { let fixup_ops = epoch .append_base_entries(base_entries_to_append.drain(..), lamport_clock) .unwrap(); network.broadcast(replica_id, fixup_ops, &mut rng); } if network.has_unreceived(replica_id) { let fixup_ops = epoch .apply_ops(network.receive(replica_id, &mut rng), lamport_clock) .unwrap(); network.broadcast(replica_id, fixup_ops, &mut rng); done = false; } } if done { break; } } for i in 0..PEERS { assert!(epochs[i].deferred_ops.is_empty()); } for i in 0..PEERS - 1 { assert_eq!(epochs[i].entries(), epochs[i + 1].entries()); assert_eq!( epochs[i].replica_locations().collect::>(), epochs[i + 1].replica_locations().collect::>() ); } for i in 0..PEERS { for _ in 0..rng.gen_range(0, 5) { let base_file_id = FileId::Base(rng.gen_range(0, base_entries.len() as u64 + 1)); assert_eq!( epochs[i].base_path(base_file_id).unwrap(), base_epoch.path(base_file_id).unwrap() ); } } } } impl Epoch { pub fn with_replica_id(replica_id: ReplicaId) -> Self { Epoch::new(replica_id, Id::default(), None) } pub fn entries(&self) -> Vec { let mut entries = Vec::new(); if let Some(mut cursor) = self.cursor() { loop { entries.push(cursor.entry().unwrap()); if !cursor.next(true) { break; } } } entries } pub fn dir_entries(&self) -> Vec { let mut entries = Vec::new(); if let Some(mut cursor) = self.cursor() { loop { let entry = cursor.entry().unwrap(); let advanced = if entry.visible { entries.push(entry.into()); cursor.next(true) } else { cursor.next(false) }; if !advanced { break; } } } entries } fn paths(&self) -> Vec { let mut paths = Vec::new(); if let Some(mut cursor) = self.cursor() { loop { paths.push(cursor.path().unwrap().to_string_lossy().into_owned()); if !cursor.next(true) { break; } } } paths } pub fn randomly_mutate( &mut self, rng: &mut T, lamport_clock: &mut time::Lamport, count: usize, ) -> Vec { let mut ops = Vec::new(); for _ in 0..count { let k = rng.gen_range(0, 10); if self.child_refs.is_empty() || k < 2 { // println!("Random mutation: Creating file"); let parent_id = self .select_file(rng, Some(FileType::Directory), true) .unwrap(); loop { let name = gen_name(rng); let file_type = if rng.gen() { FileType::Directory } else { FileType::Text }; match self.create_file(parent_id, name, file_type, lamport_clock) { Ok(op) => { ops.push(op); break; } Err(_) => {} } } } else if k < 4 { let file_id = self.select_file(rng, None, false).unwrap(); // println!("Random mutation: Removing {:?}", file_id); ops.push(self.remove(file_id, lamport_clock).unwrap()); } else if k < 7 { let file_id = self.select_file(rng, None, false).unwrap(); loop { let new_parent_id = self .select_file(rng, Some(FileType::Directory), true) .unwrap(); let new_name = gen_name(rng); // println!( // "Random mutation: Attempting to move {:?} to ({:?}, {:?})", // file_id, new_parent_id, new_name // ); match self.rename(file_id, new_parent_id, new_name, lamport_clock) { Ok(op) => { ops.push(op); break; } Err(_error) => {} } } } else if k < 9 && self.text_files.values().any(|f| f.is_buffered()) { let buffered_file_ids = self .text_files .iter() .filter_map(|(file_id, file)| { if file.is_buffered() { Some(*file_id) } else { None } }) .collect::>(); let file_id = *rng.choose(&buffered_file_ids).unwrap(); let op = self .mutate_buffer( file_id, lamport_clock, |buffer, local_clock, lamport_clock| { let (_, _, ops) = buffer.randomly_mutate(rng, local_clock, lamport_clock); Ok(ops) }, ) .unwrap(); ops.push(op); } else { let file_id = self.select_file(rng, Some(FileType::Text), false); let op = self.set_active_location(file_id, lamport_clock).unwrap(); ops.push(op); } } ops } fn select_file( &self, rng: &mut T, file_type: Option, allow_root: bool, ) -> Option { let metadata = self .metadata .cursor() .filter(|metadata| file_type.is_none() || file_type.unwrap() == metadata.file_type) .collect::>(); if allow_root && file_type.map_or(true, |file_type| file_type == FileType::Directory) && rng.gen_weighted_bool(metadata.len() as u32 + 1) { Some(ROOT_FILE_ID) } else { rng.choose(&metadata).map(|metadata| metadata.file_id) } } } impl From for DirEntry { fn from(entry: CursorEntry) -> Self { Self { depth: entry.depth, name: entry.name.as_ref().clone(), file_type: entry.file_type, } } } fn gen_name(rng: &mut T) -> String { let mut name = String::new(); for _ in 0..rng.gen_range(1, 4) { name.push(rng.gen_range(b'a', b'z' + 1).into()); } if rng.gen_weighted_bool(5) { for _ in 0..rng.gen_range(1, 2) { name.push('~'); } } name } } ================================================ FILE: memo_core/src/lib.rs ================================================ mod btree; mod buffer; mod epoch; #[allow(non_snake_case, unused_imports)] mod operation_queue; mod serialization; pub mod time; mod work_tree; pub use crate::buffer::{Buffer, Change, Point}; pub use crate::epoch::{Cursor, DirEntry, Epoch, FileStatus, FileType, ROOT_FILE_ID}; pub use crate::work_tree::{ BufferId, BufferSelectionRanges, ChangeObserver, GitProvider, LocalSelectionSetId, Operation, OperationEnvelope, WorkTree, }; use std::borrow::Cow; use std::fmt; use std::io; use uuid::Uuid; pub type ReplicaId = Uuid; pub type Oid = [u8; 20]; #[derive(Debug)] pub enum Error { IoError(io::Error), DeserializeError, InvalidPath(Cow<'static, str>), InvalidOperations, InvalidFileId(Cow<'static, str>), InvalidBufferId, InvalidDirEntry, InvalidOperation, InvalidSelectionSet(buffer::SelectionSetId), InvalidLocalSelectionSet(LocalSelectionSetId), InvalidAnchor(Cow<'static, str>), OffsetOutOfRange, CursorExhausted, } trait ReplicaIdExt { fn to_flatbuf(&self) -> serialization::ReplicaId; fn from_flatbuf(message: &serialization::ReplicaId) -> Self; } impl ReplicaIdExt for ReplicaId { fn to_flatbuf(&self) -> serialization::ReplicaId { fn u64_from_bytes(bytes: &[u8]) -> u64 { let mut n = 0; for i in 0..8 { n |= (bytes[i] as u64) << i * 8; } n } let bytes = self.as_bytes(); serialization::ReplicaId::new(u64_from_bytes(&bytes[0..8]), u64_from_bytes(&bytes[8..16])) } fn from_flatbuf(message: &serialization::ReplicaId) -> Self { fn bytes_from_u64(n: u64) -> [u8; 8] { let mut bytes = [0; 8]; for i in 0..8 { bytes[i] = (n >> i * 8) as u8; } bytes } let mut bytes = [0; 16]; bytes[0..8].copy_from_slice(&bytes_from_u64(message.first_8_bytes())); bytes[8..16].copy_from_slice(&bytes_from_u64(message.last_8_bytes())); Uuid::from_bytes(bytes) } } impl From for String { fn from(error: Error) -> Self { format!("{:?}", error) } } impl From for Error { fn from(error: io::Error) -> Self { Error::IoError(error) } } impl fmt::Display for Error { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fmt::Debug::fmt(self, f) } } impl PartialEq for Error { fn eq(&self, other: &Self) -> bool { match (self, other) { (Error::IoError(err_1), Error::IoError(err_2)) => { err_1.kind() == err_2.kind() && err_1.to_string() == err_2.to_string() } (Error::DeserializeError, Error::DeserializeError) => true, (Error::InvalidPath(err_1), Error::InvalidPath(err_2)) => err_1 == err_2, (Error::InvalidOperations, Error::InvalidOperations) => true, (Error::InvalidFileId(err_1), Error::InvalidFileId(err_2)) => err_1 == err_2, (Error::InvalidBufferId, Error::InvalidBufferId) => true, (Error::InvalidDirEntry, Error::InvalidDirEntry) => true, (Error::InvalidOperation, Error::InvalidOperation) => true, (Error::InvalidSelectionSet(id_1), Error::InvalidSelectionSet(id_2)) => id_1 == id_2, (Error::InvalidLocalSelectionSet(id_1), Error::InvalidLocalSelectionSet(id_2)) => { id_1 == id_2 } (Error::InvalidAnchor(err_1), Error::InvalidAnchor(err_2)) => err_1 == err_2, (Error::OffsetOutOfRange, Error::OffsetOutOfRange) => true, (Error::CursorExhausted, Error::CursorExhausted) => true, _ => false, } } } #[cfg(test)] mod tests { use crate::ReplicaId; use rand::Rng; use std::collections::BTreeMap; #[derive(Clone)] struct Envelope { message: T, sender: ReplicaId, } pub(crate) struct Network { inboxes: BTreeMap>>, all_messages: Vec, } impl Network { pub fn new() -> Self { Network { inboxes: BTreeMap::new(), all_messages: Vec::new(), } } pub fn add_peer(&mut self, id: ReplicaId) { self.inboxes.insert(id, Vec::new()); } pub fn is_idle(&self) -> bool { self.inboxes.values().all(|i| i.is_empty()) } pub fn all_messages(&self) -> &Vec { &self.all_messages } pub fn broadcast(&mut self, sender: ReplicaId, messages: Vec, rng: &mut R) where R: Rng, { for (replica, inbox) in self.inboxes.iter_mut() { if *replica != sender { for message in &messages { let min_index = inbox .iter() .enumerate() .rev() .find_map(|(index, envelope)| { if sender == envelope.sender { Some(index + 1) } else { None } }) .unwrap_or(0); // Insert one or more duplicates of this message *after* the previous // message delivered by this replica. for _ in 0..rng.gen_range(1, 4) { let insertion_index = rng.gen_range(min_index, inbox.len() + 1); inbox.insert( insertion_index, Envelope { message: message.clone(), sender, }, ); } } } } self.all_messages.extend(messages); } pub fn has_unreceived(&self, receiver: ReplicaId) -> bool { !self.inboxes[&receiver].is_empty() } pub fn receive(&mut self, receiver: ReplicaId, rng: &mut R) -> Vec where R: Rng, { let inbox = self.inboxes.get_mut(&receiver).unwrap(); let count = rng.gen_range(0, inbox.len() + 1); inbox .drain(0..count) .map(|envelope| envelope.message) .collect() } pub fn clear_unreceived(&mut self, receiver: ReplicaId) { self.inboxes.get_mut(&receiver).unwrap().clear(); } } } ================================================ FILE: memo_core/src/operation_queue.rs ================================================ use crate::btree::{Cursor, Dimension, Edit, Item, KeyedItem, Tree}; use crate::time; use std::fmt::Debug; use std::ops::{Add, AddAssign}; pub trait Operation: Clone + Debug + Eq { fn timestamp(&self) -> time::Lamport; } #[derive(Clone, Debug)] pub struct OperationQueue(Tree); #[derive(Clone, Copy, Debug, Default, Eq, Ord, PartialEq, PartialOrd)] pub struct OperationKey(time::Lamport); #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] pub struct OperationSummary { key: OperationKey, len: usize, } impl OperationQueue { pub fn new() -> Self { OperationQueue(Tree::new()) } #[cfg(test)] pub fn is_empty(&self) -> bool { self.0.is_empty() } pub fn len(&self) -> usize { self.0.summary().len } pub fn insert(&mut self, mut ops: Vec) { ops.sort_by_key(|op| op.timestamp()); ops.dedup_by_key(|op| op.timestamp()); let mut edits = ops .into_iter() .map(|op| Edit::Insert(op)) .collect::>>(); self.0.edit(&mut edits); } pub fn drain(&mut self) -> Cursor { let cursor = self.0.cursor(); self.0 = Tree::new(); cursor } } impl Item for T { type Summary = OperationSummary; fn summarize(&self) -> Self::Summary { OperationSummary { key: OperationKey(self.timestamp()), len: 1, } } } impl KeyedItem for T { type Key = OperationKey; fn key(&self) -> Self::Key { OperationKey(self.timestamp()) } } impl<'a> AddAssign<&'a Self> for OperationSummary { fn add_assign(&mut self, other: &Self) { assert!(self.key < other.key); self.key = other.key; self.len += other.len; } } impl<'a> Add<&'a Self> for OperationSummary { type Output = Self; fn add(self, other: &Self) -> Self { assert!(self.key < other.key); OperationSummary { key: other.key, len: self.len + other.len, } } } impl Dimension for OperationKey { fn from_summary(summary: &OperationSummary) -> Self { summary.key } } impl<'a> Add<&'a Self> for OperationKey { type Output = Self; fn add(self, other: &Self) -> Self { assert!(self < *other); *other } } impl<'a> AddAssign<&'a Self> for OperationKey { fn add_assign(&mut self, other: &Self) { assert!(*self < *other); *self = *other; } } #[cfg(test)] mod tests { use super::*; use crate::ReplicaId; #[test] fn test_len() { let mut clock = time::Lamport::new(ReplicaId::from_u128(1)); let mut queue = OperationQueue::new(); assert_eq!(queue.len(), 0); queue.insert(vec![ TestOperation(clock.tick()), TestOperation(clock.tick()), ]); assert_eq!(queue.len(), 2); queue.insert(vec![TestOperation(clock.tick())]); assert_eq!(queue.len(), 3); drop(queue.drain()); assert_eq!(queue.len(), 0); queue.insert(vec![TestOperation(clock.tick())]); assert_eq!(queue.len(), 1); } #[derive(Clone, Debug, Eq, PartialEq)] struct TestOperation(time::Lamport); impl Operation for TestOperation { fn timestamp(&self) -> time::Lamport { self.0 } } } ================================================ FILE: memo_core/src/serialization/mod.rs ================================================ mod schema_generated; pub use self::schema_generated::*; ================================================ FILE: memo_core/src/serialization/schema.fbs ================================================ struct ReplicaId { first_8_bytes: uint64; last_8_bytes: uint64; } struct Timestamp { value:uint64; replica_id:ReplicaId; } table GlobalTimestamp { timestamps:[Timestamp]; } namespace buffer; enum AnchorVariant : byte { Start, Middle, End } enum AnchorBias : byte { Left, Right } table Anchor { variant:AnchorVariant; insertion_id:Timestamp; offset:uint64; bias:AnchorBias; } table Selection { start:Anchor; end:Anchor; reversed:bool; } table Edit { start_id:Timestamp; start_offset:uint64; end_id:Timestamp; end_offset:uint64; version_in_range:GlobalTimestamp; new_text:string; local_timestamp:Timestamp; lamport_timestamp:Timestamp; } table UpdateSelections { set_id:Timestamp; selections:[Selection]; lamport_timestamp:Timestamp; } union OperationVariant { Edit, UpdateSelections } table Operation { variant: OperationVariant; } namespace epoch; table BaseFileId { index:uint64; } table NewFileId { id:Timestamp; } union FileId { BaseFileId, NewFileId } enum FileType : byte { Directory, Text } table InsertMetadata { file_id:FileId; file_type:FileType; parent_id:FileId; name_in_parent:string; local_timestamp:Timestamp; lamport_timestamp:Timestamp; } table UpdateParent { child_id:FileId; new_parent_id:FileId; new_name_in_parent:string; local_timestamp:Timestamp; lamport_timestamp:Timestamp; } table BufferOperation { file_id:FileId; operations:[buffer.Operation]; local_timestamp:Timestamp; lamport_timestamp:Timestamp; } table UpdateActiveLocation { file_id:FileId; lamport_timestamp:Timestamp; } union Operation { InsertMetadata, UpdateParent, BufferOperation, UpdateActiveLocation } namespace worktree; table StartEpoch { epoch_id:Timestamp; head:[ubyte]; } table EpochOperation { epoch_id:Timestamp; operation:epoch.Operation; } union OperationVariant { StartEpoch, EpochOperation } table Operation { variant:OperationVariant; } root_type Operation; ================================================ FILE: memo_core/src/serialization/schema_generated.rs ================================================ // automatically generated by the FlatBuffers compiler, do not modify #![allow(dead_code)] #![allow(unused_imports)] extern crate flatbuffers; // struct ReplicaId, aligned to 8 #[repr(C, align(8))] #[derive(Clone, Copy, Debug, PartialEq)] pub struct ReplicaId { first_8_bytes_: u64, last_8_bytes_: u64, } // pub struct ReplicaId impl flatbuffers::SafeSliceAccess for ReplicaId {} impl<'a> flatbuffers::Follow<'a> for ReplicaId { type Inner = &'a ReplicaId; #[inline] fn follow(buf: &'a [u8], loc: usize) -> Self::Inner { <&'a ReplicaId>::follow(buf, loc) } } impl<'a> flatbuffers::Follow<'a> for &'a ReplicaId { type Inner = &'a ReplicaId; #[inline] fn follow(buf: &'a [u8], loc: usize) -> Self::Inner { flatbuffers::follow_cast_ref::(buf, loc) } } impl<'b> flatbuffers::Push for ReplicaId { type Output = ReplicaId; #[inline] fn push(&self, dst: &mut [u8], _rest: &[u8]) { let src = unsafe { ::std::slice::from_raw_parts(self as *const ReplicaId as *const u8, Self::size()) }; dst.copy_from_slice(src); } } impl<'b> flatbuffers::Push for &'b ReplicaId { type Output = ReplicaId; #[inline] fn push(&self, dst: &mut [u8], _rest: &[u8]) { let src = unsafe { ::std::slice::from_raw_parts(*self as *const ReplicaId as *const u8, Self::size()) }; dst.copy_from_slice(src); } } impl ReplicaId { pub fn new<'a>(_first_8_bytes: u64, _last_8_bytes: u64) -> Self { ReplicaId { first_8_bytes_: _first_8_bytes.to_little_endian(), last_8_bytes_: _last_8_bytes.to_little_endian(), } } pub fn first_8_bytes<'a>(&'a self) -> u64 { self.first_8_bytes_.from_little_endian() } pub fn last_8_bytes<'a>(&'a self) -> u64 { self.last_8_bytes_.from_little_endian() } } // struct Timestamp, aligned to 8 #[repr(C, align(8))] #[derive(Clone, Copy, Debug, PartialEq)] pub struct Timestamp { value_: u64, replica_id_: ReplicaId, } // pub struct Timestamp impl flatbuffers::SafeSliceAccess for Timestamp {} impl<'a> flatbuffers::Follow<'a> for Timestamp { type Inner = &'a Timestamp; #[inline] fn follow(buf: &'a [u8], loc: usize) -> Self::Inner { <&'a Timestamp>::follow(buf, loc) } } impl<'a> flatbuffers::Follow<'a> for &'a Timestamp { type Inner = &'a Timestamp; #[inline] fn follow(buf: &'a [u8], loc: usize) -> Self::Inner { flatbuffers::follow_cast_ref::(buf, loc) } } impl<'b> flatbuffers::Push for Timestamp { type Output = Timestamp; #[inline] fn push(&self, dst: &mut [u8], _rest: &[u8]) { let src = unsafe { ::std::slice::from_raw_parts(self as *const Timestamp as *const u8, Self::size()) }; dst.copy_from_slice(src); } } impl<'b> flatbuffers::Push for &'b Timestamp { type Output = Timestamp; #[inline] fn push(&self, dst: &mut [u8], _rest: &[u8]) { let src = unsafe { ::std::slice::from_raw_parts(*self as *const Timestamp as *const u8, Self::size()) }; dst.copy_from_slice(src); } } impl Timestamp { pub fn new<'a>(_value: u64, _replica_id: &'a ReplicaId) -> Self { Timestamp { value_: _value.to_little_endian(), replica_id_: *_replica_id, } } pub fn value<'a>(&'a self) -> u64 { self.value_.from_little_endian() } pub fn replica_id<'a>(&'a self) -> &'a ReplicaId { &self.replica_id_ } } pub enum GlobalTimestampOffset {} #[derive(Copy, Clone, Debug, PartialEq)] pub struct GlobalTimestamp<'a> { pub _tab: flatbuffers::Table<'a>, } impl<'a> flatbuffers::Follow<'a> for GlobalTimestamp<'a> { type Inner = GlobalTimestamp<'a>; #[inline] fn follow(buf: &'a [u8], loc: usize) -> Self::Inner { Self { _tab: flatbuffers::Table { buf: buf, loc: loc }, } } } impl<'a> GlobalTimestamp<'a> { #[inline] pub fn init_from_table(table: flatbuffers::Table<'a>) -> Self { GlobalTimestamp { _tab: table, } } #[allow(unused_mut)] pub fn create<'bldr: 'args, 'args: 'mut_bldr, 'mut_bldr>( _fbb: &'mut_bldr mut flatbuffers::FlatBufferBuilder<'bldr>, args: &'args GlobalTimestampArgs<'args>) -> flatbuffers::WIPOffset> { let mut builder = GlobalTimestampBuilder::new(_fbb); if let Some(x) = args.timestamps { builder.add_timestamps(x); } builder.finish() } pub const VT_TIMESTAMPS: flatbuffers::VOffsetT = 4; #[inline] pub fn timestamps(&self) -> Option<&'a [Timestamp]> { self._tab.get::>>(GlobalTimestamp::VT_TIMESTAMPS, None).map(|v| v.safe_slice() ) } } pub struct GlobalTimestampArgs<'a> { pub timestamps: Option>>, } impl<'a> Default for GlobalTimestampArgs<'a> { #[inline] fn default() -> Self { GlobalTimestampArgs { timestamps: None, } } } pub struct GlobalTimestampBuilder<'a: 'b, 'b> { fbb_: &'b mut flatbuffers::FlatBufferBuilder<'a>, start_: flatbuffers::WIPOffset, } impl<'a: 'b, 'b> GlobalTimestampBuilder<'a, 'b> { #[inline] pub fn add_timestamps(&mut self, timestamps: flatbuffers::WIPOffset>) { self.fbb_.push_slot_always::>(GlobalTimestamp::VT_TIMESTAMPS, timestamps); } #[inline] pub fn new(_fbb: &'b mut flatbuffers::FlatBufferBuilder<'a>) -> GlobalTimestampBuilder<'a, 'b> { let start = _fbb.start_table(); GlobalTimestampBuilder { fbb_: _fbb, start_: start, } } #[inline] pub fn finish(self) -> flatbuffers::WIPOffset> { let o = self.fbb_.end_table(self.start_); flatbuffers::WIPOffset::new(o.value()) } } pub mod buffer { #![allow(dead_code)] #![allow(unused_imports)] use std::mem; use std::cmp::Ordering; extern crate flatbuffers; use self::flatbuffers::EndianScalar; #[allow(non_camel_case_types)] #[repr(i8)] #[derive(Clone, Copy, PartialEq, Debug)] pub enum AnchorVariant { Start = 0, Middle = 1, End = 2, } const ENUM_MIN_ANCHOR_VARIANT: i8 = 0; const ENUM_MAX_ANCHOR_VARIANT: i8 = 2; impl<'a> flatbuffers::Follow<'a> for AnchorVariant { type Inner = Self; #[inline] fn follow(buf: &'a [u8], loc: usize) -> Self::Inner { flatbuffers::read_scalar_at::(buf, loc) } } impl flatbuffers::EndianScalar for AnchorVariant { #[inline] fn to_little_endian(self) -> Self { let n = i8::to_le(self as i8); let p = &n as *const i8 as *const AnchorVariant; unsafe { *p } } #[inline] fn from_little_endian(self) -> Self { let n = i8::from_le(self as i8); let p = &n as *const i8 as *const AnchorVariant; unsafe { *p } } } impl flatbuffers::Push for AnchorVariant { type Output = AnchorVariant; #[inline] fn push(&self, dst: &mut [u8], _rest: &[u8]) { flatbuffers::emplace_scalar::(dst, *self); } } #[allow(non_camel_case_types)] const ENUM_VALUES_ANCHOR_VARIANT:[AnchorVariant; 3] = [ AnchorVariant::Start, AnchorVariant::Middle, AnchorVariant::End ]; #[allow(non_camel_case_types)] const ENUM_NAMES_ANCHOR_VARIANT:[&'static str; 3] = [ "Start", "Middle", "End" ]; pub fn enum_name_anchor_variant(e: AnchorVariant) -> &'static str { let index: usize = e as usize; ENUM_NAMES_ANCHOR_VARIANT[index] } #[allow(non_camel_case_types)] #[repr(i8)] #[derive(Clone, Copy, PartialEq, Debug)] pub enum AnchorBias { Left = 0, Right = 1, } const ENUM_MIN_ANCHOR_BIAS: i8 = 0; const ENUM_MAX_ANCHOR_BIAS: i8 = 1; impl<'a> flatbuffers::Follow<'a> for AnchorBias { type Inner = Self; #[inline] fn follow(buf: &'a [u8], loc: usize) -> Self::Inner { flatbuffers::read_scalar_at::(buf, loc) } } impl flatbuffers::EndianScalar for AnchorBias { #[inline] fn to_little_endian(self) -> Self { let n = i8::to_le(self as i8); let p = &n as *const i8 as *const AnchorBias; unsafe { *p } } #[inline] fn from_little_endian(self) -> Self { let n = i8::from_le(self as i8); let p = &n as *const i8 as *const AnchorBias; unsafe { *p } } } impl flatbuffers::Push for AnchorBias { type Output = AnchorBias; #[inline] fn push(&self, dst: &mut [u8], _rest: &[u8]) { flatbuffers::emplace_scalar::(dst, *self); } } #[allow(non_camel_case_types)] const ENUM_VALUES_ANCHOR_BIAS:[AnchorBias; 2] = [ AnchorBias::Left, AnchorBias::Right ]; #[allow(non_camel_case_types)] const ENUM_NAMES_ANCHOR_BIAS:[&'static str; 2] = [ "Left", "Right" ]; pub fn enum_name_anchor_bias(e: AnchorBias) -> &'static str { let index: usize = e as usize; ENUM_NAMES_ANCHOR_BIAS[index] } #[allow(non_camel_case_types)] #[repr(u8)] #[derive(Clone, Copy, PartialEq, Debug)] pub enum OperationVariant { NONE = 0, Edit = 1, UpdateSelections = 2, } const ENUM_MIN_OPERATION_VARIANT: u8 = 0; const ENUM_MAX_OPERATION_VARIANT: u8 = 2; impl<'a> flatbuffers::Follow<'a> for OperationVariant { type Inner = Self; #[inline] fn follow(buf: &'a [u8], loc: usize) -> Self::Inner { flatbuffers::read_scalar_at::(buf, loc) } } impl flatbuffers::EndianScalar for OperationVariant { #[inline] fn to_little_endian(self) -> Self { let n = u8::to_le(self as u8); let p = &n as *const u8 as *const OperationVariant; unsafe { *p } } #[inline] fn from_little_endian(self) -> Self { let n = u8::from_le(self as u8); let p = &n as *const u8 as *const OperationVariant; unsafe { *p } } } impl flatbuffers::Push for OperationVariant { type Output = OperationVariant; #[inline] fn push(&self, dst: &mut [u8], _rest: &[u8]) { flatbuffers::emplace_scalar::(dst, *self); } } #[allow(non_camel_case_types)] const ENUM_VALUES_OPERATION_VARIANT:[OperationVariant; 3] = [ OperationVariant::NONE, OperationVariant::Edit, OperationVariant::UpdateSelections ]; #[allow(non_camel_case_types)] const ENUM_NAMES_OPERATION_VARIANT:[&'static str; 3] = [ "NONE", "Edit", "UpdateSelections" ]; pub fn enum_name_operation_variant(e: OperationVariant) -> &'static str { let index: usize = e as usize; ENUM_NAMES_OPERATION_VARIANT[index] } pub struct OperationVariantUnionTableOffset {} pub enum AnchorOffset {} #[derive(Copy, Clone, Debug, PartialEq)] pub struct Anchor<'a> { pub _tab: flatbuffers::Table<'a>, } impl<'a> flatbuffers::Follow<'a> for Anchor<'a> { type Inner = Anchor<'a>; #[inline] fn follow(buf: &'a [u8], loc: usize) -> Self::Inner { Self { _tab: flatbuffers::Table { buf: buf, loc: loc }, } } } impl<'a> Anchor<'a> { #[inline] pub fn init_from_table(table: flatbuffers::Table<'a>) -> Self { Anchor { _tab: table, } } #[allow(unused_mut)] pub fn create<'bldr: 'args, 'args: 'mut_bldr, 'mut_bldr>( _fbb: &'mut_bldr mut flatbuffers::FlatBufferBuilder<'bldr>, args: &'args AnchorArgs<'args>) -> flatbuffers::WIPOffset> { let mut builder = AnchorBuilder::new(_fbb); builder.add_offset(args.offset); if let Some(x) = args.insertion_id { builder.add_insertion_id(x); } builder.add_bias(args.bias); builder.add_variant(args.variant); builder.finish() } pub const VT_VARIANT: flatbuffers::VOffsetT = 4; pub const VT_INSERTION_ID: flatbuffers::VOffsetT = 6; pub const VT_OFFSET: flatbuffers::VOffsetT = 8; pub const VT_BIAS: flatbuffers::VOffsetT = 10; #[inline] pub fn variant(&self) -> AnchorVariant { self._tab.get::(Anchor::VT_VARIANT, Some(AnchorVariant::Start)).unwrap() } #[inline] pub fn insertion_id(&self) -> Option<&'a super::Timestamp> { self._tab.get::(Anchor::VT_INSERTION_ID, None) } #[inline] pub fn offset(&self) -> u64 { self._tab.get::(Anchor::VT_OFFSET, Some(0)).unwrap() } #[inline] pub fn bias(&self) -> AnchorBias { self._tab.get::(Anchor::VT_BIAS, Some(AnchorBias::Left)).unwrap() } } pub struct AnchorArgs<'a> { pub variant: AnchorVariant, pub insertion_id: Option<&'a super::Timestamp>, pub offset: u64, pub bias: AnchorBias, } impl<'a> Default for AnchorArgs<'a> { #[inline] fn default() -> Self { AnchorArgs { variant: AnchorVariant::Start, insertion_id: None, offset: 0, bias: AnchorBias::Left, } } } pub struct AnchorBuilder<'a: 'b, 'b> { fbb_: &'b mut flatbuffers::FlatBufferBuilder<'a>, start_: flatbuffers::WIPOffset, } impl<'a: 'b, 'b> AnchorBuilder<'a, 'b> { #[inline] pub fn add_variant(&mut self, variant: AnchorVariant) { self.fbb_.push_slot::(Anchor::VT_VARIANT, variant, AnchorVariant::Start); } #[inline] pub fn add_insertion_id(&mut self, insertion_id: &'b super::Timestamp) { self.fbb_.push_slot_always::<&super::Timestamp>(Anchor::VT_INSERTION_ID, insertion_id); } #[inline] pub fn add_offset(&mut self, offset: u64) { self.fbb_.push_slot::(Anchor::VT_OFFSET, offset, 0); } #[inline] pub fn add_bias(&mut self, bias: AnchorBias) { self.fbb_.push_slot::(Anchor::VT_BIAS, bias, AnchorBias::Left); } #[inline] pub fn new(_fbb: &'b mut flatbuffers::FlatBufferBuilder<'a>) -> AnchorBuilder<'a, 'b> { let start = _fbb.start_table(); AnchorBuilder { fbb_: _fbb, start_: start, } } #[inline] pub fn finish(self) -> flatbuffers::WIPOffset> { let o = self.fbb_.end_table(self.start_); flatbuffers::WIPOffset::new(o.value()) } } pub enum SelectionOffset {} #[derive(Copy, Clone, Debug, PartialEq)] pub struct Selection<'a> { pub _tab: flatbuffers::Table<'a>, } impl<'a> flatbuffers::Follow<'a> for Selection<'a> { type Inner = Selection<'a>; #[inline] fn follow(buf: &'a [u8], loc: usize) -> Self::Inner { Self { _tab: flatbuffers::Table { buf: buf, loc: loc }, } } } impl<'a> Selection<'a> { #[inline] pub fn init_from_table(table: flatbuffers::Table<'a>) -> Self { Selection { _tab: table, } } #[allow(unused_mut)] pub fn create<'bldr: 'args, 'args: 'mut_bldr, 'mut_bldr>( _fbb: &'mut_bldr mut flatbuffers::FlatBufferBuilder<'bldr>, args: &'args SelectionArgs<'args>) -> flatbuffers::WIPOffset> { let mut builder = SelectionBuilder::new(_fbb); if let Some(x) = args.end { builder.add_end(x); } if let Some(x) = args.start { builder.add_start(x); } builder.add_reversed(args.reversed); builder.finish() } pub const VT_START: flatbuffers::VOffsetT = 4; pub const VT_END: flatbuffers::VOffsetT = 6; pub const VT_REVERSED: flatbuffers::VOffsetT = 8; #[inline] pub fn start(&self) -> Option> { self._tab.get::>>(Selection::VT_START, None) } #[inline] pub fn end(&self) -> Option> { self._tab.get::>>(Selection::VT_END, None) } #[inline] pub fn reversed(&self) -> bool { self._tab.get::(Selection::VT_REVERSED, Some(false)).unwrap() } } pub struct SelectionArgs<'a> { pub start: Option>>, pub end: Option>>, pub reversed: bool, } impl<'a> Default for SelectionArgs<'a> { #[inline] fn default() -> Self { SelectionArgs { start: None, end: None, reversed: false, } } } pub struct SelectionBuilder<'a: 'b, 'b> { fbb_: &'b mut flatbuffers::FlatBufferBuilder<'a>, start_: flatbuffers::WIPOffset, } impl<'a: 'b, 'b> SelectionBuilder<'a, 'b> { #[inline] pub fn add_start(&mut self, start: flatbuffers::WIPOffset>) { self.fbb_.push_slot_always::>(Selection::VT_START, start); } #[inline] pub fn add_end(&mut self, end: flatbuffers::WIPOffset>) { self.fbb_.push_slot_always::>(Selection::VT_END, end); } #[inline] pub fn add_reversed(&mut self, reversed: bool) { self.fbb_.push_slot::(Selection::VT_REVERSED, reversed, false); } #[inline] pub fn new(_fbb: &'b mut flatbuffers::FlatBufferBuilder<'a>) -> SelectionBuilder<'a, 'b> { let start = _fbb.start_table(); SelectionBuilder { fbb_: _fbb, start_: start, } } #[inline] pub fn finish(self) -> flatbuffers::WIPOffset> { let o = self.fbb_.end_table(self.start_); flatbuffers::WIPOffset::new(o.value()) } } pub enum EditOffset {} #[derive(Copy, Clone, Debug, PartialEq)] pub struct Edit<'a> { pub _tab: flatbuffers::Table<'a>, } impl<'a> flatbuffers::Follow<'a> for Edit<'a> { type Inner = Edit<'a>; #[inline] fn follow(buf: &'a [u8], loc: usize) -> Self::Inner { Self { _tab: flatbuffers::Table { buf: buf, loc: loc }, } } } impl<'a> Edit<'a> { #[inline] pub fn init_from_table(table: flatbuffers::Table<'a>) -> Self { Edit { _tab: table, } } #[allow(unused_mut)] pub fn create<'bldr: 'args, 'args: 'mut_bldr, 'mut_bldr>( _fbb: &'mut_bldr mut flatbuffers::FlatBufferBuilder<'bldr>, args: &'args EditArgs<'args>) -> flatbuffers::WIPOffset> { let mut builder = EditBuilder::new(_fbb); builder.add_end_offset(args.end_offset); builder.add_start_offset(args.start_offset); if let Some(x) = args.lamport_timestamp { builder.add_lamport_timestamp(x); } if let Some(x) = args.local_timestamp { builder.add_local_timestamp(x); } if let Some(x) = args.new_text { builder.add_new_text(x); } if let Some(x) = args.version_in_range { builder.add_version_in_range(x); } if let Some(x) = args.end_id { builder.add_end_id(x); } if let Some(x) = args.start_id { builder.add_start_id(x); } builder.finish() } pub const VT_START_ID: flatbuffers::VOffsetT = 4; pub const VT_START_OFFSET: flatbuffers::VOffsetT = 6; pub const VT_END_ID: flatbuffers::VOffsetT = 8; pub const VT_END_OFFSET: flatbuffers::VOffsetT = 10; pub const VT_VERSION_IN_RANGE: flatbuffers::VOffsetT = 12; pub const VT_NEW_TEXT: flatbuffers::VOffsetT = 14; pub const VT_LOCAL_TIMESTAMP: flatbuffers::VOffsetT = 16; pub const VT_LAMPORT_TIMESTAMP: flatbuffers::VOffsetT = 18; #[inline] pub fn start_id(&self) -> Option<&'a super::Timestamp> { self._tab.get::(Edit::VT_START_ID, None) } #[inline] pub fn start_offset(&self) -> u64 { self._tab.get::(Edit::VT_START_OFFSET, Some(0)).unwrap() } #[inline] pub fn end_id(&self) -> Option<&'a super::Timestamp> { self._tab.get::(Edit::VT_END_ID, None) } #[inline] pub fn end_offset(&self) -> u64 { self._tab.get::(Edit::VT_END_OFFSET, Some(0)).unwrap() } #[inline] pub fn version_in_range(&self) -> Option> { self._tab.get::>>(Edit::VT_VERSION_IN_RANGE, None) } #[inline] pub fn new_text(&self) -> Option<&'a str> { self._tab.get::>(Edit::VT_NEW_TEXT, None) } #[inline] pub fn local_timestamp(&self) -> Option<&'a super::Timestamp> { self._tab.get::(Edit::VT_LOCAL_TIMESTAMP, None) } #[inline] pub fn lamport_timestamp(&self) -> Option<&'a super::Timestamp> { self._tab.get::(Edit::VT_LAMPORT_TIMESTAMP, None) } } pub struct EditArgs<'a> { pub start_id: Option<&'a super::Timestamp>, pub start_offset: u64, pub end_id: Option<&'a super::Timestamp>, pub end_offset: u64, pub version_in_range: Option>>, pub new_text: Option>, pub local_timestamp: Option<&'a super::Timestamp>, pub lamport_timestamp: Option<&'a super::Timestamp>, } impl<'a> Default for EditArgs<'a> { #[inline] fn default() -> Self { EditArgs { start_id: None, start_offset: 0, end_id: None, end_offset: 0, version_in_range: None, new_text: None, local_timestamp: None, lamport_timestamp: None, } } } pub struct EditBuilder<'a: 'b, 'b> { fbb_: &'b mut flatbuffers::FlatBufferBuilder<'a>, start_: flatbuffers::WIPOffset, } impl<'a: 'b, 'b> EditBuilder<'a, 'b> { #[inline] pub fn add_start_id(&mut self, start_id: &'b super::Timestamp) { self.fbb_.push_slot_always::<&super::Timestamp>(Edit::VT_START_ID, start_id); } #[inline] pub fn add_start_offset(&mut self, start_offset: u64) { self.fbb_.push_slot::(Edit::VT_START_OFFSET, start_offset, 0); } #[inline] pub fn add_end_id(&mut self, end_id: &'b super::Timestamp) { self.fbb_.push_slot_always::<&super::Timestamp>(Edit::VT_END_ID, end_id); } #[inline] pub fn add_end_offset(&mut self, end_offset: u64) { self.fbb_.push_slot::(Edit::VT_END_OFFSET, end_offset, 0); } #[inline] pub fn add_version_in_range(&mut self, version_in_range: flatbuffers::WIPOffset>) { self.fbb_.push_slot_always::>(Edit::VT_VERSION_IN_RANGE, version_in_range); } #[inline] pub fn add_new_text(&mut self, new_text: flatbuffers::WIPOffset<&'b str>) { self.fbb_.push_slot_always::>(Edit::VT_NEW_TEXT, new_text); } #[inline] pub fn add_local_timestamp(&mut self, local_timestamp: &'b super::Timestamp) { self.fbb_.push_slot_always::<&super::Timestamp>(Edit::VT_LOCAL_TIMESTAMP, local_timestamp); } #[inline] pub fn add_lamport_timestamp(&mut self, lamport_timestamp: &'b super::Timestamp) { self.fbb_.push_slot_always::<&super::Timestamp>(Edit::VT_LAMPORT_TIMESTAMP, lamport_timestamp); } #[inline] pub fn new(_fbb: &'b mut flatbuffers::FlatBufferBuilder<'a>) -> EditBuilder<'a, 'b> { let start = _fbb.start_table(); EditBuilder { fbb_: _fbb, start_: start, } } #[inline] pub fn finish(self) -> flatbuffers::WIPOffset> { let o = self.fbb_.end_table(self.start_); flatbuffers::WIPOffset::new(o.value()) } } pub enum UpdateSelectionsOffset {} #[derive(Copy, Clone, Debug, PartialEq)] pub struct UpdateSelections<'a> { pub _tab: flatbuffers::Table<'a>, } impl<'a> flatbuffers::Follow<'a> for UpdateSelections<'a> { type Inner = UpdateSelections<'a>; #[inline] fn follow(buf: &'a [u8], loc: usize) -> Self::Inner { Self { _tab: flatbuffers::Table { buf: buf, loc: loc }, } } } impl<'a> UpdateSelections<'a> { #[inline] pub fn init_from_table(table: flatbuffers::Table<'a>) -> Self { UpdateSelections { _tab: table, } } #[allow(unused_mut)] pub fn create<'bldr: 'args, 'args: 'mut_bldr, 'mut_bldr>( _fbb: &'mut_bldr mut flatbuffers::FlatBufferBuilder<'bldr>, args: &'args UpdateSelectionsArgs<'args>) -> flatbuffers::WIPOffset> { let mut builder = UpdateSelectionsBuilder::new(_fbb); if let Some(x) = args.lamport_timestamp { builder.add_lamport_timestamp(x); } if let Some(x) = args.selections { builder.add_selections(x); } if let Some(x) = args.set_id { builder.add_set_id(x); } builder.finish() } pub const VT_SET_ID: flatbuffers::VOffsetT = 4; pub const VT_SELECTIONS: flatbuffers::VOffsetT = 6; pub const VT_LAMPORT_TIMESTAMP: flatbuffers::VOffsetT = 8; #[inline] pub fn set_id(&self) -> Option<&'a super::Timestamp> { self._tab.get::(UpdateSelections::VT_SET_ID, None) } #[inline] pub fn selections(&self) -> Option>>> { self._tab.get::>>>>(UpdateSelections::VT_SELECTIONS, None) } #[inline] pub fn lamport_timestamp(&self) -> Option<&'a super::Timestamp> { self._tab.get::(UpdateSelections::VT_LAMPORT_TIMESTAMP, None) } } pub struct UpdateSelectionsArgs<'a> { pub set_id: Option<&'a super::Timestamp>, pub selections: Option>>>>, pub lamport_timestamp: Option<&'a super::Timestamp>, } impl<'a> Default for UpdateSelectionsArgs<'a> { #[inline] fn default() -> Self { UpdateSelectionsArgs { set_id: None, selections: None, lamport_timestamp: None, } } } pub struct UpdateSelectionsBuilder<'a: 'b, 'b> { fbb_: &'b mut flatbuffers::FlatBufferBuilder<'a>, start_: flatbuffers::WIPOffset, } impl<'a: 'b, 'b> UpdateSelectionsBuilder<'a, 'b> { #[inline] pub fn add_set_id(&mut self, set_id: &'b super::Timestamp) { self.fbb_.push_slot_always::<&super::Timestamp>(UpdateSelections::VT_SET_ID, set_id); } #[inline] pub fn add_selections(&mut self, selections: flatbuffers::WIPOffset>>>) { self.fbb_.push_slot_always::>(UpdateSelections::VT_SELECTIONS, selections); } #[inline] pub fn add_lamport_timestamp(&mut self, lamport_timestamp: &'b super::Timestamp) { self.fbb_.push_slot_always::<&super::Timestamp>(UpdateSelections::VT_LAMPORT_TIMESTAMP, lamport_timestamp); } #[inline] pub fn new(_fbb: &'b mut flatbuffers::FlatBufferBuilder<'a>) -> UpdateSelectionsBuilder<'a, 'b> { let start = _fbb.start_table(); UpdateSelectionsBuilder { fbb_: _fbb, start_: start, } } #[inline] pub fn finish(self) -> flatbuffers::WIPOffset> { let o = self.fbb_.end_table(self.start_); flatbuffers::WIPOffset::new(o.value()) } } pub enum OperationOffset {} #[derive(Copy, Clone, Debug, PartialEq)] pub struct Operation<'a> { pub _tab: flatbuffers::Table<'a>, } impl<'a> flatbuffers::Follow<'a> for Operation<'a> { type Inner = Operation<'a>; #[inline] fn follow(buf: &'a [u8], loc: usize) -> Self::Inner { Self { _tab: flatbuffers::Table { buf: buf, loc: loc }, } } } impl<'a> Operation<'a> { #[inline] pub fn init_from_table(table: flatbuffers::Table<'a>) -> Self { Operation { _tab: table, } } #[allow(unused_mut)] pub fn create<'bldr: 'args, 'args: 'mut_bldr, 'mut_bldr>( _fbb: &'mut_bldr mut flatbuffers::FlatBufferBuilder<'bldr>, args: &'args OperationArgs) -> flatbuffers::WIPOffset> { let mut builder = OperationBuilder::new(_fbb); if let Some(x) = args.variant { builder.add_variant(x); } builder.add_variant_type(args.variant_type); builder.finish() } pub const VT_VARIANT_TYPE: flatbuffers::VOffsetT = 4; pub const VT_VARIANT: flatbuffers::VOffsetT = 6; #[inline] pub fn variant_type(&self) -> OperationVariant { self._tab.get::(Operation::VT_VARIANT_TYPE, Some(OperationVariant::NONE)).unwrap() } #[inline] pub fn variant(&self) -> Option> { self._tab.get::>>(Operation::VT_VARIANT, None) } #[inline] #[allow(non_snake_case)] pub fn variant_as_edit(&'a self) -> Option { if self.variant_type() == OperationVariant::Edit { self.variant().map(|u| Edit::init_from_table(u)) } else { None } } #[inline] #[allow(non_snake_case)] pub fn variant_as_update_selections(&'a self) -> Option { if self.variant_type() == OperationVariant::UpdateSelections { self.variant().map(|u| UpdateSelections::init_from_table(u)) } else { None } } } pub struct OperationArgs { pub variant_type: OperationVariant, pub variant: Option>, } impl<'a> Default for OperationArgs { #[inline] fn default() -> Self { OperationArgs { variant_type: OperationVariant::NONE, variant: None, } } } pub struct OperationBuilder<'a: 'b, 'b> { fbb_: &'b mut flatbuffers::FlatBufferBuilder<'a>, start_: flatbuffers::WIPOffset, } impl<'a: 'b, 'b> OperationBuilder<'a, 'b> { #[inline] pub fn add_variant_type(&mut self, variant_type: OperationVariant) { self.fbb_.push_slot::(Operation::VT_VARIANT_TYPE, variant_type, OperationVariant::NONE); } #[inline] pub fn add_variant(&mut self, variant: flatbuffers::WIPOffset) { self.fbb_.push_slot_always::>(Operation::VT_VARIANT, variant); } #[inline] pub fn new(_fbb: &'b mut flatbuffers::FlatBufferBuilder<'a>) -> OperationBuilder<'a, 'b> { let start = _fbb.start_table(); OperationBuilder { fbb_: _fbb, start_: start, } } #[inline] pub fn finish(self) -> flatbuffers::WIPOffset> { let o = self.fbb_.end_table(self.start_); flatbuffers::WIPOffset::new(o.value()) } } } // pub mod buffer pub mod epoch { #![allow(dead_code)] #![allow(unused_imports)] use std::mem; use std::cmp::Ordering; extern crate flatbuffers; use self::flatbuffers::EndianScalar; #[allow(non_camel_case_types)] #[repr(u8)] #[derive(Clone, Copy, PartialEq, Debug)] pub enum FileId { NONE = 0, BaseFileId = 1, NewFileId = 2, } const ENUM_MIN_FILE_ID: u8 = 0; const ENUM_MAX_FILE_ID: u8 = 2; impl<'a> flatbuffers::Follow<'a> for FileId { type Inner = Self; #[inline] fn follow(buf: &'a [u8], loc: usize) -> Self::Inner { flatbuffers::read_scalar_at::(buf, loc) } } impl flatbuffers::EndianScalar for FileId { #[inline] fn to_little_endian(self) -> Self { let n = u8::to_le(self as u8); let p = &n as *const u8 as *const FileId; unsafe { *p } } #[inline] fn from_little_endian(self) -> Self { let n = u8::from_le(self as u8); let p = &n as *const u8 as *const FileId; unsafe { *p } } } impl flatbuffers::Push for FileId { type Output = FileId; #[inline] fn push(&self, dst: &mut [u8], _rest: &[u8]) { flatbuffers::emplace_scalar::(dst, *self); } } #[allow(non_camel_case_types)] const ENUM_VALUES_FILE_ID:[FileId; 3] = [ FileId::NONE, FileId::BaseFileId, FileId::NewFileId ]; #[allow(non_camel_case_types)] const ENUM_NAMES_FILE_ID:[&'static str; 3] = [ "NONE", "BaseFileId", "NewFileId" ]; pub fn enum_name_file_id(e: FileId) -> &'static str { let index: usize = e as usize; ENUM_NAMES_FILE_ID[index] } pub struct FileIdUnionTableOffset {} #[allow(non_camel_case_types)] #[repr(i8)] #[derive(Clone, Copy, PartialEq, Debug)] pub enum FileType { Directory = 0, Text = 1, } const ENUM_MIN_FILE_TYPE: i8 = 0; const ENUM_MAX_FILE_TYPE: i8 = 1; impl<'a> flatbuffers::Follow<'a> for FileType { type Inner = Self; #[inline] fn follow(buf: &'a [u8], loc: usize) -> Self::Inner { flatbuffers::read_scalar_at::(buf, loc) } } impl flatbuffers::EndianScalar for FileType { #[inline] fn to_little_endian(self) -> Self { let n = i8::to_le(self as i8); let p = &n as *const i8 as *const FileType; unsafe { *p } } #[inline] fn from_little_endian(self) -> Self { let n = i8::from_le(self as i8); let p = &n as *const i8 as *const FileType; unsafe { *p } } } impl flatbuffers::Push for FileType { type Output = FileType; #[inline] fn push(&self, dst: &mut [u8], _rest: &[u8]) { flatbuffers::emplace_scalar::(dst, *self); } } #[allow(non_camel_case_types)] const ENUM_VALUES_FILE_TYPE:[FileType; 2] = [ FileType::Directory, FileType::Text ]; #[allow(non_camel_case_types)] const ENUM_NAMES_FILE_TYPE:[&'static str; 2] = [ "Directory", "Text" ]; pub fn enum_name_file_type(e: FileType) -> &'static str { let index: usize = e as usize; ENUM_NAMES_FILE_TYPE[index] } #[allow(non_camel_case_types)] #[repr(u8)] #[derive(Clone, Copy, PartialEq, Debug)] pub enum Operation { NONE = 0, InsertMetadata = 1, UpdateParent = 2, BufferOperation = 3, UpdateActiveLocation = 4, } const ENUM_MIN_OPERATION: u8 = 0; const ENUM_MAX_OPERATION: u8 = 4; impl<'a> flatbuffers::Follow<'a> for Operation { type Inner = Self; #[inline] fn follow(buf: &'a [u8], loc: usize) -> Self::Inner { flatbuffers::read_scalar_at::(buf, loc) } } impl flatbuffers::EndianScalar for Operation { #[inline] fn to_little_endian(self) -> Self { let n = u8::to_le(self as u8); let p = &n as *const u8 as *const Operation; unsafe { *p } } #[inline] fn from_little_endian(self) -> Self { let n = u8::from_le(self as u8); let p = &n as *const u8 as *const Operation; unsafe { *p } } } impl flatbuffers::Push for Operation { type Output = Operation; #[inline] fn push(&self, dst: &mut [u8], _rest: &[u8]) { flatbuffers::emplace_scalar::(dst, *self); } } #[allow(non_camel_case_types)] const ENUM_VALUES_OPERATION:[Operation; 5] = [ Operation::NONE, Operation::InsertMetadata, Operation::UpdateParent, Operation::BufferOperation, Operation::UpdateActiveLocation ]; #[allow(non_camel_case_types)] const ENUM_NAMES_OPERATION:[&'static str; 5] = [ "NONE", "InsertMetadata", "UpdateParent", "BufferOperation", "UpdateActiveLocation" ]; pub fn enum_name_operation(e: Operation) -> &'static str { let index: usize = e as usize; ENUM_NAMES_OPERATION[index] } pub struct OperationUnionTableOffset {} pub enum BaseFileIdOffset {} #[derive(Copy, Clone, Debug, PartialEq)] pub struct BaseFileId<'a> { pub _tab: flatbuffers::Table<'a>, } impl<'a> flatbuffers::Follow<'a> for BaseFileId<'a> { type Inner = BaseFileId<'a>; #[inline] fn follow(buf: &'a [u8], loc: usize) -> Self::Inner { Self { _tab: flatbuffers::Table { buf: buf, loc: loc }, } } } impl<'a> BaseFileId<'a> { #[inline] pub fn init_from_table(table: flatbuffers::Table<'a>) -> Self { BaseFileId { _tab: table, } } #[allow(unused_mut)] pub fn create<'bldr: 'args, 'args: 'mut_bldr, 'mut_bldr>( _fbb: &'mut_bldr mut flatbuffers::FlatBufferBuilder<'bldr>, args: &'args BaseFileIdArgs) -> flatbuffers::WIPOffset> { let mut builder = BaseFileIdBuilder::new(_fbb); builder.add_index(args.index); builder.finish() } pub const VT_INDEX: flatbuffers::VOffsetT = 4; #[inline] pub fn index(&self) -> u64 { self._tab.get::(BaseFileId::VT_INDEX, Some(0)).unwrap() } } pub struct BaseFileIdArgs { pub index: u64, } impl<'a> Default for BaseFileIdArgs { #[inline] fn default() -> Self { BaseFileIdArgs { index: 0, } } } pub struct BaseFileIdBuilder<'a: 'b, 'b> { fbb_: &'b mut flatbuffers::FlatBufferBuilder<'a>, start_: flatbuffers::WIPOffset, } impl<'a: 'b, 'b> BaseFileIdBuilder<'a, 'b> { #[inline] pub fn add_index(&mut self, index: u64) { self.fbb_.push_slot::(BaseFileId::VT_INDEX, index, 0); } #[inline] pub fn new(_fbb: &'b mut flatbuffers::FlatBufferBuilder<'a>) -> BaseFileIdBuilder<'a, 'b> { let start = _fbb.start_table(); BaseFileIdBuilder { fbb_: _fbb, start_: start, } } #[inline] pub fn finish(self) -> flatbuffers::WIPOffset> { let o = self.fbb_.end_table(self.start_); flatbuffers::WIPOffset::new(o.value()) } } pub enum NewFileIdOffset {} #[derive(Copy, Clone, Debug, PartialEq)] pub struct NewFileId<'a> { pub _tab: flatbuffers::Table<'a>, } impl<'a> flatbuffers::Follow<'a> for NewFileId<'a> { type Inner = NewFileId<'a>; #[inline] fn follow(buf: &'a [u8], loc: usize) -> Self::Inner { Self { _tab: flatbuffers::Table { buf: buf, loc: loc }, } } } impl<'a> NewFileId<'a> { #[inline] pub fn init_from_table(table: flatbuffers::Table<'a>) -> Self { NewFileId { _tab: table, } } #[allow(unused_mut)] pub fn create<'bldr: 'args, 'args: 'mut_bldr, 'mut_bldr>( _fbb: &'mut_bldr mut flatbuffers::FlatBufferBuilder<'bldr>, args: &'args NewFileIdArgs<'args>) -> flatbuffers::WIPOffset> { let mut builder = NewFileIdBuilder::new(_fbb); if let Some(x) = args.id { builder.add_id(x); } builder.finish() } pub const VT_ID: flatbuffers::VOffsetT = 4; #[inline] pub fn id(&self) -> Option<&'a super::Timestamp> { self._tab.get::(NewFileId::VT_ID, None) } } pub struct NewFileIdArgs<'a> { pub id: Option<&'a super::Timestamp>, } impl<'a> Default for NewFileIdArgs<'a> { #[inline] fn default() -> Self { NewFileIdArgs { id: None, } } } pub struct NewFileIdBuilder<'a: 'b, 'b> { fbb_: &'b mut flatbuffers::FlatBufferBuilder<'a>, start_: flatbuffers::WIPOffset, } impl<'a: 'b, 'b> NewFileIdBuilder<'a, 'b> { #[inline] pub fn add_id(&mut self, id: &'b super::Timestamp) { self.fbb_.push_slot_always::<&super::Timestamp>(NewFileId::VT_ID, id); } #[inline] pub fn new(_fbb: &'b mut flatbuffers::FlatBufferBuilder<'a>) -> NewFileIdBuilder<'a, 'b> { let start = _fbb.start_table(); NewFileIdBuilder { fbb_: _fbb, start_: start, } } #[inline] pub fn finish(self) -> flatbuffers::WIPOffset> { let o = self.fbb_.end_table(self.start_); flatbuffers::WIPOffset::new(o.value()) } } pub enum InsertMetadataOffset {} #[derive(Copy, Clone, Debug, PartialEq)] pub struct InsertMetadata<'a> { pub _tab: flatbuffers::Table<'a>, } impl<'a> flatbuffers::Follow<'a> for InsertMetadata<'a> { type Inner = InsertMetadata<'a>; #[inline] fn follow(buf: &'a [u8], loc: usize) -> Self::Inner { Self { _tab: flatbuffers::Table { buf: buf, loc: loc }, } } } impl<'a> InsertMetadata<'a> { #[inline] pub fn init_from_table(table: flatbuffers::Table<'a>) -> Self { InsertMetadata { _tab: table, } } #[allow(unused_mut)] pub fn create<'bldr: 'args, 'args: 'mut_bldr, 'mut_bldr>( _fbb: &'mut_bldr mut flatbuffers::FlatBufferBuilder<'bldr>, args: &'args InsertMetadataArgs<'args>) -> flatbuffers::WIPOffset> { let mut builder = InsertMetadataBuilder::new(_fbb); if let Some(x) = args.lamport_timestamp { builder.add_lamport_timestamp(x); } if let Some(x) = args.local_timestamp { builder.add_local_timestamp(x); } if let Some(x) = args.name_in_parent { builder.add_name_in_parent(x); } if let Some(x) = args.parent_id { builder.add_parent_id(x); } if let Some(x) = args.file_id { builder.add_file_id(x); } builder.add_parent_id_type(args.parent_id_type); builder.add_file_type(args.file_type); builder.add_file_id_type(args.file_id_type); builder.finish() } pub const VT_FILE_ID_TYPE: flatbuffers::VOffsetT = 4; pub const VT_FILE_ID: flatbuffers::VOffsetT = 6; pub const VT_FILE_TYPE: flatbuffers::VOffsetT = 8; pub const VT_PARENT_ID_TYPE: flatbuffers::VOffsetT = 10; pub const VT_PARENT_ID: flatbuffers::VOffsetT = 12; pub const VT_NAME_IN_PARENT: flatbuffers::VOffsetT = 14; pub const VT_LOCAL_TIMESTAMP: flatbuffers::VOffsetT = 16; pub const VT_LAMPORT_TIMESTAMP: flatbuffers::VOffsetT = 18; #[inline] pub fn file_id_type(&self) -> FileId { self._tab.get::(InsertMetadata::VT_FILE_ID_TYPE, Some(FileId::NONE)).unwrap() } #[inline] pub fn file_id(&self) -> Option> { self._tab.get::>>(InsertMetadata::VT_FILE_ID, None) } #[inline] pub fn file_type(&self) -> FileType { self._tab.get::(InsertMetadata::VT_FILE_TYPE, Some(FileType::Directory)).unwrap() } #[inline] pub fn parent_id_type(&self) -> FileId { self._tab.get::(InsertMetadata::VT_PARENT_ID_TYPE, Some(FileId::NONE)).unwrap() } #[inline] pub fn parent_id(&self) -> Option> { self._tab.get::>>(InsertMetadata::VT_PARENT_ID, None) } #[inline] pub fn name_in_parent(&self) -> Option<&'a str> { self._tab.get::>(InsertMetadata::VT_NAME_IN_PARENT, None) } #[inline] pub fn local_timestamp(&self) -> Option<&'a super::Timestamp> { self._tab.get::(InsertMetadata::VT_LOCAL_TIMESTAMP, None) } #[inline] pub fn lamport_timestamp(&self) -> Option<&'a super::Timestamp> { self._tab.get::(InsertMetadata::VT_LAMPORT_TIMESTAMP, None) } #[inline] #[allow(non_snake_case)] pub fn file_id_as_base_file_id(&'a self) -> Option { if self.file_id_type() == FileId::BaseFileId { self.file_id().map(|u| BaseFileId::init_from_table(u)) } else { None } } #[inline] #[allow(non_snake_case)] pub fn file_id_as_new_file_id(&'a self) -> Option { if self.file_id_type() == FileId::NewFileId { self.file_id().map(|u| NewFileId::init_from_table(u)) } else { None } } #[inline] #[allow(non_snake_case)] pub fn parent_id_as_base_file_id(&'a self) -> Option { if self.parent_id_type() == FileId::BaseFileId { self.parent_id().map(|u| BaseFileId::init_from_table(u)) } else { None } } #[inline] #[allow(non_snake_case)] pub fn parent_id_as_new_file_id(&'a self) -> Option { if self.parent_id_type() == FileId::NewFileId { self.parent_id().map(|u| NewFileId::init_from_table(u)) } else { None } } } pub struct InsertMetadataArgs<'a> { pub file_id_type: FileId, pub file_id: Option>, pub file_type: FileType, pub parent_id_type: FileId, pub parent_id: Option>, pub name_in_parent: Option>, pub local_timestamp: Option<&'a super::Timestamp>, pub lamport_timestamp: Option<&'a super::Timestamp>, } impl<'a> Default for InsertMetadataArgs<'a> { #[inline] fn default() -> Self { InsertMetadataArgs { file_id_type: FileId::NONE, file_id: None, file_type: FileType::Directory, parent_id_type: FileId::NONE, parent_id: None, name_in_parent: None, local_timestamp: None, lamport_timestamp: None, } } } pub struct InsertMetadataBuilder<'a: 'b, 'b> { fbb_: &'b mut flatbuffers::FlatBufferBuilder<'a>, start_: flatbuffers::WIPOffset, } impl<'a: 'b, 'b> InsertMetadataBuilder<'a, 'b> { #[inline] pub fn add_file_id_type(&mut self, file_id_type: FileId) { self.fbb_.push_slot::(InsertMetadata::VT_FILE_ID_TYPE, file_id_type, FileId::NONE); } #[inline] pub fn add_file_id(&mut self, file_id: flatbuffers::WIPOffset) { self.fbb_.push_slot_always::>(InsertMetadata::VT_FILE_ID, file_id); } #[inline] pub fn add_file_type(&mut self, file_type: FileType) { self.fbb_.push_slot::(InsertMetadata::VT_FILE_TYPE, file_type, FileType::Directory); } #[inline] pub fn add_parent_id_type(&mut self, parent_id_type: FileId) { self.fbb_.push_slot::(InsertMetadata::VT_PARENT_ID_TYPE, parent_id_type, FileId::NONE); } #[inline] pub fn add_parent_id(&mut self, parent_id: flatbuffers::WIPOffset) { self.fbb_.push_slot_always::>(InsertMetadata::VT_PARENT_ID, parent_id); } #[inline] pub fn add_name_in_parent(&mut self, name_in_parent: flatbuffers::WIPOffset<&'b str>) { self.fbb_.push_slot_always::>(InsertMetadata::VT_NAME_IN_PARENT, name_in_parent); } #[inline] pub fn add_local_timestamp(&mut self, local_timestamp: &'b super::Timestamp) { self.fbb_.push_slot_always::<&super::Timestamp>(InsertMetadata::VT_LOCAL_TIMESTAMP, local_timestamp); } #[inline] pub fn add_lamport_timestamp(&mut self, lamport_timestamp: &'b super::Timestamp) { self.fbb_.push_slot_always::<&super::Timestamp>(InsertMetadata::VT_LAMPORT_TIMESTAMP, lamport_timestamp); } #[inline] pub fn new(_fbb: &'b mut flatbuffers::FlatBufferBuilder<'a>) -> InsertMetadataBuilder<'a, 'b> { let start = _fbb.start_table(); InsertMetadataBuilder { fbb_: _fbb, start_: start, } } #[inline] pub fn finish(self) -> flatbuffers::WIPOffset> { let o = self.fbb_.end_table(self.start_); flatbuffers::WIPOffset::new(o.value()) } } pub enum UpdateParentOffset {} #[derive(Copy, Clone, Debug, PartialEq)] pub struct UpdateParent<'a> { pub _tab: flatbuffers::Table<'a>, } impl<'a> flatbuffers::Follow<'a> for UpdateParent<'a> { type Inner = UpdateParent<'a>; #[inline] fn follow(buf: &'a [u8], loc: usize) -> Self::Inner { Self { _tab: flatbuffers::Table { buf: buf, loc: loc }, } } } impl<'a> UpdateParent<'a> { #[inline] pub fn init_from_table(table: flatbuffers::Table<'a>) -> Self { UpdateParent { _tab: table, } } #[allow(unused_mut)] pub fn create<'bldr: 'args, 'args: 'mut_bldr, 'mut_bldr>( _fbb: &'mut_bldr mut flatbuffers::FlatBufferBuilder<'bldr>, args: &'args UpdateParentArgs<'args>) -> flatbuffers::WIPOffset> { let mut builder = UpdateParentBuilder::new(_fbb); if let Some(x) = args.lamport_timestamp { builder.add_lamport_timestamp(x); } if let Some(x) = args.local_timestamp { builder.add_local_timestamp(x); } if let Some(x) = args.new_name_in_parent { builder.add_new_name_in_parent(x); } if let Some(x) = args.new_parent_id { builder.add_new_parent_id(x); } if let Some(x) = args.child_id { builder.add_child_id(x); } builder.add_new_parent_id_type(args.new_parent_id_type); builder.add_child_id_type(args.child_id_type); builder.finish() } pub const VT_CHILD_ID_TYPE: flatbuffers::VOffsetT = 4; pub const VT_CHILD_ID: flatbuffers::VOffsetT = 6; pub const VT_NEW_PARENT_ID_TYPE: flatbuffers::VOffsetT = 8; pub const VT_NEW_PARENT_ID: flatbuffers::VOffsetT = 10; pub const VT_NEW_NAME_IN_PARENT: flatbuffers::VOffsetT = 12; pub const VT_LOCAL_TIMESTAMP: flatbuffers::VOffsetT = 14; pub const VT_LAMPORT_TIMESTAMP: flatbuffers::VOffsetT = 16; #[inline] pub fn child_id_type(&self) -> FileId { self._tab.get::(UpdateParent::VT_CHILD_ID_TYPE, Some(FileId::NONE)).unwrap() } #[inline] pub fn child_id(&self) -> Option> { self._tab.get::>>(UpdateParent::VT_CHILD_ID, None) } #[inline] pub fn new_parent_id_type(&self) -> FileId { self._tab.get::(UpdateParent::VT_NEW_PARENT_ID_TYPE, Some(FileId::NONE)).unwrap() } #[inline] pub fn new_parent_id(&self) -> Option> { self._tab.get::>>(UpdateParent::VT_NEW_PARENT_ID, None) } #[inline] pub fn new_name_in_parent(&self) -> Option<&'a str> { self._tab.get::>(UpdateParent::VT_NEW_NAME_IN_PARENT, None) } #[inline] pub fn local_timestamp(&self) -> Option<&'a super::Timestamp> { self._tab.get::(UpdateParent::VT_LOCAL_TIMESTAMP, None) } #[inline] pub fn lamport_timestamp(&self) -> Option<&'a super::Timestamp> { self._tab.get::(UpdateParent::VT_LAMPORT_TIMESTAMP, None) } #[inline] #[allow(non_snake_case)] pub fn child_id_as_base_file_id(&'a self) -> Option { if self.child_id_type() == FileId::BaseFileId { self.child_id().map(|u| BaseFileId::init_from_table(u)) } else { None } } #[inline] #[allow(non_snake_case)] pub fn child_id_as_new_file_id(&'a self) -> Option { if self.child_id_type() == FileId::NewFileId { self.child_id().map(|u| NewFileId::init_from_table(u)) } else { None } } #[inline] #[allow(non_snake_case)] pub fn new_parent_id_as_base_file_id(&'a self) -> Option { if self.new_parent_id_type() == FileId::BaseFileId { self.new_parent_id().map(|u| BaseFileId::init_from_table(u)) } else { None } } #[inline] #[allow(non_snake_case)] pub fn new_parent_id_as_new_file_id(&'a self) -> Option { if self.new_parent_id_type() == FileId::NewFileId { self.new_parent_id().map(|u| NewFileId::init_from_table(u)) } else { None } } } pub struct UpdateParentArgs<'a> { pub child_id_type: FileId, pub child_id: Option>, pub new_parent_id_type: FileId, pub new_parent_id: Option>, pub new_name_in_parent: Option>, pub local_timestamp: Option<&'a super::Timestamp>, pub lamport_timestamp: Option<&'a super::Timestamp>, } impl<'a> Default for UpdateParentArgs<'a> { #[inline] fn default() -> Self { UpdateParentArgs { child_id_type: FileId::NONE, child_id: None, new_parent_id_type: FileId::NONE, new_parent_id: None, new_name_in_parent: None, local_timestamp: None, lamport_timestamp: None, } } } pub struct UpdateParentBuilder<'a: 'b, 'b> { fbb_: &'b mut flatbuffers::FlatBufferBuilder<'a>, start_: flatbuffers::WIPOffset, } impl<'a: 'b, 'b> UpdateParentBuilder<'a, 'b> { #[inline] pub fn add_child_id_type(&mut self, child_id_type: FileId) { self.fbb_.push_slot::(UpdateParent::VT_CHILD_ID_TYPE, child_id_type, FileId::NONE); } #[inline] pub fn add_child_id(&mut self, child_id: flatbuffers::WIPOffset) { self.fbb_.push_slot_always::>(UpdateParent::VT_CHILD_ID, child_id); } #[inline] pub fn add_new_parent_id_type(&mut self, new_parent_id_type: FileId) { self.fbb_.push_slot::(UpdateParent::VT_NEW_PARENT_ID_TYPE, new_parent_id_type, FileId::NONE); } #[inline] pub fn add_new_parent_id(&mut self, new_parent_id: flatbuffers::WIPOffset) { self.fbb_.push_slot_always::>(UpdateParent::VT_NEW_PARENT_ID, new_parent_id); } #[inline] pub fn add_new_name_in_parent(&mut self, new_name_in_parent: flatbuffers::WIPOffset<&'b str>) { self.fbb_.push_slot_always::>(UpdateParent::VT_NEW_NAME_IN_PARENT, new_name_in_parent); } #[inline] pub fn add_local_timestamp(&mut self, local_timestamp: &'b super::Timestamp) { self.fbb_.push_slot_always::<&super::Timestamp>(UpdateParent::VT_LOCAL_TIMESTAMP, local_timestamp); } #[inline] pub fn add_lamport_timestamp(&mut self, lamport_timestamp: &'b super::Timestamp) { self.fbb_.push_slot_always::<&super::Timestamp>(UpdateParent::VT_LAMPORT_TIMESTAMP, lamport_timestamp); } #[inline] pub fn new(_fbb: &'b mut flatbuffers::FlatBufferBuilder<'a>) -> UpdateParentBuilder<'a, 'b> { let start = _fbb.start_table(); UpdateParentBuilder { fbb_: _fbb, start_: start, } } #[inline] pub fn finish(self) -> flatbuffers::WIPOffset> { let o = self.fbb_.end_table(self.start_); flatbuffers::WIPOffset::new(o.value()) } } pub enum BufferOperationOffset {} #[derive(Copy, Clone, Debug, PartialEq)] pub struct BufferOperation<'a> { pub _tab: flatbuffers::Table<'a>, } impl<'a> flatbuffers::Follow<'a> for BufferOperation<'a> { type Inner = BufferOperation<'a>; #[inline] fn follow(buf: &'a [u8], loc: usize) -> Self::Inner { Self { _tab: flatbuffers::Table { buf: buf, loc: loc }, } } } impl<'a> BufferOperation<'a> { #[inline] pub fn init_from_table(table: flatbuffers::Table<'a>) -> Self { BufferOperation { _tab: table, } } #[allow(unused_mut)] pub fn create<'bldr: 'args, 'args: 'mut_bldr, 'mut_bldr>( _fbb: &'mut_bldr mut flatbuffers::FlatBufferBuilder<'bldr>, args: &'args BufferOperationArgs<'args>) -> flatbuffers::WIPOffset> { let mut builder = BufferOperationBuilder::new(_fbb); if let Some(x) = args.lamport_timestamp { builder.add_lamport_timestamp(x); } if let Some(x) = args.local_timestamp { builder.add_local_timestamp(x); } if let Some(x) = args.operations { builder.add_operations(x); } if let Some(x) = args.file_id { builder.add_file_id(x); } builder.add_file_id_type(args.file_id_type); builder.finish() } pub const VT_FILE_ID_TYPE: flatbuffers::VOffsetT = 4; pub const VT_FILE_ID: flatbuffers::VOffsetT = 6; pub const VT_OPERATIONS: flatbuffers::VOffsetT = 8; pub const VT_LOCAL_TIMESTAMP: flatbuffers::VOffsetT = 10; pub const VT_LAMPORT_TIMESTAMP: flatbuffers::VOffsetT = 12; #[inline] pub fn file_id_type(&self) -> FileId { self._tab.get::(BufferOperation::VT_FILE_ID_TYPE, Some(FileId::NONE)).unwrap() } #[inline] pub fn file_id(&self) -> Option> { self._tab.get::>>(BufferOperation::VT_FILE_ID, None) } #[inline] pub fn operations(&self) -> Option>>> { self._tab.get::>>>>(BufferOperation::VT_OPERATIONS, None) } #[inline] pub fn local_timestamp(&self) -> Option<&'a super::Timestamp> { self._tab.get::(BufferOperation::VT_LOCAL_TIMESTAMP, None) } #[inline] pub fn lamport_timestamp(&self) -> Option<&'a super::Timestamp> { self._tab.get::(BufferOperation::VT_LAMPORT_TIMESTAMP, None) } #[inline] #[allow(non_snake_case)] pub fn file_id_as_base_file_id(&'a self) -> Option { if self.file_id_type() == FileId::BaseFileId { self.file_id().map(|u| BaseFileId::init_from_table(u)) } else { None } } #[inline] #[allow(non_snake_case)] pub fn file_id_as_new_file_id(&'a self) -> Option { if self.file_id_type() == FileId::NewFileId { self.file_id().map(|u| NewFileId::init_from_table(u)) } else { None } } } pub struct BufferOperationArgs<'a> { pub file_id_type: FileId, pub file_id: Option>, pub operations: Option>>>>, pub local_timestamp: Option<&'a super::Timestamp>, pub lamport_timestamp: Option<&'a super::Timestamp>, } impl<'a> Default for BufferOperationArgs<'a> { #[inline] fn default() -> Self { BufferOperationArgs { file_id_type: FileId::NONE, file_id: None, operations: None, local_timestamp: None, lamport_timestamp: None, } } } pub struct BufferOperationBuilder<'a: 'b, 'b> { fbb_: &'b mut flatbuffers::FlatBufferBuilder<'a>, start_: flatbuffers::WIPOffset, } impl<'a: 'b, 'b> BufferOperationBuilder<'a, 'b> { #[inline] pub fn add_file_id_type(&mut self, file_id_type: FileId) { self.fbb_.push_slot::(BufferOperation::VT_FILE_ID_TYPE, file_id_type, FileId::NONE); } #[inline] pub fn add_file_id(&mut self, file_id: flatbuffers::WIPOffset) { self.fbb_.push_slot_always::>(BufferOperation::VT_FILE_ID, file_id); } #[inline] pub fn add_operations(&mut self, operations: flatbuffers::WIPOffset>>>) { self.fbb_.push_slot_always::>(BufferOperation::VT_OPERATIONS, operations); } #[inline] pub fn add_local_timestamp(&mut self, local_timestamp: &'b super::Timestamp) { self.fbb_.push_slot_always::<&super::Timestamp>(BufferOperation::VT_LOCAL_TIMESTAMP, local_timestamp); } #[inline] pub fn add_lamport_timestamp(&mut self, lamport_timestamp: &'b super::Timestamp) { self.fbb_.push_slot_always::<&super::Timestamp>(BufferOperation::VT_LAMPORT_TIMESTAMP, lamport_timestamp); } #[inline] pub fn new(_fbb: &'b mut flatbuffers::FlatBufferBuilder<'a>) -> BufferOperationBuilder<'a, 'b> { let start = _fbb.start_table(); BufferOperationBuilder { fbb_: _fbb, start_: start, } } #[inline] pub fn finish(self) -> flatbuffers::WIPOffset> { let o = self.fbb_.end_table(self.start_); flatbuffers::WIPOffset::new(o.value()) } } pub enum UpdateActiveLocationOffset {} #[derive(Copy, Clone, Debug, PartialEq)] pub struct UpdateActiveLocation<'a> { pub _tab: flatbuffers::Table<'a>, } impl<'a> flatbuffers::Follow<'a> for UpdateActiveLocation<'a> { type Inner = UpdateActiveLocation<'a>; #[inline] fn follow(buf: &'a [u8], loc: usize) -> Self::Inner { Self { _tab: flatbuffers::Table { buf: buf, loc: loc }, } } } impl<'a> UpdateActiveLocation<'a> { #[inline] pub fn init_from_table(table: flatbuffers::Table<'a>) -> Self { UpdateActiveLocation { _tab: table, } } #[allow(unused_mut)] pub fn create<'bldr: 'args, 'args: 'mut_bldr, 'mut_bldr>( _fbb: &'mut_bldr mut flatbuffers::FlatBufferBuilder<'bldr>, args: &'args UpdateActiveLocationArgs<'args>) -> flatbuffers::WIPOffset> { let mut builder = UpdateActiveLocationBuilder::new(_fbb); if let Some(x) = args.lamport_timestamp { builder.add_lamport_timestamp(x); } if let Some(x) = args.file_id { builder.add_file_id(x); } builder.add_file_id_type(args.file_id_type); builder.finish() } pub const VT_FILE_ID_TYPE: flatbuffers::VOffsetT = 4; pub const VT_FILE_ID: flatbuffers::VOffsetT = 6; pub const VT_LAMPORT_TIMESTAMP: flatbuffers::VOffsetT = 8; #[inline] pub fn file_id_type(&self) -> FileId { self._tab.get::(UpdateActiveLocation::VT_FILE_ID_TYPE, Some(FileId::NONE)).unwrap() } #[inline] pub fn file_id(&self) -> Option> { self._tab.get::>>(UpdateActiveLocation::VT_FILE_ID, None) } #[inline] pub fn lamport_timestamp(&self) -> Option<&'a super::Timestamp> { self._tab.get::(UpdateActiveLocation::VT_LAMPORT_TIMESTAMP, None) } #[inline] #[allow(non_snake_case)] pub fn file_id_as_base_file_id(&'a self) -> Option { if self.file_id_type() == FileId::BaseFileId { self.file_id().map(|u| BaseFileId::init_from_table(u)) } else { None } } #[inline] #[allow(non_snake_case)] pub fn file_id_as_new_file_id(&'a self) -> Option { if self.file_id_type() == FileId::NewFileId { self.file_id().map(|u| NewFileId::init_from_table(u)) } else { None } } } pub struct UpdateActiveLocationArgs<'a> { pub file_id_type: FileId, pub file_id: Option>, pub lamport_timestamp: Option<&'a super::Timestamp>, } impl<'a> Default for UpdateActiveLocationArgs<'a> { #[inline] fn default() -> Self { UpdateActiveLocationArgs { file_id_type: FileId::NONE, file_id: None, lamport_timestamp: None, } } } pub struct UpdateActiveLocationBuilder<'a: 'b, 'b> { fbb_: &'b mut flatbuffers::FlatBufferBuilder<'a>, start_: flatbuffers::WIPOffset, } impl<'a: 'b, 'b> UpdateActiveLocationBuilder<'a, 'b> { #[inline] pub fn add_file_id_type(&mut self, file_id_type: FileId) { self.fbb_.push_slot::(UpdateActiveLocation::VT_FILE_ID_TYPE, file_id_type, FileId::NONE); } #[inline] pub fn add_file_id(&mut self, file_id: flatbuffers::WIPOffset) { self.fbb_.push_slot_always::>(UpdateActiveLocation::VT_FILE_ID, file_id); } #[inline] pub fn add_lamport_timestamp(&mut self, lamport_timestamp: &'b super::Timestamp) { self.fbb_.push_slot_always::<&super::Timestamp>(UpdateActiveLocation::VT_LAMPORT_TIMESTAMP, lamport_timestamp); } #[inline] pub fn new(_fbb: &'b mut flatbuffers::FlatBufferBuilder<'a>) -> UpdateActiveLocationBuilder<'a, 'b> { let start = _fbb.start_table(); UpdateActiveLocationBuilder { fbb_: _fbb, start_: start, } } #[inline] pub fn finish(self) -> flatbuffers::WIPOffset> { let o = self.fbb_.end_table(self.start_); flatbuffers::WIPOffset::new(o.value()) } } } // pub mod epoch pub mod worktree { #![allow(dead_code)] #![allow(unused_imports)] use std::mem; use std::cmp::Ordering; extern crate flatbuffers; use self::flatbuffers::EndianScalar; #[allow(non_camel_case_types)] #[repr(u8)] #[derive(Clone, Copy, PartialEq, Debug)] pub enum OperationVariant { NONE = 0, StartEpoch = 1, EpochOperation = 2, } const ENUM_MIN_OPERATION_VARIANT: u8 = 0; const ENUM_MAX_OPERATION_VARIANT: u8 = 2; impl<'a> flatbuffers::Follow<'a> for OperationVariant { type Inner = Self; #[inline] fn follow(buf: &'a [u8], loc: usize) -> Self::Inner { flatbuffers::read_scalar_at::(buf, loc) } } impl flatbuffers::EndianScalar for OperationVariant { #[inline] fn to_little_endian(self) -> Self { let n = u8::to_le(self as u8); let p = &n as *const u8 as *const OperationVariant; unsafe { *p } } #[inline] fn from_little_endian(self) -> Self { let n = u8::from_le(self as u8); let p = &n as *const u8 as *const OperationVariant; unsafe { *p } } } impl flatbuffers::Push for OperationVariant { type Output = OperationVariant; #[inline] fn push(&self, dst: &mut [u8], _rest: &[u8]) { flatbuffers::emplace_scalar::(dst, *self); } } #[allow(non_camel_case_types)] const ENUM_VALUES_OPERATION_VARIANT:[OperationVariant; 3] = [ OperationVariant::NONE, OperationVariant::StartEpoch, OperationVariant::EpochOperation ]; #[allow(non_camel_case_types)] const ENUM_NAMES_OPERATION_VARIANT:[&'static str; 3] = [ "NONE", "StartEpoch", "EpochOperation" ]; pub fn enum_name_operation_variant(e: OperationVariant) -> &'static str { let index: usize = e as usize; ENUM_NAMES_OPERATION_VARIANT[index] } pub struct OperationVariantUnionTableOffset {} pub enum StartEpochOffset {} #[derive(Copy, Clone, Debug, PartialEq)] pub struct StartEpoch<'a> { pub _tab: flatbuffers::Table<'a>, } impl<'a> flatbuffers::Follow<'a> for StartEpoch<'a> { type Inner = StartEpoch<'a>; #[inline] fn follow(buf: &'a [u8], loc: usize) -> Self::Inner { Self { _tab: flatbuffers::Table { buf: buf, loc: loc }, } } } impl<'a> StartEpoch<'a> { #[inline] pub fn init_from_table(table: flatbuffers::Table<'a>) -> Self { StartEpoch { _tab: table, } } #[allow(unused_mut)] pub fn create<'bldr: 'args, 'args: 'mut_bldr, 'mut_bldr>( _fbb: &'mut_bldr mut flatbuffers::FlatBufferBuilder<'bldr>, args: &'args StartEpochArgs<'args>) -> flatbuffers::WIPOffset> { let mut builder = StartEpochBuilder::new(_fbb); if let Some(x) = args.head { builder.add_head(x); } if let Some(x) = args.epoch_id { builder.add_epoch_id(x); } builder.finish() } pub const VT_EPOCH_ID: flatbuffers::VOffsetT = 4; pub const VT_HEAD: flatbuffers::VOffsetT = 6; #[inline] pub fn epoch_id(&self) -> Option<&'a super::Timestamp> { self._tab.get::(StartEpoch::VT_EPOCH_ID, None) } #[inline] pub fn head(&self) -> Option<&'a [u8]> { self._tab.get::>>(StartEpoch::VT_HEAD, None).map(|v| v.safe_slice()) } } pub struct StartEpochArgs<'a> { pub epoch_id: Option<&'a super::Timestamp>, pub head: Option>>, } impl<'a> Default for StartEpochArgs<'a> { #[inline] fn default() -> Self { StartEpochArgs { epoch_id: None, head: None, } } } pub struct StartEpochBuilder<'a: 'b, 'b> { fbb_: &'b mut flatbuffers::FlatBufferBuilder<'a>, start_: flatbuffers::WIPOffset, } impl<'a: 'b, 'b> StartEpochBuilder<'a, 'b> { #[inline] pub fn add_epoch_id(&mut self, epoch_id: &'b super::Timestamp) { self.fbb_.push_slot_always::<&super::Timestamp>(StartEpoch::VT_EPOCH_ID, epoch_id); } #[inline] pub fn add_head(&mut self, head: flatbuffers::WIPOffset>) { self.fbb_.push_slot_always::>(StartEpoch::VT_HEAD, head); } #[inline] pub fn new(_fbb: &'b mut flatbuffers::FlatBufferBuilder<'a>) -> StartEpochBuilder<'a, 'b> { let start = _fbb.start_table(); StartEpochBuilder { fbb_: _fbb, start_: start, } } #[inline] pub fn finish(self) -> flatbuffers::WIPOffset> { let o = self.fbb_.end_table(self.start_); flatbuffers::WIPOffset::new(o.value()) } } pub enum EpochOperationOffset {} #[derive(Copy, Clone, Debug, PartialEq)] pub struct EpochOperation<'a> { pub _tab: flatbuffers::Table<'a>, } impl<'a> flatbuffers::Follow<'a> for EpochOperation<'a> { type Inner = EpochOperation<'a>; #[inline] fn follow(buf: &'a [u8], loc: usize) -> Self::Inner { Self { _tab: flatbuffers::Table { buf: buf, loc: loc }, } } } impl<'a> EpochOperation<'a> { #[inline] pub fn init_from_table(table: flatbuffers::Table<'a>) -> Self { EpochOperation { _tab: table, } } #[allow(unused_mut)] pub fn create<'bldr: 'args, 'args: 'mut_bldr, 'mut_bldr>( _fbb: &'mut_bldr mut flatbuffers::FlatBufferBuilder<'bldr>, args: &'args EpochOperationArgs<'args>) -> flatbuffers::WIPOffset> { let mut builder = EpochOperationBuilder::new(_fbb); if let Some(x) = args.operation { builder.add_operation(x); } if let Some(x) = args.epoch_id { builder.add_epoch_id(x); } builder.add_operation_type(args.operation_type); builder.finish() } pub const VT_EPOCH_ID: flatbuffers::VOffsetT = 4; pub const VT_OPERATION_TYPE: flatbuffers::VOffsetT = 6; pub const VT_OPERATION: flatbuffers::VOffsetT = 8; #[inline] pub fn epoch_id(&self) -> Option<&'a super::Timestamp> { self._tab.get::(EpochOperation::VT_EPOCH_ID, None) } #[inline] pub fn operation_type(&self) -> super::epoch::Operation { self._tab.get::(EpochOperation::VT_OPERATION_TYPE, Some(super::epoch::Operation::NONE)).unwrap() } #[inline] pub fn operation(&self) -> Option> { self._tab.get::>>(EpochOperation::VT_OPERATION, None) } #[inline] #[allow(non_snake_case)] pub fn operation_as_insert_metadata(&'a self) -> Option { if self.operation_type() == super::epoch::Operation::InsertMetadata { self.operation().map(|u| super::epoch::InsertMetadata::init_from_table(u)) } else { None } } #[inline] #[allow(non_snake_case)] pub fn operation_as_update_parent(&'a self) -> Option { if self.operation_type() == super::epoch::Operation::UpdateParent { self.operation().map(|u| super::epoch::UpdateParent::init_from_table(u)) } else { None } } #[inline] #[allow(non_snake_case)] pub fn operation_as_buffer_operation(&'a self) -> Option { if self.operation_type() == super::epoch::Operation::BufferOperation { self.operation().map(|u| super::epoch::BufferOperation::init_from_table(u)) } else { None } } #[inline] #[allow(non_snake_case)] pub fn operation_as_update_active_location(&'a self) -> Option { if self.operation_type() == super::epoch::Operation::UpdateActiveLocation { self.operation().map(|u| super::epoch::UpdateActiveLocation::init_from_table(u)) } else { None } } } pub struct EpochOperationArgs<'a> { pub epoch_id: Option<&'a super::Timestamp>, pub operation_type: super::epoch::Operation, pub operation: Option>, } impl<'a> Default for EpochOperationArgs<'a> { #[inline] fn default() -> Self { EpochOperationArgs { epoch_id: None, operation_type: super::epoch::Operation::NONE, operation: None, } } } pub struct EpochOperationBuilder<'a: 'b, 'b> { fbb_: &'b mut flatbuffers::FlatBufferBuilder<'a>, start_: flatbuffers::WIPOffset, } impl<'a: 'b, 'b> EpochOperationBuilder<'a, 'b> { #[inline] pub fn add_epoch_id(&mut self, epoch_id: &'b super::Timestamp) { self.fbb_.push_slot_always::<&super::Timestamp>(EpochOperation::VT_EPOCH_ID, epoch_id); } #[inline] pub fn add_operation_type(&mut self, operation_type: super::epoch::Operation) { self.fbb_.push_slot::(EpochOperation::VT_OPERATION_TYPE, operation_type, super::epoch::Operation::NONE); } #[inline] pub fn add_operation(&mut self, operation: flatbuffers::WIPOffset) { self.fbb_.push_slot_always::>(EpochOperation::VT_OPERATION, operation); } #[inline] pub fn new(_fbb: &'b mut flatbuffers::FlatBufferBuilder<'a>) -> EpochOperationBuilder<'a, 'b> { let start = _fbb.start_table(); EpochOperationBuilder { fbb_: _fbb, start_: start, } } #[inline] pub fn finish(self) -> flatbuffers::WIPOffset> { let o = self.fbb_.end_table(self.start_); flatbuffers::WIPOffset::new(o.value()) } } pub enum OperationOffset {} #[derive(Copy, Clone, Debug, PartialEq)] pub struct Operation<'a> { pub _tab: flatbuffers::Table<'a>, } impl<'a> flatbuffers::Follow<'a> for Operation<'a> { type Inner = Operation<'a>; #[inline] fn follow(buf: &'a [u8], loc: usize) -> Self::Inner { Self { _tab: flatbuffers::Table { buf: buf, loc: loc }, } } } impl<'a> Operation<'a> { #[inline] pub fn init_from_table(table: flatbuffers::Table<'a>) -> Self { Operation { _tab: table, } } #[allow(unused_mut)] pub fn create<'bldr: 'args, 'args: 'mut_bldr, 'mut_bldr>( _fbb: &'mut_bldr mut flatbuffers::FlatBufferBuilder<'bldr>, args: &'args OperationArgs) -> flatbuffers::WIPOffset> { let mut builder = OperationBuilder::new(_fbb); if let Some(x) = args.variant { builder.add_variant(x); } builder.add_variant_type(args.variant_type); builder.finish() } pub const VT_VARIANT_TYPE: flatbuffers::VOffsetT = 4; pub const VT_VARIANT: flatbuffers::VOffsetT = 6; #[inline] pub fn variant_type(&self) -> OperationVariant { self._tab.get::(Operation::VT_VARIANT_TYPE, Some(OperationVariant::NONE)).unwrap() } #[inline] pub fn variant(&self) -> Option> { self._tab.get::>>(Operation::VT_VARIANT, None) } #[inline] #[allow(non_snake_case)] pub fn variant_as_start_epoch(&'a self) -> Option { if self.variant_type() == OperationVariant::StartEpoch { self.variant().map(|u| StartEpoch::init_from_table(u)) } else { None } } #[inline] #[allow(non_snake_case)] pub fn variant_as_epoch_operation(&'a self) -> Option { if self.variant_type() == OperationVariant::EpochOperation { self.variant().map(|u| EpochOperation::init_from_table(u)) } else { None } } } pub struct OperationArgs { pub variant_type: OperationVariant, pub variant: Option>, } impl<'a> Default for OperationArgs { #[inline] fn default() -> Self { OperationArgs { variant_type: OperationVariant::NONE, variant: None, } } } pub struct OperationBuilder<'a: 'b, 'b> { fbb_: &'b mut flatbuffers::FlatBufferBuilder<'a>, start_: flatbuffers::WIPOffset, } impl<'a: 'b, 'b> OperationBuilder<'a, 'b> { #[inline] pub fn add_variant_type(&mut self, variant_type: OperationVariant) { self.fbb_.push_slot::(Operation::VT_VARIANT_TYPE, variant_type, OperationVariant::NONE); } #[inline] pub fn add_variant(&mut self, variant: flatbuffers::WIPOffset) { self.fbb_.push_slot_always::>(Operation::VT_VARIANT, variant); } #[inline] pub fn new(_fbb: &'b mut flatbuffers::FlatBufferBuilder<'a>) -> OperationBuilder<'a, 'b> { let start = _fbb.start_table(); OperationBuilder { fbb_: _fbb, start_: start, } } #[inline] pub fn finish(self) -> flatbuffers::WIPOffset> { let o = self.fbb_.end_table(self.start_); flatbuffers::WIPOffset::new(o.value()) } } #[inline] pub fn get_root_as_operation<'a>(buf: &'a [u8]) -> Operation<'a> { flatbuffers::get_root::>(buf) } #[inline] pub fn get_size_prefixed_root_as_operation<'a>(buf: &'a [u8]) -> Operation<'a> { flatbuffers::get_size_prefixed_root::>(buf) } #[inline] pub fn finish_operation_buffer<'a, 'b>( fbb: &'b mut flatbuffers::FlatBufferBuilder<'a>, root: flatbuffers::WIPOffset>) { fbb.finish(root, None); } #[inline] pub fn finish_size_prefixed_operation_buffer<'a, 'b>(fbb: &'b mut flatbuffers::FlatBufferBuilder<'a>, root: flatbuffers::WIPOffset>) { fbb.finish_size_prefixed(root, None); } } // pub mod worktree use flatbuffers::EndianScalar; ================================================ FILE: memo_core/src/time.rs ================================================ use crate::serialization; use crate::Error; use crate::ReplicaId; use crate::ReplicaIdExt; use flatbuffers::{FlatBufferBuilder, WIPOffset}; use serde::{Deserializer, Serializer}; use serde_derive::{Deserialize, Serialize}; use std::cmp::{self, Ordering}; use std::collections::HashMap; use std::mem; use std::ops::{Add, AddAssign}; use std::sync::Arc; #[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq, Ord, PartialOrd)] pub struct Local { pub replica_id: ReplicaId, pub value: u64, } #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] pub struct Global( #[serde( serialize_with = "Global::serialize_inner", deserialize_with = "Global::deserialize_inner" )] Arc>, ); #[derive( Clone, Copy, Debug, Default, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, )] pub struct Lamport { pub value: u64, pub replica_id: ReplicaId, } impl Local { pub fn new(replica_id: ReplicaId) -> Self { Self { replica_id, value: 1, } } pub fn tick(&mut self) -> Self { let timestamp = *self; self.value += 1; timestamp } pub fn observe(&mut self, timestamp: Self) { if timestamp.replica_id == self.replica_id { self.value = cmp::max(self.value, timestamp.value + 1); } } pub fn to_flatbuf(&self) -> serialization::Timestamp { serialization::Timestamp::new(self.value, &self.replica_id.to_flatbuf()) } pub fn from_flatbuf(message: &serialization::Timestamp) -> Self { Self { value: message.value(), replica_id: ReplicaId::from_flatbuf(message.replica_id()), } } } impl<'a> Add<&'a Self> for Local { type Output = Local; fn add(self, other: &'a Self) -> Self::Output { cmp::max(&self, other).clone() } } impl<'a> AddAssign<&'a Local> for Local { fn add_assign(&mut self, other: &Self) { if *self < *other { *self = other.clone(); } } } impl Global { pub fn new() -> Self { Global(Arc::new(HashMap::new())) } fn serialize_inner( inner: &Arc>, serializer: S, ) -> Result where S: Serializer, { use serde::Serialize; inner.serialize(serializer) } fn deserialize_inner<'de, D>(deserializer: D) -> Result>, D::Error> where D: Deserializer<'de>, { use serde::Deserialize; Ok(Arc::new(HashMap::deserialize(deserializer)?)) } pub fn get(&self, replica_id: ReplicaId) -> u64 { *self.0.get(&replica_id).unwrap_or(&0) } pub fn observe(&mut self, timestamp: Local) { let map = Arc::make_mut(&mut self.0); let value = map.entry(timestamp.replica_id).or_insert(0); *value = cmp::max(*value, timestamp.value); } pub fn observe_all(&mut self, other: &Self) { for (replica_id, value) in other.0.as_ref() { self.observe(Local { replica_id: *replica_id, value: *value, }); } } pub fn observed(&self, timestamp: Local) -> bool { self.get(timestamp.replica_id) >= timestamp.value } pub fn changed_since(&self, other: &Self) -> bool { self.0 .iter() .any(|(replica_id, value)| *value > other.get(*replica_id)) } pub fn to_flatbuf<'fbb>( &self, builder: &mut FlatBufferBuilder<'fbb>, ) -> WIPOffset> { builder.start_vector::(self.0.len()); for (replica_id, value) in self.0.as_ref() { builder.push(&serialization::Timestamp::new( *value, &replica_id.to_flatbuf(), )); } let timestamps = Some(builder.end_vector(self.0.len())); serialization::GlobalTimestamp::create( builder, &serialization::GlobalTimestampArgs { timestamps }, ) } pub fn from_flatbuf<'fbb>( message: serialization::GlobalTimestamp<'fbb>, ) -> Result { let mut local_timestamps = HashMap::new(); for local_timestamp in message.timestamps().ok_or(Error::DeserializeError)? { let replica_id = ReplicaId::from_flatbuf(local_timestamp.replica_id()); let value = local_timestamp.value(); local_timestamps.insert(replica_id, value); } Ok(Global(Arc::new(local_timestamps))) } } impl PartialOrd for Global { fn partial_cmp(&self, other: &Self) -> Option { let mut global_ordering = Ordering::Equal; for replica_id in self.0.keys().chain(other.0.keys()) { let ordering = self.get(*replica_id).cmp(&other.get(*replica_id)); if ordering != Ordering::Equal { if global_ordering == Ordering::Equal { global_ordering = ordering; } else if ordering != global_ordering { return None; } } } Some(global_ordering) } } impl Lamport { pub fn new(replica_id: ReplicaId) -> Self { Self { value: 1, replica_id, } } pub fn tick(&mut self) -> Self { let timestamp = *self; self.value += 1; timestamp } pub fn observe(&mut self, timestamp: Self) { self.value = cmp::max(self.value, timestamp.value) + 1; } pub fn to_flatbuf(&self) -> serialization::Timestamp { serialization::Timestamp::new(self.value, &self.replica_id.to_flatbuf()) } pub fn from_flatbuf(message: &serialization::Timestamp) -> Self { Self { value: message.value(), replica_id: ReplicaId::from_flatbuf(message.replica_id()), } } pub fn to_bytes(&self) -> [u8; 24] { let mut bytes = [0; 24]; bytes[0..8].copy_from_slice(unsafe { &mem::transmute::(self.value.to_be()) }); bytes[8..24].copy_from_slice(self.replica_id.as_bytes()); bytes } } ================================================ FILE: memo_core/src/work_tree.rs ================================================ use crate::buffer::{self, Change, Point, Text}; use crate::epoch::{self, Cursor, DirEntry, Epoch, FileId, FileType}; use crate::serialization; use crate::{time, Error, Oid, ReplicaId}; use flatbuffers::{FlatBufferBuilder, WIPOffset}; use futures::{future, stream, Async, Future, Poll, Stream}; use serde_derive::{Deserialize, Serialize}; use std::cell::{Ref, RefCell, RefMut}; use std::cmp::Ordering; use std::collections::HashMap; use std::io; use std::mem; use std::ops::Range; use std::path::{Path, PathBuf}; use std::rc::Rc; pub trait GitProvider { fn base_entries(&self, oid: Oid) -> Box>; fn base_text(&self, oid: Oid, path: &Path) -> Box>; } pub trait ChangeObserver { fn changed(&self, buffer_id: BufferId, changes: Vec, selections: BufferSelectionRanges); } pub struct WorkTree { epoch: Option>>, buffers: Rc>>, next_buffer_id: Rc>, local_selection_sets: Rc>>>, next_local_selection_set_id: Rc>, deferred_ops: Rc>>>, lamport_clock: Rc>, git: Rc, observer: Option>, } #[derive(Serialize, Deserialize)] pub struct Version { epoch_id: epoch::Id, epoch_version: time::Global, } pub struct OperationEnvelope { pub epoch_head: Option, pub operation: Operation, } #[derive(Clone, Debug, Eq, PartialEq)] pub enum Operation { StartEpoch { epoch_id: epoch::Id, head: Option, }, EpochOperation { epoch_id: epoch::Id, operation: epoch::Operation, }, } #[derive(Copy, Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] pub struct BufferId(u32); #[derive(Copy, Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] pub struct LocalSelectionSetId(u32); #[derive(Clone, Debug, Eq, PartialEq)] pub struct BufferSelectionRanges { pub local: HashMap>>, pub remote: HashMap>>>, } enum MaybeDone { Pending(F), Done(Result), } struct BaseTextRequest { future: MaybeDone>>, path: PathBuf, } struct SwitchEpoch { to_assign: Rc>, cur_epoch: Rc>, last_seen: epoch::Id, base_text_requests: HashMap>, buffers: Rc>>, local_selection_sets: Rc>>>, deferred_ops: Rc>>>, lamport_clock: Rc>, git: Rc, observer: Option>, } impl WorkTree { pub fn new( replica_id: ReplicaId, base: Option, ops: I, git: Rc, observer: Option>, ) -> Result< ( WorkTree, Box>, ), Error, > where I: 'static + IntoIterator, { let mut ops = ops.into_iter().peekable(); let mut tree = WorkTree { epoch: None, buffers: Rc::new(RefCell::new(HashMap::new())), next_buffer_id: Rc::new(RefCell::new(BufferId(0))), local_selection_sets: Rc::new(RefCell::new(HashMap::new())), next_local_selection_set_id: Rc::new(RefCell::new(LocalSelectionSetId(0))), deferred_ops: Rc::new(RefCell::new(HashMap::new())), lamport_clock: Rc::new(RefCell::new(time::Lamport::new(replica_id))), git, observer, }; let ops = if ops.peek().is_none() { Box::new(tree.reset(base)) as Box> } else { Box::new(tree.apply_ops(ops)?) as Box> }; Ok((tree, ops)) } pub fn head(&self) -> Option { self.epoch.as_ref().and_then(|e| e.borrow().head) } pub fn epoch_id(&self) -> epoch::Id { self.cur_epoch().id } pub fn reset( &mut self, head: Option, ) -> impl Stream { let epoch_id = self.lamport_clock.borrow_mut().tick(); stream::once(Ok(OperationEnvelope { epoch_head: head, operation: Operation::StartEpoch { epoch_id, head }, })) .chain(self.start_epoch(epoch_id, head)) } pub fn apply_ops( &mut self, ops: I, ) -> Result, Error> where I: IntoIterator, { let mut cur_epoch_ops = Vec::new(); let mut epoch_streams = Vec::new(); for op in ops { match op { Operation::StartEpoch { epoch_id, head } => { self.lamport_clock.borrow_mut().observe(epoch_id); epoch_streams.push(self.start_epoch(epoch_id, head)); } Operation::EpochOperation { epoch_id, operation, } => { if let Some(epoch) = self.epoch.clone() { match epoch_id.cmp(&epoch.borrow().id) { Ordering::Less => {} Ordering::Equal => cur_epoch_ops.push(operation), Ordering::Greater => self.defer_epoch_op(epoch_id, operation), } } else { self.defer_epoch_op(epoch_id, operation); } } } } if let Some(epoch_ref) = self.epoch.clone() { let mut epoch = epoch_ref.borrow_mut(); let mut prev_versions = HashMap::new(); for file_id in self.buffers.borrow().values() { let edit_version = epoch.buffer_version(*file_id).unwrap(); let selections_last_update = epoch.buffer_selections_last_update(*file_id).unwrap(); prev_versions.insert(*file_id, (edit_version, selections_last_update)); } let fixup_ops = epoch.apply_ops(cur_epoch_ops, &mut self.lamport_clock.borrow_mut())?; if let Some(observer) = self.observer.as_ref() { for (buffer_id, file_id) in self.buffers.borrow().iter() { let (edit_version, selections_last_update) = prev_versions.remove(file_id).unwrap(); let changes: Vec<_> = epoch.changes_since(*file_id, &edit_version)?.collect(); if !changes.is_empty() || epoch.selections_changed_since(*file_id, selections_last_update)? { observer.changed( *buffer_id, changes, Self::selection_ranges_internal( &self.local_selection_sets.borrow(), &self.buffers.borrow(), &epoch, *buffer_id, )?, ); } } } let fixup_ops_stream = Box::new(stream::iter_ok(OperationEnvelope::wrap_many( epoch.id, epoch.head, fixup_ops, ))); Ok(epoch_streams.into_iter().fold( fixup_ops_stream as Box>, |acc, stream| Box::new(acc.chain(stream)), )) } else { Err(Error::InvalidOperations) } } fn start_epoch( &mut self, new_epoch_id: epoch::Id, new_head: Option, ) -> Box> { if self .epoch .as_ref() .map_or(true, |e| new_epoch_id > e.borrow().id) { let new_epoch = Rc::new(RefCell::new(Epoch::new( self.replica_id(), new_epoch_id, new_head, ))); let lamport_clock = self.lamport_clock.clone(); let new_epoch_clone = new_epoch.clone(); let load_base_entries = if let Some(new_head) = new_head { Box::new( self.git .base_entries(new_head) .map_err(|err| Error::IoError(err)) .chunks(500) .and_then(move |base_entries| { let fixup_ops = new_epoch_clone.borrow_mut().append_base_entries( base_entries, &mut lamport_clock.borrow_mut(), )?; Ok(stream::iter_ok(OperationEnvelope::wrap_many( new_epoch_id, Some(new_head), fixup_ops, ))) }) .flatten(), ) as Box> } else { Box::new(stream::empty()) }; if let Some(cur_epoch) = self.epoch.clone() { let switch_epoch = SwitchEpoch::new( new_epoch, cur_epoch, self.buffers.clone(), self.local_selection_sets.clone(), self.deferred_ops.clone(), self.lamport_clock.clone(), self.git.clone(), self.observer.clone(), ) .then(|fixup_ops| Ok(stream::iter_ok(fixup_ops?))) .flatten_stream(); Box::new(load_base_entries.chain(switch_epoch)) } else { self.epoch = Some(new_epoch.clone()); load_base_entries } } else { Box::new(stream::empty()) } } pub fn observed(&self, other: Version) -> bool { let version = self.version(); match version.epoch_id.cmp(&other.epoch_id) { Ordering::Less => false, Ordering::Equal => other.epoch_version <= version.epoch_version, Ordering::Greater => true, } } pub fn version(&self) -> Version { let epoch = self.cur_epoch(); Version { epoch_id: epoch.id, epoch_version: epoch.version(), } } pub fn with_cursor(&self, mut f: F) where F: FnMut(&mut Cursor), { if let Some(mut cursor) = self.cur_epoch().cursor() { f(&mut cursor); } } pub fn create_file

(&self, path: P, file_type: FileType) -> Result where P: AsRef, { let path = path.as_ref(); let name = path .file_name() .ok_or(Error::InvalidPath("path has no file name".into()))?; let mut cur_epoch = self.cur_epoch_mut(); let parent_id = if let Some(parent_path) = path.parent() { cur_epoch.file_id(parent_path)? } else { epoch::ROOT_FILE_ID }; let operation = cur_epoch.create_file( parent_id, name, file_type, &mut self.lamport_clock.borrow_mut(), )?; Ok(OperationEnvelope::wrap( cur_epoch.id, cur_epoch.head, operation, )) } pub fn rename(&self, old_path: P1, new_path: P2) -> Result where P1: AsRef, P2: AsRef, { let old_path = old_path.as_ref(); let new_path = new_path.as_ref(); let mut cur_epoch = self.cur_epoch_mut(); let file_id = cur_epoch.file_id(old_path)?; let new_name = new_path .file_name() .ok_or(Error::InvalidPath("new path has no file name".into()))?; let new_parent_id = if let Some(parent_path) = new_path.parent() { cur_epoch.file_id(parent_path)? } else { epoch::ROOT_FILE_ID }; let operation = cur_epoch.rename( file_id, new_parent_id, new_name, &mut self.lamport_clock.borrow_mut(), )?; Ok(OperationEnvelope::wrap( cur_epoch.id, cur_epoch.head, operation, )) } pub fn set_active_location( &self, buffer_id: Option, ) -> Result { let mut cur_epoch = self.cur_epoch_mut(); let file_id = if let Some(buffer_id) = buffer_id { Some(self.buffer_file_id(buffer_id)?) } else { None }; let operation = cur_epoch.set_active_location(file_id, &mut self.lamport_clock.borrow_mut())?; Ok(OperationEnvelope::wrap( cur_epoch.id, cur_epoch.head, operation, )) } pub fn replica_locations(&self) -> HashMap { let epoch = self.cur_epoch(); let mut locations = HashMap::new(); for (replica_id, file_id) in epoch.replica_locations() { if let Some(path) = epoch.path(file_id) { locations.insert(replica_id, path); } } locations } pub fn remove

(&self, path: P) -> Result where P: AsRef, { let mut cur_epoch = self.cur_epoch_mut(); let file_id = cur_epoch.file_id(path.as_ref())?; let operation = cur_epoch.remove(file_id, &mut self.lamport_clock.borrow_mut())?; Ok(OperationEnvelope::wrap( cur_epoch.id, cur_epoch.head, operation, )) } pub fn exists

(&self, path: P) -> bool where P: AsRef, { self.cur_epoch().file_id(path).is_ok() } pub fn open_text_file

(&self, path: P) -> Box> where P: Into, { Self::open_text_file_internal( path.into(), self.epoch.clone().unwrap(), self.git.clone(), self.buffers.clone(), self.next_buffer_id.clone(), self.lamport_clock.clone(), ) } fn open_text_file_internal( path: PathBuf, epoch: Rc>, git: Rc, buffers: Rc>>, next_buffer_id: Rc>, lamport_clock: Rc>, ) -> Box> { if let Some(buffer_id) = Self::existing_buffer(&epoch, &buffers, &path) { Box::new(future::ok(buffer_id)) } else { let epoch_id = epoch.borrow().id; Box::new( Self::base_text(&path, epoch.as_ref(), git.as_ref()).and_then( move |(file_id, base_text)| { if let Some(buffer_id) = Self::existing_buffer(&epoch, &buffers, &path) { Box::new(future::ok(buffer_id)) } else if epoch.borrow().id == epoch_id { match epoch.borrow_mut().open_text_file( file_id, base_text, &mut lamport_clock.borrow_mut(), ) { Ok(()) => { let buffer_id = *next_buffer_id.borrow(); next_buffer_id.borrow_mut().0 += 1; buffers.borrow_mut().insert(buffer_id, file_id); Box::new(future::ok(buffer_id)) } Err(error) => Box::new(future::err(error)), } } else { Self::open_text_file_internal( path, epoch, git, buffers, next_buffer_id, lamport_clock, ) } }, ), ) } } fn existing_buffer( epoch: &Rc>, buffers: &Rc>>, path: &Path, ) -> Option { let epoch = epoch.borrow(); for (buffer_id, file_id) in buffers.borrow().iter() { if let Some(existing_path) = epoch.path(*file_id) { if path == existing_path { return Some(*buffer_id); } } } None } fn base_text( path: &Path, epoch: &RefCell, git: &GitProvider, ) -> Box> { let epoch = epoch.borrow(); match epoch.file_id(&path) { Ok(file_id) => { if let (Some(head), Some(base_path)) = (epoch.head, epoch.base_path(file_id)) { Box::new( git.base_text(head, &base_path) .map_err(|err| Error::IoError(err)) .map(move |text| (file_id, text)), ) } else { Box::new(future::ok((file_id, String::new()))) } } Err(error) => Box::new(future::err(error)), } } pub fn edit( &self, buffer_id: BufferId, old_ranges: I, new_text: T, ) -> Result where I: IntoIterator>, T: Into, { let file_id = self.buffer_file_id(buffer_id)?; let mut cur_epoch = self.cur_epoch_mut(); let operation = cur_epoch .edit( file_id, old_ranges, new_text, &mut self.lamport_clock.borrow_mut(), ) .unwrap(); Ok(OperationEnvelope::wrap( cur_epoch.id, cur_epoch.head, operation, )) } pub fn edit_2d( &self, buffer_id: BufferId, old_ranges: I, new_text: T, ) -> Result where I: IntoIterator>, T: Into, { let file_id = self.buffer_file_id(buffer_id)?; let mut cur_epoch = self.cur_epoch_mut(); let operation = cur_epoch .edit_2d( file_id, old_ranges, new_text, &mut self.lamport_clock.borrow_mut(), ) .unwrap(); Ok(OperationEnvelope::wrap( cur_epoch.id, cur_epoch.head, operation, )) } pub fn add_selection_set( &self, buffer_id: BufferId, ranges: I, ) -> Result<(LocalSelectionSetId, OperationEnvelope), Error> where I: IntoIterator>, { let file_id = self.buffer_file_id(buffer_id)?; let mut cur_epoch = self.cur_epoch_mut(); let (remote_set_id, operation) = cur_epoch.add_selection_set(file_id, ranges, &mut self.lamport_clock.borrow_mut())?; let local_set_id = self.gen_local_set_id(); let mut local_selection_sets = self.local_selection_sets.borrow_mut(); let buffer_sets = local_selection_sets .entry(buffer_id) .or_insert(HashMap::new()); buffer_sets.insert(local_set_id, remote_set_id); Ok(( local_set_id, OperationEnvelope::wrap(cur_epoch.id, cur_epoch.head, operation), )) } pub fn replace_selection_set( &self, buffer_id: BufferId, local_set_id: LocalSelectionSetId, ranges: I, ) -> Result where I: IntoIterator>, { let file_id = self.buffer_file_id(buffer_id)?; let set_id = self.selection_set_id(buffer_id, local_set_id)?; let mut cur_epoch = self.cur_epoch_mut(); let operation = cur_epoch.replace_selection_set( file_id, set_id, ranges, &mut self.lamport_clock.borrow_mut(), )?; Ok(OperationEnvelope::wrap( cur_epoch.id, cur_epoch.head, operation, )) } pub fn remove_selection_set( &self, buffer_id: BufferId, local_set_id: LocalSelectionSetId, ) -> Result { let file_id = self.buffer_file_id(buffer_id)?; let set_id = self.selection_set_id(buffer_id, local_set_id)?; let mut cur_epoch = self.cur_epoch_mut(); let operation = cur_epoch.remove_selection_set( file_id, set_id, &mut self.lamport_clock.borrow_mut(), )?; self.local_selection_sets .borrow_mut() .get_mut(&buffer_id) .unwrap() .remove(&local_set_id); Ok(OperationEnvelope::wrap( cur_epoch.id, cur_epoch.head, operation, )) } pub fn path(&self, buffer_id: BufferId) -> Option { self.buffers .borrow() .get(&buffer_id) .and_then(|file_id| self.cur_epoch().path(*file_id)) } pub fn text(&self, buffer_id: BufferId) -> Result { let file_id = self.buffer_file_id(buffer_id)?; self.cur_epoch().text(file_id) } pub fn selection_ranges(&self, buffer_id: BufferId) -> Result { Self::selection_ranges_internal( &self.local_selection_sets.borrow(), &self.buffers.borrow(), &self.cur_epoch(), buffer_id, ) } fn selection_ranges_internal( local_selection_sets: &HashMap< BufferId, HashMap, >, buffers: &HashMap, epoch: &Epoch, buffer_id: BufferId, ) -> Result { let file_id = buffers .get(&buffer_id) .cloned() .ok_or(Error::InvalidBufferId)?; let mut set_ids_to_local_set_ids = HashMap::new(); if let Some(buffer_sets) = local_selection_sets.get(&buffer_id) { for (local_set_id, set_id) in buffer_sets { set_ids_to_local_set_ids.insert(*set_id, *local_set_id); } } let mut selections = BufferSelectionRanges { local: HashMap::new(), remote: HashMap::new(), }; for (set_id, ranges) in epoch.all_selection_ranges(file_id)? { if let Some(local_set_id) = set_ids_to_local_set_ids.get(&set_id) { selections.local.insert(*local_set_id, ranges); } else { selections .remote .entry(set_id.replica_id) .or_insert(Vec::new()) .push(ranges); } } Ok(selections) } pub fn changes_since( &self, buffer_id: BufferId, version: &time::Global, ) -> Result, Error> { let file_id = self.buffer_file_id(buffer_id)?; self.cur_epoch().changes_since(file_id, version) } pub fn buffer_deferred_ops_len(&self, buffer_id: BufferId) -> Result { let file_id = self.buffer_file_id(buffer_id)?; self.cur_epoch().buffer_deferred_ops_len(file_id) } fn cur_epoch(&self) -> Ref { self.epoch.as_ref().unwrap().borrow() } fn cur_epoch_mut(&self) -> RefMut { self.epoch.as_ref().unwrap().borrow_mut() } fn defer_epoch_op(&self, epoch_id: epoch::Id, operation: epoch::Operation) { self.deferred_ops .borrow_mut() .entry(epoch_id) .or_insert(Vec::new()) .push(operation); } fn replica_id(&self) -> ReplicaId { self.lamport_clock.borrow().replica_id } fn buffer_file_id(&self, buffer_id: BufferId) -> Result { self.buffers .borrow() .get(&buffer_id) .cloned() .ok_or(Error::InvalidBufferId) } fn gen_local_set_id(&self) -> LocalSelectionSetId { let local_set_id = *self.next_local_selection_set_id.borrow(); self.next_local_selection_set_id.borrow_mut().0 += 1; local_set_id } fn selection_set_id( &self, buffer_id: BufferId, set_id: LocalSelectionSetId, ) -> Result { self.local_selection_sets .borrow() .get(&buffer_id) .ok_or(Error::InvalidLocalSelectionSet(set_id))? .get(&set_id) .cloned() .ok_or(Error::InvalidLocalSelectionSet(set_id)) } } impl OperationEnvelope { fn wrap(epoch_id: epoch::Id, epoch_head: Option, operation: epoch::Operation) -> Self { OperationEnvelope { epoch_head, operation: Operation::EpochOperation { epoch_id, operation, }, } } fn wrap_many(epoch_id: epoch::Id, epoch_head: Option, operations: T) -> Vec where T: IntoIterator, { operations .into_iter() .map(move |operation| OperationEnvelope { epoch_head, operation: Operation::EpochOperation { epoch_id, operation, }, }) .collect() } } impl Operation { pub fn epoch_id(&self) -> epoch::Id { match self { Operation::StartEpoch { epoch_id, .. } => *epoch_id, Operation::EpochOperation { epoch_id, .. } => *epoch_id, } } pub fn is_selection_update(&self) -> bool { match self { Operation::EpochOperation { operation, .. } => match operation { epoch::Operation::BufferOperation { operations, .. } => { operations.iter().all(|buffer_op| match buffer_op { buffer::Operation::UpdateSelections { .. } => true, _ => false, }) } _ => false, }, _ => false, } } pub fn serialize(&self) -> Vec { let mut builder = FlatBufferBuilder::new(); let root = self.to_flatbuf(&mut builder); builder.finish(root, None); let (mut bytes, first_valid_byte_index) = builder.collapse(); bytes.drain(0..first_valid_byte_index); bytes } pub fn deserialize<'a>(buffer: &'a [u8]) -> Result, Error> { use crate::serialization::worktree::Operation; let root = flatbuffers::get_root::>(buffer); Self::from_flatbuf(root) } pub fn to_flatbuf<'fbb>( &self, builder: &mut FlatBufferBuilder<'fbb>, ) -> WIPOffset> { use crate::serialization::worktree::{ EpochOperation, EpochOperationArgs, Operation as OperationFlatbuf, OperationArgs, OperationVariant, StartEpoch, StartEpochArgs, }; let variant_type; let variant; match self { Operation::StartEpoch { epoch_id, head } => { variant_type = OperationVariant::StartEpoch; let head = head.map(|head| builder.create_vector(&head)); variant = StartEpoch::create( builder, &StartEpochArgs { epoch_id: Some(&epoch_id.to_flatbuf()), head, }, ) .as_union_value(); } Operation::EpochOperation { epoch_id, operation, } => { variant_type = OperationVariant::EpochOperation; let (epoch_operation_type, epoch_operation_table) = operation.to_flatbuf(builder); variant = EpochOperation::create( builder, &EpochOperationArgs { epoch_id: Some(&epoch_id.to_flatbuf()), operation_type: epoch_operation_type, operation: Some(epoch_operation_table), }, ) .as_union_value(); } } OperationFlatbuf::create( builder, &OperationArgs { variant_type, variant: Some(variant), }, ) } pub fn from_flatbuf<'fbb>( message: serialization::worktree::Operation<'fbb>, ) -> Result, Error> { use crate::serialization::worktree::{EpochOperation, OperationVariant, StartEpoch}; let variant = message.variant().ok_or(Error::DeserializeError)?; match message.variant_type() { OperationVariant::StartEpoch => { let message = StartEpoch::init_from_table(variant); let epoch_id = message.epoch_id().ok_or(Error::DeserializeError)?; Ok(Some(Operation::StartEpoch { epoch_id: time::Lamport::from_flatbuf(epoch_id), head: message.head().map(|head| { let mut oid = [0; 20]; oid.copy_from_slice(head); oid }), })) } OperationVariant::EpochOperation => { let message = EpochOperation::init_from_table(variant); let operation = message.operation().ok_or(Error::DeserializeError)?; let epoch_id = message.epoch_id().ok_or(Error::DeserializeError)?; if let Some(epoch_op) = epoch::Operation::from_flatbuf(message.operation_type(), operation)? { Ok(Some(Operation::EpochOperation { epoch_id: time::Lamport::from_flatbuf(epoch_id), operation: epoch_op, })) } else { Ok(None) } } OperationVariant::NONE => Ok(None), } } } impl SwitchEpoch { fn new( to_assign: Rc>, cur_epoch: Rc>, buffers: Rc>>, local_selection_sets: Rc< RefCell>>, >, deferred_ops: Rc>>>, lamport_clock: Rc>, git: Rc, observer: Option>, ) -> Self { let last_seen = cur_epoch.borrow().id; Self { to_assign, cur_epoch, last_seen, base_text_requests: HashMap::new(), buffers, local_selection_sets, deferred_ops, lamport_clock, git, observer, } } } impl Future for SwitchEpoch { type Item = Vec; type Error = Error; fn poll(&mut self) -> Poll { let mut buffers = self.buffers.borrow_mut(); let mut cur_epoch = self.cur_epoch.borrow_mut(); let mut to_assign = self.to_assign.borrow_mut(); let mut deferred_ops = self.deferred_ops.borrow_mut(); let mut lamport_clock = self.lamport_clock.borrow_mut(); let mut local_selection_sets = self.local_selection_sets.borrow_mut(); if to_assign.id > cur_epoch.id { if self.last_seen != cur_epoch.id { self.last_seen = cur_epoch.id; self.base_text_requests.clear(); } for (buffer_id, file_id) in buffers.iter() { let path = cur_epoch.path(*file_id); let request_is_outdated = if let Some(request) = self.base_text_requests.get(&buffer_id) { path.as_ref() != request.as_ref().map(|r| &r.path) } else { true }; if request_is_outdated { let will_be_untitled = path.as_ref().map_or(true, |path| { if let Ok(file_id) = to_assign.file_id(path) { to_assign.file_type(file_id).unwrap() != FileType::Text } else { true } }); if will_be_untitled { self.base_text_requests.insert(*buffer_id, None); } else { let path = path.unwrap(); let head = to_assign .head .expect("If we found a path, destination epoch must have a head"); self.base_text_requests.insert( *buffer_id, Some(BaseTextRequest { future: MaybeDone::Pending(self.git.base_text(head, &path)), path, }), ); } } } let mut is_done = true; for request in self.base_text_requests.values_mut() { if let Some(request) = request { request.future.poll(); is_done = is_done && request.future.is_done(); } } if is_done { let mut fixup_ops = Vec::new(); let mut buffer_mappings = Vec::with_capacity(self.base_text_requests.len()); for (buffer_id, request) in self.base_text_requests.drain() { if let Some(request) = request { let base_text = request.future.take_result().unwrap()?; let new_file_id = to_assign.file_id(request.path).unwrap(); to_assign.open_text_file(new_file_id, base_text, &mut lamport_clock)?; buffer_mappings.push((buffer_id, new_file_id)); } else { // TODO: This may be okay for now, but I think we should take a smarter // approach, where the site which initiates the reset transmits a mapping // of previous file ids to new file ids. Then, when receiving a new epoch, // we will check if we can map the open buffer to a file id and, only if we // can't, we will resort to path-based mapping or to creating a completely // new file id for untitled buffers. let (new_file_id, operation) = to_assign.new_text_file(&mut lamport_clock); fixup_ops.push(OperationEnvelope::wrap( to_assign.id, to_assign.head, operation, )); to_assign.open_text_file(new_file_id, "", &mut lamport_clock)?; let operation = to_assign.edit( new_file_id, Some(0..0), cur_epoch.text(buffers[&buffer_id])?.into_string().as_str(), &mut lamport_clock, )?; fixup_ops.push(OperationEnvelope::wrap( to_assign.id, to_assign.head, operation, )); buffer_mappings.push((buffer_id, new_file_id)); } } if let Some(ops) = deferred_ops.remove(&to_assign.id) { fixup_ops.extend(OperationEnvelope::wrap_many( to_assign.id, to_assign.head, to_assign.apply_ops(ops, &mut lamport_clock)?, )); } deferred_ops.retain(|id, _| *id > to_assign.id); let old_active_location = cur_epoch.replica_location(lamport_clock.replica_id); let mut buffer_changes = Vec::new(); for (buffer_id, new_file_id) in buffer_mappings { let old_file_id = buffers[&buffer_id]; let changes = buffer::diff( &cur_epoch.text(old_file_id)?.collect::>(), &to_assign.text(new_file_id)?.collect::>(), ); // TODO: This is inefficient and somewhat inelegant. We should transform // selections using only spatial coordinates, as opposed to editing the // previous buffer's text. let mut tmp_lamport_clock = lamport_clock.clone(); for change in &changes { cur_epoch.edit_2d( old_file_id, Some(change.range.clone()), change.code_units.clone(), &mut tmp_lamport_clock, )?; } if let Some(buffer_sets) = local_selection_sets.get_mut(&buffer_id) { for set_id in buffer_sets.values_mut() { let new_ranges = cur_epoch.selection_ranges(old_file_id, *set_id).unwrap(); let (new_set_id, op) = to_assign .add_selection_set(new_file_id, new_ranges, &mut lamport_clock) .unwrap(); fixup_ops.push(OperationEnvelope::wrap( to_assign.id, to_assign.head, op, )); *set_id = new_set_id; } } if old_active_location.map_or(false, |location| location == old_file_id) { let op = to_assign .set_active_location(Some(new_file_id), &mut lamport_clock) .unwrap(); fixup_ops.push(OperationEnvelope::wrap(to_assign.id, to_assign.head, op)); } buffer_changes.push((buffer_id, changes)); buffers.insert(buffer_id, new_file_id); } mem::swap(&mut *cur_epoch, &mut *to_assign); if let Some(observer) = self.observer.as_ref() { for (buffer_id, changes) in buffer_changes { observer.changed( buffer_id, changes, WorkTree::selection_ranges_internal( &local_selection_sets, &buffers, &cur_epoch, buffer_id, )?, ); } } Ok(Async::Ready(fixup_ops)) } else { Ok(Async::NotReady) } } else { // Cancel future prematurely if the current epoch is newer than the one we wanted to // assign. Ok(Async::Ready(Vec::new())) } } } impl MaybeDone { fn is_done(&self) -> bool { match self { MaybeDone::Pending(_) => false, MaybeDone::Done(_) => true, } } fn poll(&mut self) { match self { MaybeDone::Pending(f) => match f.poll() { Ok(Async::Ready(value)) => *self = MaybeDone::Done(Ok(value)), Ok(Async::NotReady) => {} Err(error) => *self = MaybeDone::Done(Err(error)), }, MaybeDone::Done(_) => {} } } fn take_result(self) -> Option> { match self { MaybeDone::Pending(_) => None, MaybeDone::Done(result) => Some(result), } } } #[cfg(test)] mod tests { use super::*; use crate::epoch::CursorEntry; use rand::{Rng, SeedableRng, StdRng}; use uuid::Uuid; #[test] fn test_random() { use crate::tests::Network; const PEERS: usize = 5; for seed in 0..100 { println!("SEED: {:?}", seed); let mut rng = StdRng::from_seed(&[seed]); let git = Rc::new(TestGitProvider::new()); let mut commits = vec![None]; let base_tree = WorkTree::empty(); for _ in 0..10 { for path in base_tree.visible_paths(FileType::Text) { base_tree.open_text_file(&path).wait().unwrap(); } base_tree.randomly_mutate(&mut rng, 5); commits.push(Some(git.commit(&base_tree))); } let mut observers = Vec::new(); let mut trees = Vec::new(); let mut network = Network::new(); for i in 0..PEERS { let observer = Rc::new(TestChangeObserver::new()); let commit = if rng.gen_weighted_bool(4) { *rng.choose(&commits).unwrap() } else { *commits.last().unwrap() }; let (tree, ops) = WorkTree::new( Uuid::from_u128((i + 1) as u128), commit, None, git.clone(), Some(observer.clone()), ) .unwrap(); network.add_peer(tree.replica_id()); network.broadcast( tree.replica_id(), serialize_ops(open_envelopes(ops.collect().wait().unwrap())), &mut rng, ); observers.push(observer); trees.push(tree); } for _ in 0..10 { let replica_index = rng.gen_range(0, PEERS); let tree = &mut trees[replica_index]; let observer = &observers[replica_index]; let replica_id = tree.replica_id(); let k = rng.gen_range(0, 4); if k == 0 { tree.open_random_buffers(&mut rng, observer, 5); } else if k == 1 { let head = *rng.choose(&commits).unwrap(); let ops = open_envelopes(tree.reset(head).collect().wait().unwrap()); network.broadcast(replica_id, serialize_ops(ops), &mut rng); } else if k == 2 && network.has_unreceived(replica_id) { let received_ops = network.receive(replica_id, &mut rng); let fixup_ops = open_envelopes( tree.apply_ops(deserialize_ops(received_ops)) .unwrap() .collect() .wait() .unwrap(), ); network.broadcast(replica_id, serialize_ops(fixup_ops), &mut rng); } else { let ops = tree.randomly_mutate(&mut rng, 5); network.broadcast(replica_id, serialize_ops(open_envelopes(ops)), &mut rng); } } while !network.is_idle() { for replica_index in 0..PEERS { let tree = &mut trees[replica_index]; let replica_id = tree.replica_id(); let received_ops = network.receive(replica_id, &mut rng); let fixup_ops = tree.apply_ops(deserialize_ops(received_ops)).unwrap(); network.broadcast( replica_id, serialize_ops(open_envelopes(fixup_ops.collect().wait().unwrap())), &mut rng, ); } } for replica_index in 0..PEERS - 1 { let tree_1 = &trees[replica_index]; let tree_2 = &trees[replica_index + 1]; assert_eq!(tree_1.cur_epoch().id, tree_2.cur_epoch().id); assert_eq!(tree_1.cur_epoch().head, tree_2.cur_epoch().head); assert_eq!(tree_1.entries(), tree_2.entries()); assert_eq!(tree_1.replica_locations(), tree_2.replica_locations()); } for replica_index in 0..PEERS { let tree = &trees[replica_index]; let observer = &observers[replica_index]; for buffer_id in tree.open_buffers() { assert_eq!( observer.text(buffer_id), tree.text(buffer_id).unwrap().into_string() ); assert_eq!( observer.selection_ranges(buffer_id), tree.selection_ranges(buffer_id).unwrap() ); } } } } #[test] fn test_reset() { let git = Rc::new(TestGitProvider::new()); let base_tree = WorkTree::empty(); base_tree.create_file("a", FileType::Text).unwrap(); let a_base = base_tree.open_text_file("a").wait().unwrap(); base_tree.edit(a_base, Some(0..0), "abc").unwrap(); let commit_0 = git.commit(&base_tree); base_tree.edit(a_base, Some(1..2), "def").unwrap(); base_tree.create_file("b", FileType::Directory).unwrap(); let commit_1 = git.commit(&base_tree); base_tree.edit(a_base, Some(2..3), "ghi").unwrap(); base_tree.create_file("b/c", FileType::Text).unwrap(); let commit_2 = git.commit(&base_tree); let observer_1 = Rc::new(TestChangeObserver::new()); let observer_2 = Rc::new(TestChangeObserver::new()); let (mut tree_1, ops_1) = WorkTree::new( Uuid::from_u128(1), Some(commit_0), vec![], git.clone(), Some(observer_1.clone()), ) .unwrap(); let (mut tree_2, ops_2) = WorkTree::new( Uuid::from_u128(2), Some(commit_0), open_envelopes(ops_1.collect().wait().unwrap()), git.clone(), Some(observer_2.clone()), ) .unwrap(); assert!(ops_2.wait().next().is_none()); assert_eq!(tree_1.head(), Some(commit_0)); assert_eq!(tree_1.dir_entries(), git.tree(commit_0).dir_entries()); assert_eq!(tree_2.head(), Some(commit_0)); assert_eq!(tree_2.dir_entries(), git.tree(commit_0).dir_entries()); let a_1 = tree_1.open_text_file("a").wait().unwrap(); let a_2 = tree_2.open_text_file("a").wait().unwrap(); observer_1.opened_buffer(a_1, &tree_1); observer_2.opened_buffer(a_2, &tree_2); assert_eq!(tree_1.text_str(a_1), git.tree(commit_0).text_str(a_base)); assert_eq!(tree_2.text_str(a_2), git.tree(commit_0).text_str(a_base)); let ops_1 = open_envelopes(tree_1.reset(Some(commit_1)).collect().wait().unwrap()); let fixup_ops_2 = tree_2.apply_ops(ops_1).unwrap().collect().wait().unwrap(); assert!(fixup_ops_2.is_empty()); assert_eq!(tree_1.head(), Some(commit_1)); assert_eq!(tree_2.head(), Some(commit_1)); assert_eq!(tree_1.entries(), tree_2.entries()); assert_eq!(tree_1.dir_entries(), git.tree(commit_1).dir_entries()); assert_eq!(tree_1.text_str(a_1), git.tree(commit_1).text_str(a_1)); assert_eq!(observer_1.text(a_1), tree_1.text_str(a_1)); assert_eq!(tree_2.text_str(a_2), git.tree(commit_1).text_str(a_2)); assert_eq!(observer_2.text(a_2), tree_2.text_str(a_2)); let ops_2 = open_envelopes(tree_2.reset(Some(commit_2)).collect().wait().unwrap()); let fixup_ops_1 = tree_1 .apply_ops(ops_2.clone()) .unwrap() .collect() .wait() .unwrap(); assert!(fixup_ops_1.is_empty()); assert_eq!(tree_1.head(), Some(commit_2)); assert_eq!(tree_2.head(), Some(commit_2)); assert_eq!(tree_1.entries(), tree_2.entries()); assert_eq!(tree_1.dir_entries(), git.tree(commit_2).dir_entries()); assert_eq!(tree_1.text_str(a_1), git.tree(commit_2).text_str(a_1)); assert_eq!(observer_1.text(a_1), tree_1.text_str(a_1)); assert_eq!(tree_2.text_str(a_2), git.tree(commit_2).text_str(a_2)); assert_eq!(observer_2.text(a_2), tree_2.text_str(a_2)); // Reload tree using only ops for the newest epoch. let (mut tree_1, ops_1) = WorkTree::new( Uuid::from_u128(1), Some(commit_0), ops_2, git.clone(), Some(observer_1.clone()), ) .unwrap(); assert!(ops_1.wait().next().is_none()); assert_eq!(tree_1.head(), Some(commit_2)); let ops_1 = open_envelopes(tree_1.reset(Some(commit_0)).collect().wait().unwrap()); let fixup_ops_2 = tree_2.apply_ops(ops_1).unwrap().collect().wait().unwrap(); assert!(fixup_ops_2.is_empty()); assert_eq!(tree_1.head(), Some(commit_0)); assert_eq!(tree_2.head(), Some(commit_0)); } #[test] fn test_selections_across_resets() { let git = Rc::new(TestGitProvider::new()); let base_tree = WorkTree::empty(); base_tree.create_file("a", FileType::Text).unwrap(); let a_base = base_tree.open_text_file("a").wait().unwrap(); base_tree.edit(a_base, Some(0..0), "def\njkl").unwrap(); let commit_0 = git.commit(&base_tree); base_tree.edit(a_base, Some(0..0), "abc\n").unwrap(); base_tree.edit(a_base, Some(8..8), "ghi\n").unwrap(); let commit_1 = git.commit(&base_tree); let (mut tree_1, ops_1) = WorkTree::new( Uuid::from_u128(1), Some(commit_0), vec![], git.clone(), None, ) .unwrap(); let (mut tree_2, ops_2) = WorkTree::new( Uuid::from_u128(2), Some(commit_0), open_envelopes(ops_1.collect().wait().unwrap()), git.clone(), None, ) .unwrap(); assert!(ops_2.wait().next().is_none()); let a_1 = tree_1.open_text_file("a").wait().unwrap(); let (a_1_set, a_1_set_op) = tree_1 .add_selection_set(a_1, vec![Point::new(1, 1)..Point::new(1, 1)]) .unwrap(); let a_2 = tree_2.open_text_file("a").wait().unwrap(); let (a_2_set, a_2_set_op) = tree_2 .add_selection_set(a_2, vec![Point::new(0, 0)..Point::new(0, 0)]) .unwrap(); tree_1 .apply_ops(Some(a_2_set_op.operation)) .unwrap() .collect() .wait() .unwrap(); let tree_1_selections = tree_1.selection_ranges(a_1).unwrap(); assert_eq!( tree_1_selections.local.into_iter().collect::>(), vec![(a_1_set, vec![Point::new(1, 1)..Point::new(1, 1)])] ); assert_eq!( tree_1_selections.remote.into_iter().collect::>(), vec![( tree_2.replica_id(), vec![vec![Point::new(0, 0)..Point::new(0, 0)]] )] ); tree_2 .apply_ops(Some(a_1_set_op.operation)) .unwrap() .collect() .wait() .unwrap(); let tree_2_selections = tree_2.selection_ranges(a_2).unwrap(); assert_eq!( tree_2_selections.local.into_iter().collect::>(), vec![(a_2_set, vec![Point::new(0, 0)..Point::new(0, 0)])] ); assert_eq!( tree_2_selections.remote.into_iter().collect::>(), vec![( tree_1.replica_id(), vec![vec![Point::new(1, 1)..Point::new(1, 1)]] )] ); let fixup_ops_1 = tree_1.reset(Some(commit_1)).collect().wait().unwrap(); let tree_1_selections = tree_1.selection_ranges(a_1).unwrap(); assert_eq!( tree_1_selections.local.into_iter().collect::>(), vec![(a_1_set, vec![Point::new(3, 1)..Point::new(3, 1)])] ); assert_eq!( tree_1_selections.remote.into_iter().collect::>(), vec![] ); let fixup_ops_2 = tree_2 .apply_ops(open_envelopes(fixup_ops_1)) .unwrap() .collect() .wait() .unwrap(); let tree_2_selections = tree_2.selection_ranges(a_2).unwrap(); assert_eq!( tree_2_selections.local.into_iter().collect::>(), vec![(a_2_set, vec![Point::new(0, 0)..Point::new(0, 0)])] ); assert_eq!( tree_2_selections.remote.into_iter().collect::>(), vec![( tree_1.replica_id(), vec![vec![Point::new(3, 1)..Point::new(3, 1)]] )] ); tree_1 .apply_ops(open_envelopes(fixup_ops_2)) .unwrap() .collect() .wait() .unwrap(); let tree_1_selections = tree_1.selection_ranges(a_1).unwrap(); assert_eq!( tree_1_selections.local.into_iter().collect::>(), vec![(a_1_set, vec![Point::new(3, 1)..Point::new(3, 1)])] ); assert_eq!( tree_1_selections.remote.into_iter().collect::>(), vec![( tree_2.replica_id(), vec![vec![Point::new(0, 0)..Point::new(0, 0)]] )] ); } #[test] fn test_active_location_across_resets() { let git = Rc::new(TestGitProvider::new()); let base_tree = WorkTree::empty(); base_tree.create_file("a", FileType::Text).unwrap(); base_tree.create_file("b", FileType::Text).unwrap(); base_tree.create_file("c", FileType::Text).unwrap(); let commit_0 = git.commit(&base_tree); base_tree.create_file("d", FileType::Text).unwrap(); base_tree.create_file("e", FileType::Text).unwrap(); let commit_1 = git.commit(&base_tree); let replica_1_id = Uuid::from_u128(1); let (mut tree_1, ops_1) = WorkTree::new(replica_1_id, Some(commit_0), vec![], git.clone(), None).unwrap(); let replica_2_id = Uuid::from_u128(2); let (mut tree_2, ops_2) = WorkTree::new( replica_2_id, Some(commit_0), open_envelopes(ops_1.collect().wait().unwrap()), git.clone(), None, ) .unwrap(); assert!(ops_2.wait().next().is_none()); let a_1 = tree_1.open_text_file("a").wait().unwrap(); let tree_1_location_op = tree_1.set_active_location(Some(a_1)).unwrap().operation; tree_2 .apply_ops(Some(tree_1_location_op)) .unwrap() .collect() .wait() .unwrap(); let b_2 = tree_2.open_text_file("b").wait().unwrap(); let tree_2_location_op = tree_2.set_active_location(Some(b_2)).unwrap().operation; tree_1 .apply_ops(Some(tree_2_location_op)) .unwrap() .collect() .wait() .unwrap(); assert_eq!(tree_1.replica_location(replica_1_id).unwrap(), "a"); assert_eq!(tree_1.replica_location(replica_2_id).unwrap(), "b"); assert_eq!(tree_2.replica_location(replica_1_id).unwrap(), "a"); assert_eq!(tree_2.replica_location(replica_2_id).unwrap(), "b"); let fixup_ops_1 = tree_1.reset(Some(commit_1)).collect().wait().unwrap(); assert_eq!(tree_1.replica_location(replica_1_id).unwrap(), "a"); let fixup_ops_2 = tree_2 .apply_ops(open_envelopes(fixup_ops_1)) .unwrap() .collect() .wait() .unwrap(); tree_1 .apply_ops(open_envelopes(fixup_ops_2)) .unwrap() .collect() .wait() .unwrap(); assert_eq!(tree_1.replica_location(replica_1_id).unwrap(), "a"); assert_eq!(tree_1.replica_location(replica_2_id).unwrap(), "b"); assert_eq!(tree_2.replica_location(replica_1_id).unwrap(), "a"); assert_eq!(tree_2.replica_location(replica_2_id).unwrap(), "b"); } #[test] fn test_exists() { let git = Rc::new(TestGitProvider::new()); let commit = git.commit(&WorkTree::empty()); let (tree, ops) = WorkTree::new(Uuid::from_u128(1), Some(commit), vec![], git.clone(), None).unwrap(); ops.collect().wait().unwrap(); tree.create_file("a", FileType::Directory).unwrap(); tree.create_file("a/b", FileType::Directory).unwrap(); tree.create_file("a/b/c", FileType::Text).unwrap(); tree.create_file("a/b/d", FileType::Text).unwrap(); tree.remove("a/b/d").unwrap(); assert!(tree.exists("a")); assert!(tree.exists("a/b")); assert!(tree.exists("a/b/c")); assert!(!tree.exists("a/b/d")); assert!(!tree.exists("non-existent-path")); assert!(!tree.exists("invalid-path-;.'")); } #[test] fn test_version() { let git = Rc::new(TestGitProvider::new()); let base_tree = WorkTree::empty(); base_tree.create_file("a", FileType::Text).unwrap(); let a_base = base_tree.open_text_file("a").wait().unwrap(); base_tree.edit(a_base, Some(0..0), "abc").unwrap(); let commit_0 = git.commit(&base_tree); base_tree.edit(a_base, Some(1..2), "def").unwrap(); base_tree.create_file("b", FileType::Directory).unwrap(); let commit_1 = git.commit(&base_tree); base_tree.edit(a_base, Some(2..3), "ghi").unwrap(); base_tree.create_file("b/c", FileType::Text).unwrap(); let commit_2 = git.commit(&base_tree); let (mut tree_1, ops_1) = WorkTree::new( Uuid::from_u128(1), Some(commit_0), vec![], git.clone(), None, ) .unwrap(); let (mut tree_2, ops_2) = WorkTree::new( Uuid::from_u128(2), Some(commit_0), open_envelopes(ops_1.collect().wait().unwrap()), git.clone(), None, ) .unwrap(); assert!(ops_2.wait().next().is_none()); let ops_1 = open_envelopes(tree_1.create_file("x.txt", FileType::Text)); let ops_2 = open_envelopes(tree_2.create_file("y.txt", FileType::Text)); assert!(!tree_1.observed(tree_2.version())); assert!(!tree_2.observed(tree_1.version())); tree_1.apply_ops(ops_2).unwrap().collect().wait().unwrap(); assert!(tree_1.observed(tree_2.version())); tree_2.apply_ops(ops_1).unwrap().collect().wait().unwrap(); assert!(tree_2.observed(tree_1.version())); let ops_1 = open_envelopes(tree_1.reset(Some(commit_1)).collect().wait().unwrap()); let ops_2 = open_envelopes(tree_2.reset(Some(commit_2)).collect().wait().unwrap()); // Even though the two sites haven't exchanged operations yet, it's as if tree_2 has // already observed tree_1's state, since it won't ever go back to an epoch whose Lamport // timestamp is smaller. assert!(!tree_1.observed(tree_2.version())); assert!(tree_2.observed(tree_1.version())); tree_1.apply_ops(ops_2).unwrap().collect().wait().unwrap(); assert!(tree_1.observed(tree_2.version())); tree_2.apply_ops(ops_1).unwrap().collect().wait().unwrap(); assert!(tree_2.observed(tree_1.version())); } fn open_envelopes>(envelopes: I) -> Vec { envelopes.into_iter().map(|e| e.operation).collect() } fn serialize_ops>(ops: I) -> Vec> { ops.into_iter().map(|op| op.serialize()).collect() } fn deserialize_ops>>(ops: I) -> Vec { ops.into_iter() .map(|op| Operation::deserialize(&op).unwrap().unwrap()) .collect() } #[derive(Clone, Debug, Eq, PartialEq)] struct BufferSelections { local: HashMap>, remote: HashMap>>, } impl WorkTree { fn empty() -> Self { let (tree, _) = Self::new( Uuid::from_u128(999 as u128), None, Vec::new(), Rc::new(TestGitProvider::new()), None, ) .unwrap(); tree } fn entries(&self) -> Vec { self.cur_epoch().entries() } fn dir_entries(&self) -> Vec { self.cur_epoch().dir_entries() } fn open_buffers(&self) -> Vec { self.buffers.borrow().keys().cloned().collect() } fn text_str(&self, buffer_id: BufferId) -> String { self.text(buffer_id).unwrap().into_string() } fn randomly_mutate(&self, rng: &mut T, count: usize) -> Vec { // Store version for all open buffers so that we can keep the observer up to date. let mut buffer_versions = Vec::new(); let buffers = self.buffers.borrow(); for (buffer_id, file_id) in buffers.iter() { let version = self.cur_epoch().buffer_version(*file_id).unwrap(); buffer_versions.push((*buffer_id, *file_id, version)); } let operations = self.cur_epoch_mut().randomly_mutate( rng, &mut self.lamport_clock.borrow_mut(), count, ); self.update_local_selection_sets(); // Apply the random changes to the observer as well so that it matches what's in the tree. if let Some(observer) = self.observer.as_ref() { for (buffer_id, file_id, version) in buffer_versions { let text_changes = self .cur_epoch() .changes_since(file_id, &version) .unwrap() .collect(); observer.changed( buffer_id, text_changes, self.selection_ranges(buffer_id).unwrap(), ); } } OperationEnvelope::wrap_many(self.cur_epoch().id, self.cur_epoch().head, operations) } fn open_random_buffers( &mut self, rng: &mut T, observer: &TestChangeObserver, count: usize, ) { for _ in 0..rng.gen_range(0, count) { if let Some(path) = self.select_path(rng, FileType::Text) { let buffer_id = self.open_text_file(path).wait().unwrap(); self.update_local_selection_sets(); observer.opened_buffer(buffer_id, self); } } } fn visible_paths(&self, file_type: FileType) -> Vec { let mut visible_paths = Vec::new(); self.with_cursor(|cursor| loop { let entry = cursor.entry().unwrap(); let advanced = if entry.visible { if file_type == entry.file_type { visible_paths.push(cursor.path().unwrap().to_path_buf()); } cursor.next(true) } else { cursor.next(false) }; if !advanced { break; } }); visible_paths } fn select_path(&self, rng: &mut T, file_type: FileType) -> Option { let mut visible_paths = self.visible_paths(file_type); if visible_paths.is_empty() { None } else { Some(visible_paths.swap_remove(rng.gen_range(0, visible_paths.len()))) } } fn update_local_selection_sets(&self) { use std::collections::HashSet; let mut local_selection_sets = self.local_selection_sets.borrow_mut(); for (buffer_id, file_id) in self.buffers.borrow().iter() { let buffer_sets = local_selection_sets .entry(*buffer_id) .or_insert(HashMap::new()); for local_set_id in buffer_sets.keys().cloned().collect::>() { let set_id = buffer_sets[&local_set_id]; match self.cur_epoch().selection_ranges(*file_id, set_id) { Ok(_) => {} Err(Error::InvalidSelectionSet(_)) => { buffer_sets.remove(&local_set_id); } Err(error) => panic!("{:?}", error), } } let buffer_set_ids = buffer_sets.values().cloned().collect::>(); for (set_id, _) in self.cur_epoch().all_selections(*file_id).unwrap() { if set_id.replica_id == self.replica_id() && !buffer_set_ids.contains(&set_id) { buffer_sets.insert(self.gen_local_set_id(), set_id); } } } } fn replica_location(&self, replica_id: ReplicaId) -> Option { self.replica_locations() .get(&replica_id) .map(|path| path.to_string_lossy().into_owned()) } } struct TestGitProvider { commits: RefCell>, next_oid: RefCell, } struct TestChangeObserver { buffers: RefCell>, local_clock: RefCell, lamport_clock: RefCell, selections: RefCell>, } impl TestGitProvider { fn new() -> Self { TestGitProvider { commits: RefCell::new(HashMap::new()), next_oid: RefCell::new(0), } } fn commit(&self, tree: &WorkTree) -> Oid { let mut tree_clone = WorkTree::empty(); tree_clone.epoch = tree .epoch .as_ref() .map(|e| Rc::new(RefCell::new(e.borrow().clone()))); tree_clone.buffers = Rc::new(RefCell::new(tree.buffers.borrow().clone())); let oid = self.gen_oid(); self.commits.borrow_mut().insert(oid, tree_clone); oid } fn tree(&self, oid: Oid) -> Ref { Ref::map(self.commits.borrow(), |commits| commits.get(&oid).unwrap()) } fn gen_oid(&self) -> Oid { let mut next_oid = self.next_oid.borrow_mut(); let mut oid = [0; 20]; oid[0] = (*next_oid >> 0) as u8; oid[1] = (*next_oid >> 8) as u8; oid[2] = (*next_oid >> 16) as u8; oid[3] = (*next_oid >> 24) as u8; oid[4] = (*next_oid >> 32) as u8; oid[5] = (*next_oid >> 40) as u8; oid[6] = (*next_oid >> 48) as u8; oid[7] = (*next_oid >> 56) as u8; *next_oid += 1; oid } } impl GitProvider for TestGitProvider { fn base_entries(&self, oid: Oid) -> Box> { match self.commits.borrow().get(&oid) { Some(tree) => Box::new(stream::iter_ok(tree.dir_entries().into_iter())), None => Box::new(stream::once(Err(io::Error::new( io::ErrorKind::Other, "Commit does not exist", )))), } } fn base_text( &self, oid: Oid, path: &Path, ) -> Box> { use futures::IntoFuture; Box::new( self.commits .borrow_mut() .get_mut(&oid) .ok_or(io::Error::new( io::ErrorKind::Other, "Commit does not exist", )) .and_then(|tree| { tree.open_text_file(path) .wait() .map_err(|_| { io::Error::new(io::ErrorKind::Other, "Path does not exist") }) .map(|buffer_id| tree.text(buffer_id).unwrap().into_string()) }) .into_future(), ) } } impl TestChangeObserver { fn new() -> Self { Self { buffers: RefCell::new(HashMap::new()), local_clock: RefCell::new(time::Local::default()), lamport_clock: RefCell::new(time::Lamport::default()), selections: RefCell::new(HashMap::new()), } } fn opened_buffer(&self, buffer_id: BufferId, tree: &WorkTree) { let text = tree.text(buffer_id).unwrap().collect::>(); self.buffers .borrow_mut() .insert(buffer_id, buffer::Buffer::new(text)); self.selections .borrow_mut() .insert(buffer_id, tree.selection_ranges(buffer_id).unwrap()); } fn text(&self, buffer_id: BufferId) -> String { self.buffers.borrow().get(&buffer_id).unwrap().to_string() } fn selection_ranges(&self, buffer_id: BufferId) -> BufferSelectionRanges { self.selections.borrow().get(&buffer_id).unwrap().clone() } } impl ChangeObserver for TestChangeObserver { fn changed( &self, buffer_id: BufferId, changes: Vec, selections: BufferSelectionRanges, ) { if let Some(buffer) = self.buffers.borrow_mut().get_mut(&buffer_id) { for change in changes { buffer.edit_2d( Some(change.range), change.code_units, &mut self.local_clock.borrow_mut(), &mut self.lamport_clock.borrow_mut(), ); } } self.selections.borrow_mut().insert(buffer_id, selections); } } } ================================================ FILE: memo_js/.npmignore ================================================ script test Cargo.toml README.md node_modules target webpack.config.js ================================================ FILE: memo_js/.nvmrc ================================================ 11.9.0 ================================================ FILE: memo_js/Cargo.toml ================================================ [package] name = "memo_js" version = "0.1.0" authors = ["Antonio Scandurra ", "Nathan Sobo "] edition = "2018" [lib] crate-type = ["cdylib"] [dependencies] bincode = "1.0" console_error_panic_hook = "0.1" futures = "0.1" hex = "0.3" js-sys = "0.3" memo_core = { path = "../memo_core" } serde = "1.0" serde_derive = "1.0" wasm-bindgen-futures = "0.3" [dependencies.wasm-bindgen] version = "0.2.33" features = ["serde-serialize"] ================================================ FILE: memo_js/README.md ================================================ # Memo JS Memo allows multiple remote collaborators to share the state of a single Git working copy. The core of the library is written in Rust for efficiency and reusability in other contexts. This library exposes the capabilities of the Rust core via WebAssembly and wraps them in an idiomatic JavaScript API. ## Creating a WorkTree `WorkTree` is the fundamental abstraction provided by this library. The state of a `WorkTree` is expressed as a sequence of fine-grained _operations_ applied on top of a a _base commit_. There are two possible cases when constructing a new `WorkTree`: - We are the first collaborator, and we want to build future operations on top of a given base commit. - We are joining an existing collaborative session, and we want to construct the `WorkTree` from a sequence of existing operations. Both scenarios are automatically handled when you call `WorkTree.create`: ```ts const replicaId = generateUUID(); const baseCommitOID = "8251a3c491b3884d7f828d2a1c5c565855171a2c"; const startOps = await fetchInitialOperations(); const [tree, ops] = await WorkTree.create( replicaId, baseCommitOID, startOps, gitProvider ); broadcast(ops); ``` In the example above, `WorkTree.create` is called with a replica id (`replicaId`), a base commit (`baseCommitOID`) and an array of existing operations (`startOps`). If the existing operations array is _empty_, we assume this is the first collaborator and initialize the tree at the provided base commit. If operations are provided, the `baseCommitOID` argument is ignored and the current base commit is determined from the given operations. You can ignore how `fetchInitialOperations` works for now. It is not included as part of this library, and a reference implementation will be covered later in the guide. The third parameter to `WorkTree.create` is an object that implements the `GitProvider` interface: ```ts export interface GitProvider { baseEntries(oid: Oid): AsyncIterable; baseText(oid: Oid, path: Path): Promise; } ``` This provider allows the `WorkTree` to retrieve information from the underlying Git repository. Here's a potential implementation that reads data from GitHub: ```ts class GitHubProvider implements GitProvider { async *baseEntries(oid: Oid): AsyncIterable { const entries = await fetch( `/repos/rust-lang/rust/git/trees/${oid}?recursive=1"` ); for (const entry of entries) { yield fromGitHubEntryToBaseEntry(entry); } } async baseText(oid: Oid, path: Path): Promise { const file = await fetch( `/repos/rust-lang/rust/contents/${path}?ref=${oid}` ); return fromBase64ToString(file.content); } } ``` The `baseEntries` method must return a collection that can be asynchronously iterated over and that yields `memo.BaseEntry` elements, like the following: ```ts { depth: 1, name: "a", type: memo.FileType.Directory } { depth: 2, name: "b.txt", type: memo.FileType.Text } { depth: 1, name: "c.txt", type: memo.FileType.Text } ``` ## Listing the work tree's current entries To list the work tree's current paths, call `entries`. This will return an array of entries arranged in a depth-first order, similar to the entries returned by `GitProvider.prototype.baseEntries`. For example, the base entries populated above could be retrieved as follows: ```ts for (const entry of tree.entries()) { console.log(entry.depth, entry.name, entry.type); } // Prints: // 1 a Directory // 2 b.txt File // 1 c.txt File ``` Each returned entry has the following fields: - `depth`: The length of the path leading to this entry. - `name`: The entry's name. - `path`: The entry's path. - `basePath`: The entry's original path at the beginning of the commit. - `type`: The type of this file (`"File"` or `"Directory"`) - `status`: How this path has changed since the base commit (`"New"`, `"Renamed"`, `"Removed"`, `"Modified"`, `"RenamedAndModified"`, or `"Unchanged"`) - `visible`: Whether or not this file is currently visible (not deleted). The `entries` method accepts two options as fields in an optional object passed to the method. - `showDeleted`: If `true`, returns entries for deleted files and directories, but marks them as `visible: false`. - `descendInto`: An optional array of paths. If provided, the traversal will skip descending into any directory not present in this whitelist. You can use this option to limit the number of entries you need to process if you are rendering a UI with collapsed directories. ## Creating, renaming and removing files `WorkTree` APIs all function in terms of paths and allow you to manipulate files exactly as you would expect from a typical file system: ```ts const op1 = tree.createFile("foo", memo.FileType.Directory); const op2 = tree.createFile("foo/bar", memo.FileType.Text); const op3 = tree.createFile("foo/baz", memo.FileType.Text); const op4 = tree.rename("foo/bar", "foo/qux"); const op5 = tree.remove("foo/baz"); broadcast([op1, op2, op3, op4, op5]); ``` ## Reading and manipulating text files To manipulate text files you'll need to call `openTextFile` with the path you want to open. This method will return a `Buffer` object that you can interact with: ```ts const buffer = await tree.openTextFile("foo/qux"); const editOp1 = buffer.edit( [{ start: { row: 0, column: 0 }, end: { row: 0, column: 0 } }], "Hello, world!" ); const editOp2 = buffer.edit( [{ start: { row: 0, column: 10 }, end: { row: 0, column: 12 } }], "ms" ); console.log(buffer.getText()); // ==> "Hello worms" const [set, createSetOp] = buffer.addSelectionSet([ { start: point(0, 0), end: point(0, 1) } ]); const replaceSetOp = buffer.replaceSelectionSet(set, [ { start: point(0, 2), end: point(0, 3) } ]); console.log(buffer.getSelections()); /* => { local: { 1: [{ start: { row: 0, column: 2 }, end: { row: 0, column: 3 } }] }, remote: { "65242244-9706-4b42-9785-fa5cbe5d5709": [ [{ start: { row: 0, column: 2 }, end: { row: 0, column: 3 } }] ] } }*/ const removeSetOp = buffer.removeSelectionSet(set); broadcast([editOp1, editOp2, createSetOp, replaceSetOp, removeSetOp]); ``` As you incorporate operations received from other peers, you may want to use `Buffer.prototype.onChange` to keep an external representation of the buffer up-to-date: ```ts buffer.onChange(change => { for (const textChange of change.textChanges) { console.log(textChange); // => { start: { row: 0, column: 0 }, end: { row: 0, column: 5 }, text: "Goodbye" } externalBuffer.edit(textChange.start, textChange.end, textChange.text); } console.log(change.selectionRanges); /* => { local: { 1: [{ start: { row: 0, column: 2 }, end: { row: 0, column: 3 } }] }, remote: { "65242244-9706-4b42-9785-fa5cbe5d5709": [ [{ start: { row: 0, column: 2 }, end: { row: 0, column: 3 } }] ] } }*/ }); ``` ## Changing the active location Optionally, you can also retrieve the location of other peers and transmit yours using the location API: ```ts const operation = tree.setActiveLocation(buffer); broadcast([operation]); console.log(tree.getReplicaLocations()); /* => { "65242244-9706-4b42-9785-fa5cbe5d5709": "foo/qux" }*/ ``` ## Resetting to a different base commit If you want to reset the work tree to a different (possibly `null`) base (e.g. after a commit or a `git reset`), you can use the `reset` method: ```ts const commitOid = "70403cdf91c2e6fbf76167f725935e6b0993eeb1"; const resetOps = tree.reset(commitOid); await broadcast(resetOps); console.log(tree.head()); // => 70403cdf91c2e6fbf76167f725935e6b0993eeb1 ``` This resets you and all the other peers to the new commit. Note that this is an asynchronous action, as the tree needs to perform I/O in order to retrieve the new base entries. After switching to a new base all open buffers will still be valid and you can continue using them normally. ## Working with operations All methods that update the state of the tree return _operations_, the fundamental primitive this library uses to synchronize with other peers. Sometimes operations are returned synchronously, sometimes they are async iterators instead. Make sure you handle both cases, as illustrated in the `broadcast` function later in this section. In either case, operations are wrapped in an `OperationEnvelope`. An operation envelope is defined as follows: ```ts export interface OperationEnvelope { epochId(): Uint8Array; epochTimestamp(): number; epochReplicaId(): string; operation(): Operation; } ``` Technically, to synchronize with other peers, you only need to transmit the operation that is stored inside of the envelope; so, why including those extra timestamp and replica id fields? You may recall the `fetchInitialOperations` function that we called when [creating a new `WorkTree`](#creating-a-worktree). It turns out that, in order to instantiate a new `WorkTree`, you only need operations associated with the _latest_ epoch. By exposing the epoch timestamp and replica id, we allow you to store operations such that they can be efficiently queried later when instantiating new work trees: ```ts // Here we simulate having a database that stores every operation that has been // generated. async function fetchInitialOperations(): Operation[] { // Note that this is very inefficient. In a production system, you should // perform the computation contained in this function on the database, using // an index on the (timestamp, replicaId) tuple. const allEnvelopes = await database.getAllOperationEnvelopes(); // First we sort by timestamp, then by replica id. const sortedEnvelopes = database .getAllOperationEnvelopes() .sort( (a, b) => a.epochTimestamp() - b.epochTimestamp() || a.epochReplicaId() - b.epochReplicaId() ); // Then, we only retrieve operations for the latest epoch. const lastEnvelope = sortedEnvelopes[sortedEnvelopes.length - 1]; const latestEpochEnvelopes = sortedEnvelopes.filter( e => e.epochTimestamp() == lastEnvelope.epochTimestamp() && e.epochReplicaId() == lastEnvelope.epochReplicaId() ); // Finally, we unwrap the envelopes and just return the operations inside. return latestEpochEnvelopes.map(envelope => envelope.operation()); } async function broadcast( envelopes: OperationEnvelope[] | AsyncIterable ) { for await (const envelope of envelopes) { // Note how we store the full envelope in the database, but we only transmit // the operation inside of it to peers. database.store(envelope); network.broadcast(envelope.operation()); } } ``` So far we have covered storing and trasmitting operations sent by the local replica. To apply remote operations, you should use the `applyOps` method: ```ts const remoteOps = await receiveOps(); const fixupOps = tree.applyOps(remoteOps); broadcast(fixupOps); ``` Whenever you call `applyOps`, there is a chance that additional "fixup" operations could be generated to deal with cycles and name conflicts in the tree. Be sure to broadcast these operations to peers to ensure convergence. ================================================ FILE: memo_js/package.json ================================================ { "name": "@atom/memo", "version": "0.21.0", "main": "dist/index.node.js", "browser": "dist/index.js", "types": "dist/index.d.ts", "scripts": { "prepublishOnly": "script/build", "test": "webpack --env.test && mocha --ui=tdd --require=source-map-support/register test/dist/tests.js", "tsc": "tsc", "webpack": "webpack" }, "license": "MIT", "devDependencies": { "@types/mocha": "^5.2.5", "@types/node": "^10.11.4", "@types/uuid": "^3.4.4", "@types/uuid-parse": "^1.0.0", "mocha": "^5.2.0", "source-map-support": "^0.5.9", "ts-loader": "^5.2.0", "typescript": "^3.0.3", "uuid": "^3.3.2", "uuid-parse": "^1.0.0", "webpack": "^4.20.2", "webpack-cli": "^3.1.1" } } ================================================ FILE: memo_js/rustfmt.toml ================================================ edition = "2018" ================================================ FILE: memo_js/script/build ================================================ #!/usr/bin/env bash set -e LOCAL_CRATE_PATH=./.cargo PATH=$LOCAL_CRATE_PATH/bin:$PATH WASM_BINDGEN_VERSION=0.2.33 setup_wasm_bindgen() { if (command -v wasm-bindgen) && $(wasm-bindgen --version | grep --silent $WASM_BINDGEN_VERSION); then echo 'Using existing installation of wasm-bindgen' else cargo install --force wasm-bindgen-cli --version $WASM_BINDGEN_VERSION --root $LOCAL_CRATE_PATH fi } rustup target add wasm32-unknown-unknown setup_wasm_bindgen rm -rf dist mkdir -p dist CARGO_INCREMENTAL=0 RUSTFLAGS="-C debuginfo=0 -C opt-level=s -C lto -C panic=abort" cargo build --release --target wasm32-unknown-unknown wasm-bindgen ../target/wasm32-unknown-unknown/release/memo_js.wasm --out-dir dist yarn tsc yarn webpack --mode=production ================================================ FILE: memo_js/src/index.ts ================================================ export { BaseEntry, Change, GitProvider, FileType, Oid, Path, Point, Range, ReplicaId, SelectionRanges, SelectionSetId } from "./support"; import { BufferId, ChangeObserver, ChangeObserverCallback, Disposable, GitProvider, GitProviderWrapper, FileType, Oid, Path, Range, ReplicaId, SelectionRanges, SelectionSetId, Tagged, fromMemoSelectionRanges } from "./support"; let memo: any; async function init() { if (!memo) { memo = await import("../dist/memo_js"); memo.StreamToAsyncIterator.prototype[Symbol.asyncIterator] = function() { return this; }; } } export type Version = Tagged; export type Operation = Tagged; export type EpochId = Tagged; export interface OperationEnvelope { epochId(): EpochId; epochTimestamp(): number; epochReplicaId(): ReplicaId; epochHead(): null | Oid; operation(): Operation; isSelectionUpdate(): boolean; } export enum FileStatus { New = "New", Renamed = "Renamed", Removed = "Removed", Modified = "Modified", RenamedAndModified = "RenamedAndModified", Unchanged = "Unchanged" } export interface Entry { readonly depth: number; readonly type: FileType; readonly name: string; readonly path: Path; readonly basePath: Path | null; readonly status: FileStatus; readonly visible: boolean; } export class WorkTree { private tree: any; private observer: ChangeObserver; private buffers: Map = new Map(); static async create( replicaId: string, base: Oid | null, startOps: ReadonlyArray, git: GitProvider ): Promise<[WorkTree, AsyncIterable]> { await init(); const observer = new ChangeObserver(); const result = memo.WorkTree.new( new GitProviderWrapper(git), observer, replicaId, base, startOps ); return [new WorkTree(result.tree(), observer), result.operations()]; } private constructor(tree: any, observer: ChangeObserver) { this.tree = tree; this.observer = observer; } version(): Version { return this.tree.version(); } hasObserved(version: Version): boolean { return this.tree.observed(version); } head(): null | Oid { return this.tree.head(); } epochId(): EpochId { return this.tree.epoch_id(); } reset(base: Oid | null): AsyncIterable { return this.tree.reset(base); } applyOps(ops: Operation[]): AsyncIterable { return this.tree.apply_ops(ops); } createFile(path: Path, fileType: FileType): OperationEnvelope { return this.tree.create_file(path, fileType); } rename(oldPath: Path, newPath: Path): OperationEnvelope { return this.tree.rename(oldPath, newPath); } remove(path: Path): OperationEnvelope { return this.tree.remove(path); } exists(path: Path): boolean { return this.tree.exists(path); } entries(options?: { descendInto?: Path[]; showDeleted?: boolean }): Entry[] { let descendInto = null; let showDeleted = false; if (options) { if (options.descendInto) descendInto = options.descendInto; if (options.showDeleted) showDeleted = options.showDeleted; } return this.tree.entries(descendInto, showDeleted); } async openTextFile(path: Path): Promise { const bufferId = await this.tree.open_text_file(path); let buffer = this.buffers.get(bufferId); if (!buffer) { buffer = new Buffer(bufferId, this.tree, this.observer); this.buffers.set(bufferId, buffer); } return buffer; } setActiveLocation(buffer: Buffer | null): OperationEnvelope { return this.tree.set_active_location(buffer ? buffer.id : null); } getReplicaLocations(): Map { const locations = this.tree.replica_locations(); const map = new Map(); for (const replicaId in locations) { map.set(replicaId as ReplicaId, locations[replicaId] as Path); } return map; } } export class Buffer { id: BufferId; private tree: any; private observer: ChangeObserver; constructor(id: BufferId, tree: any, observer: ChangeObserver) { this.id = id; this.tree = tree; this.observer = observer; } edit(oldRanges: Range[], newText: string): OperationEnvelope { return this.tree.edit(this.id, oldRanges, newText); } addSelectionSet(ranges: Range[]): [SelectionSetId, OperationEnvelope] { const result = this.tree.add_selection_set(this.id, ranges); return [result.set_id(), result.operation()]; } replaceSelectionSet(id: SelectionSetId, ranges: Range[]): OperationEnvelope { return this.tree.replace_selection_set(this.id, id, ranges); } removeSelectionSet(id: SelectionSetId): OperationEnvelope { return this.tree.remove_selection_set(this.id, id); } getPath(): string | null { return this.tree.path(this.id); } getText(): string { return this.tree.text(this.id); } getSelectionRanges(): SelectionRanges { const selections = this.tree.selection_ranges(this.id); return fromMemoSelectionRanges(selections); } onChange(callback: ChangeObserverCallback): Disposable { return this.observer.onChange(this.id, callback); } getDeferredOperationCount(): number { return this.tree.buffer_deferred_ops_len(this.id); } } ================================================ FILE: memo_js/src/lib.rs ================================================ #![feature(macros_in_extern)] use bincode; use futures::{Async, Future, Poll, Stream}; use memo_core as memo; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use serde_derive::{Deserialize, Serialize}; use std::cell::Cell; use std::collections::{HashMap, HashSet}; use std::io; use std::marker::PhantomData; use std::ops::Range; use std::path::{Path, PathBuf}; use std::rc::Rc; use wasm_bindgen::{prelude::*, JsCast}; use wasm_bindgen_futures::{future_to_promise, JsFuture}; trait JsValueExt { fn into_operation(self) -> Result, JsValue>; fn into_ranges_vec(self) -> Result>, JsValue>; fn into_error_message(self) -> Result; } trait IntoJsError { fn into_js_err(self) -> JsValue; } #[wasm_bindgen] pub struct WorkTree(memo::WorkTree); #[derive(Deserialize)] struct AsyncResult { value: Option, done: bool, } struct AsyncIteratorToStream { next_value: JsFuture, iterator: AsyncIteratorWrapper, _phantom: PhantomData, } #[wasm_bindgen] pub struct StreamToAsyncIterator(Rc>>>>); #[wasm_bindgen] pub struct WorkTreeNewResult { tree: Option, operations: Option, } #[wasm_bindgen] pub struct AddSelectionSetResult { set_id: memo::LocalSelectionSetId, operation: Option, } #[wasm_bindgen] pub struct OperationEnvelope(memo::OperationEnvelope); #[derive(Serialize)] struct Change { start: memo::Point, end: memo::Point, text: String, } #[derive(Serialize)] struct Entry { #[serde(rename = "type")] file_type: memo::FileType, depth: usize, name: String, path: String, #[serde(rename = "basePath")] base_path: Option, status: memo::FileStatus, visible: bool, } #[derive(Deserialize, Serialize)] struct JsRange { start: memo::Point, end: memo::Point, } #[derive(Deserialize, Serialize)] struct JsSelections { local: HashMap>, remote: HashMap>>, } pub struct HexOid(memo::Oid); #[wasm_bindgen(module = "./support")] extern "C" { pub type AsyncIteratorWrapper; #[wasm_bindgen(method)] fn next(this: &AsyncIteratorWrapper) -> js_sys::Promise; pub type GitProviderWrapper; #[wasm_bindgen(method, js_name = baseEntries)] fn base_entries(this: &GitProviderWrapper, head: &str) -> AsyncIteratorWrapper; #[wasm_bindgen(method, js_name = baseText)] fn base_text(this: &GitProviderWrapper, head: &str, path: &str) -> js_sys::Promise; pub type ChangeObserver; #[wasm_bindgen(method)] fn changed( this: &ChangeObserver, buffer_id: JsValue, changes: JsValue, selection_ranges: JsValue, ); } #[wasm_bindgen] impl WorkTree { pub fn new( git: GitProviderWrapper, observer: ChangeObserver, replica_id: JsValue, base: JsValue, js_start_ops: js_sys::Array, ) -> Result { console_error_panic_hook::set_once(); let replica_id = replica_id.into_serde().map_err(|e| { format!("ReplicaId {:?} must be a valid UUID: {}", replica_id, e).into_js_err() })?; let base = base .into_serde::>() .map_err(|e| e.into_js_err())? .map(|b| b.0); let mut start_ops = Vec::new(); for js_op in js_start_ops.values() { if let Some(op) = js_op?.into_operation()? { start_ops.push(op); } } let (tree, operations) = memo::WorkTree::new( replica_id, base, start_ops, Rc::new(git), Some(Rc::new(observer)), ) .map_err(|e| e.into_js_err())?; Ok(WorkTreeNewResult { tree: Some(WorkTree(tree)), operations: Some(StreamToAsyncIterator::new( operations .map(|op| JsValue::from(OperationEnvelope::new(op))) .map_err(|e| e.into_js_err()), )), }) } pub fn version(&self) -> Vec { bincode::serialize(&self.0.version()).unwrap() } pub fn observed(&self, version_bytes: &[u8]) -> Result { let version = bincode::deserialize(&version_bytes).map_err(|e| e.into_js_err())?; Ok(self.0.observed(version)) } pub fn head(&self) -> JsValue { JsValue::from_serde(&self.0.head().map(|head| HexOid(head))).unwrap() } pub fn epoch_id(&self) -> Vec { self.0.epoch_id().to_bytes().to_vec() } pub fn reset(&mut self, base: JsValue) -> Result { let base = base .into_serde::>() .map_err(|e| e.into_js_err())? .map(|b| b.0); Ok(StreamToAsyncIterator::new( self.0 .reset(base) .map(|op| JsValue::from(OperationEnvelope::new(op))) .map_err(|e| e.into_js_err()), )) } pub fn apply_ops(&mut self, js_ops: js_sys::Array) -> Result { let mut ops = Vec::new(); for js_op in js_ops.values() { if let Some(op) = js_op?.into_operation()? { ops.push(op); } } self.0 .apply_ops(ops) .map(|fixup_ops| { StreamToAsyncIterator::new( fixup_ops .map(|op| JsValue::from(OperationEnvelope::new(op))) .map_err(|e| e.into_js_err()), ) }) .map_err(|e| e.into_js_err()) } pub fn create_file( &self, path: String, file_type: JsValue, ) -> Result { let file_type = file_type.into_serde().map_err(|e| e.into_js_err())?; self.0 .create_file(&path, file_type) .map(|operation| OperationEnvelope::new(operation)) .map_err(|e| e.into_js_err()) } pub fn rename(&self, old_path: String, new_path: String) -> Result { self.0 .rename(&old_path, &new_path) .map(|operation| OperationEnvelope::new(operation)) .map_err(|e| e.into_js_err()) } pub fn remove(&self, path: String) -> Result { self.0 .remove(&path) .map(|operation| OperationEnvelope::new(operation)) .map_err(|e| e.into_js_err()) } pub fn exists(&self, path: String) -> bool { self.0.exists(&path) } pub fn set_active_location(&self, buffer_id: JsValue) -> Result { let buffer_id = buffer_id.into_serde().map_err(|e| e.into_js_err())?; self.0 .set_active_location(buffer_id) .map(|operation| OperationEnvelope::new(operation)) .map_err(|e| e.into_js_err()) } pub fn replica_locations(&self) -> JsValue { JsValue::from_serde(&self.0.replica_locations()).unwrap() } pub fn open_text_file(&mut self, path: String) -> js_sys::Promise { future_to_promise( self.0 .open_text_file(path) .map(|buffer_id| JsValue::from_serde(&buffer_id).unwrap()) .map_err(|e| e.into_js_err()), ) } pub fn path(&self, buffer_id: JsValue) -> Result, JsValue> { let buffer_id = buffer_id.into_serde().map_err(|e| e.into_js_err())?; Ok(self .0 .path(buffer_id) .map(|path| path.to_string_lossy().into_owned())) } pub fn text(&self, buffer_id: JsValue) -> Result { let buffer_id = buffer_id.into_serde().map_err(|e| e.into_js_err())?; self.0 .text(buffer_id) .map(|text| JsValue::from_str(&text.into_string())) .map_err(|e| e.into_js_err()) } pub fn buffer_deferred_ops_len(&self, buffer_id: JsValue) -> Result { let buffer_id = buffer_id.into_serde().map_err(|e| e.into_js_err())?; self.0 .buffer_deferred_ops_len(buffer_id) .map(|len| len as u32) .map_err(|e| e.into_js_err()) } pub fn edit( &self, buffer_id: JsValue, old_ranges: JsValue, new_text: &str, ) -> Result { let buffer_id = buffer_id.into_serde().map_err(|e| e.into_js_err())?; let old_ranges = old_ranges.into_ranges_vec()?; self.0 .edit_2d(buffer_id, old_ranges, new_text) .map(|op| OperationEnvelope::new(op)) .map_err(|e| e.into_js_err()) } pub fn add_selection_set( &self, buffer_id: JsValue, ranges: JsValue, ) -> Result { let buffer_id = buffer_id.into_serde().map_err(|e| e.into_js_err())?; let ranges = ranges.into_ranges_vec()?; let (set_id, op) = self .0 .add_selection_set(buffer_id, ranges) .map_err(|e| e.into_js_err())?; Ok(JsValue::from(AddSelectionSetResult { set_id, operation: Some(OperationEnvelope::new(op)), })) } pub fn replace_selection_set( &self, buffer_id: JsValue, set_id: JsValue, ranges: JsValue, ) -> Result { let buffer_id = buffer_id.into_serde().map_err(|e| e.into_js_err())?; let set_id = set_id.into_serde().map_err(|e| e.into_js_err())?; let ranges = ranges.into_ranges_vec()?; let op = self .0 .replace_selection_set(buffer_id, set_id, ranges) .map_err(|e| e.into_js_err())?; Ok(OperationEnvelope::new(op)) } pub fn remove_selection_set( &self, buffer_id: JsValue, set_id: JsValue, ) -> Result { let buffer_id = buffer_id.into_serde().map_err(|e| e.into_js_err())?; let set_id = set_id.into_serde().map_err(|e| e.into_js_err())?; let op = self .0 .remove_selection_set(buffer_id, set_id) .map_err(|e| e.into_js_err())?; Ok(OperationEnvelope::new(op)) } pub fn selection_ranges(&self, buffer_id: JsValue) -> Result { let buffer_id = buffer_id.into_serde().map_err(|e| e.into_js_err())?; let selections = self .0 .selection_ranges(buffer_id) .map_err(|e| e.into_js_err())?; let js_selections = JsSelections::from(selections); Ok(JsValue::from_serde(&js_selections).unwrap()) } pub fn entries(&self, descend_into: JsValue, show_deleted: bool) -> Result { let descend_into: Option> = descend_into.into_serde().map_err(|e| e.into_js_err())?; let mut entries = Vec::new(); self.0.with_cursor(|cursor| loop { let entry = cursor.entry().unwrap(); let mut descend = false; if show_deleted || entry.status != memo::FileStatus::Removed { let path = cursor.path().unwrap(); let base_path = cursor.base_path().unwrap(); entries.push(Entry { file_type: entry.file_type, depth: entry.depth, name: entry.name.to_string_lossy().into_owned(), path: path.to_string_lossy().into_owned(), base_path: base_path.map(|p| p.to_string_lossy().into_owned()), status: entry.status, visible: entry.visible, }); descend = descend_into.as_ref().map_or(true, |d| d.contains(path)); } if !cursor.next(descend) { break; } }); JsValue::from_serde(&entries).map_err(|e| e.into_js_err()) } } #[wasm_bindgen] impl WorkTreeNewResult { pub fn tree(&mut self) -> Result { self.tree .take() .ok_or(js_sys::Error::new("Cannot take tree twice").into()) } pub fn operations(&mut self) -> Result { self.operations .take() .ok_or(js_sys::Error::new("Cannot take operations twice").into()) } } #[wasm_bindgen] impl AddSelectionSetResult { pub fn set_id(&mut self) -> JsValue { JsValue::from_serde(&self.set_id).unwrap() } pub fn operation(&mut self) -> Result { self.operation .take() .ok_or(js_sys::Error::new("Cannot take operation twice").into()) } } #[wasm_bindgen] impl OperationEnvelope { fn new(operation: memo::OperationEnvelope) -> Self { OperationEnvelope(operation) } #[wasm_bindgen(js_name = epochId)] pub fn epoch_id(&self) -> Vec { self.0.operation.epoch_id().to_bytes().to_vec() } #[wasm_bindgen(js_name = epochReplicaId)] pub fn epoch_replica_id(&self) -> JsValue { JsValue::from_serde(&self.0.operation.epoch_id().replica_id).unwrap() } #[wasm_bindgen(js_name = epochTimestamp)] pub fn epoch_timestamp(&self) -> JsValue { JsValue::from_serde(&self.0.operation.epoch_id().value).unwrap() } #[wasm_bindgen(js_name = epochHead)] pub fn epoch_head(&self) -> JsValue { JsValue::from_serde(&self.0.epoch_head.map(|head| HexOid(head))).unwrap() } pub fn operation(&self) -> Vec { self.0.operation.serialize() } #[wasm_bindgen(js_name = isSelectionUpdate)] pub fn is_selection_update(&self) -> bool { self.0.operation.is_selection_update() } } impl AsyncIteratorToStream { fn new(iterator: AsyncIteratorWrapper) -> Self { AsyncIteratorToStream { next_value: JsFuture::from(iterator.next()), iterator, _phantom: PhantomData, } } } impl Stream for AsyncIteratorToStream where T: for<'de> Deserialize<'de>, { type Item = T; type Error = String; fn poll(&mut self) -> Poll, Self::Error> { match self.next_value.poll() { Ok(Async::Ready(result)) => { let result: AsyncResult = result.into_serde().map_err(|e| e.to_string())?; if result.done { Ok(Async::Ready(None)) } else { self.next_value = JsFuture::from(self.iterator.next()); Ok(Async::Ready(result.value)) } } Ok(Async::NotReady) => Ok(Async::NotReady), Err(error) => Err(error.into_error_message()?), } } } impl StreamToAsyncIterator { fn new(stream: S) -> Self where S: 'static + Stream, { let js_value_stream = stream.map(|value| { let result = JsValue::from(js_sys::Object::new()); js_sys::Reflect::set(&result, &JsValue::from_str("value"), &value).unwrap(); js_sys::Reflect::set( &result, &JsValue::from_str("done"), &JsValue::from_bool(false), ) .unwrap(); result }); StreamToAsyncIterator(Rc::new(Cell::new(Some(Box::new(js_value_stream))))) } } #[wasm_bindgen] impl StreamToAsyncIterator { pub fn next(&mut self) -> Option { let stream_rc = self.0.clone(); self.0.take().map(|stream| { future_to_promise(stream.into_future().then(move |result| match result { Ok((next, rest)) => { stream_rc.set(Some(rest)); Ok(next.unwrap_or_else(|| { let result = JsValue::from(js_sys::Object::new()); js_sys::Reflect::set( &result, &JsValue::from_str("done"), &JsValue::from_bool(true), ) .unwrap(); result })) } Err((error, _)) => Err(error), })) }) } } impl memo::GitProvider for GitProviderWrapper { fn base_entries( &self, oid: memo::Oid, ) -> Box> { let iterator = GitProviderWrapper::base_entries(self, &hex::encode(oid)); Box::new( AsyncIteratorToStream::new(iterator) .map_err(|error: String| io::Error::new(io::ErrorKind::Other, error)), ) } fn base_text( &self, oid: memo::Oid, path: &Path, ) -> Box> { Box::new( JsFuture::from(GitProviderWrapper::base_text( self, &hex::encode(oid), path.to_string_lossy().as_ref(), )) .then(|value| match value { Ok(value) => value .as_string() .ok_or_else(|| String::from("Text is not a string")), Err(error) => Err(error.into_error_message()?), }) .map_err(|error| io::Error::new(io::ErrorKind::Other, error)), ) } } impl memo::ChangeObserver for ChangeObserver { fn changed( &self, buffer_id: memo::BufferId, changes: Vec, selection_ranges: memo::BufferSelectionRanges, ) { let changes = changes .into_iter() .map(|change| Change { start: change.range.start, end: change.range.end, text: String::from_utf16_lossy(&change.code_units), }) .collect::>(); ChangeObserver::changed( self, JsValue::from_serde(&buffer_id).unwrap(), JsValue::from_serde(&changes).unwrap(), JsValue::from_serde(&JsSelections::from(selection_ranges)).unwrap(), ); } } impl From for JsSelections { fn from(selections: memo::BufferSelectionRanges) -> Self { let mut js_selections = JsSelections { local: HashMap::new(), remote: HashMap::new(), }; for (set_id, ranges) in selections.local { js_selections.local.insert( set_id, ranges.into_iter().map(|range| range.into()).collect(), ); } for (replica_id, sets) in selections.remote { let js_sets = sets .into_iter() .map(|ranges| ranges.into_iter().map(|range| range.into()).collect()) .collect(); js_selections.remote.insert(replica_id, js_sets); } js_selections } } impl From> for JsRange { fn from(range: Range) -> Self { JsRange { start: range.start, end: range.end, } } } impl Serialize for HexOid { fn serialize(&self, serializer: S) -> Result where S: Serializer, { hex::encode(self.0).serialize(serializer) } } impl<'de> Deserialize<'de> for HexOid { fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, { use serde::de::Error; let hex_string = String::deserialize(deserializer)?; let bytes = hex::decode(&hex_string).map_err(Error::custom)?; let mut oid = memo::Oid::default(); if oid.len() == bytes.len() { oid.copy_from_slice(&bytes); Ok(HexOid(oid)) } else { Err(D::Error::custom(format!( "{} cannot be parsed as a valid object id. pass a full 40-character hex string.", hex_string ))) } } } impl IntoJsError for T { fn into_js_err(self) -> JsValue { js_sys::Error::new(&self.to_string()).into() } } impl JsValueExt for JsValue { fn into_operation(self) -> Result, JsValue> { let js_bytes = self .dyn_into::() .map_err(|_| "Operation must be Uint8Array".into_js_err())?; let mut bytes = Vec::with_capacity(js_bytes.byte_length() as usize); js_bytes.for_each(&mut |byte, _, _| bytes.push(byte)); memo::Operation::deserialize(&bytes).map_err(|e| e.into_js_err()) } fn into_ranges_vec(self) -> Result>, JsValue> { Ok(self .into_serde::>() .map_err(|e| e.into_js_err())? .into_iter() .map(|JsRange { start, end }| start..end) .collect()) } fn into_error_message(self) -> Result { match self.dyn_into::() { Ok(js_err) => Ok(js_err.message().into()), Err(_) => Err(String::from( "An error occurred but can't be displayed because it's not an instance of an error", )), } } } ================================================ FILE: memo_js/src/support.ts ================================================ export type Tagged = BaseType & { __tag: TagName }; export type Oid = string; export type Path = string; export type ReplicaId = Tagged; export type BufferId = Tagged; export type SelectionSetId = Tagged; export type Point = { row: number; column: number }; export type Range = { start: Point; end: Point }; export type Change = Range & { text: string }; export interface BaseEntry { readonly depth: number; readonly name: string; readonly type: FileType; } export enum FileType { Directory = "Directory", Text = "Text" } export interface GitProvider { baseEntries(oid: Oid): AsyncIterable; baseText(oid: Oid, path: Path): Promise; } export interface SelectionRanges { local: Map>; remote: Map>>; } interface MemoSelectionRanges { local: { [setId: number]: Array }; remote: { [replicaId: string]: Array> }; } export class GitProviderWrapper { private git: GitProvider; constructor(git: GitProvider) { this.git = git; } baseEntries(oid: Oid): AsyncIteratorWrapper { return new AsyncIteratorWrapper( this.git.baseEntries(oid)[Symbol.asyncIterator]() ); } baseText(oid: Oid, path: Path): Promise { return this.git.baseText(oid, path); } } export class AsyncIteratorWrapper { private iterator: AsyncIterator; constructor(iterator: AsyncIterator) { this.iterator = iterator; } next(): Promise> { return this.iterator.next(); } } export type ChangeObserverCallback = ( change: { textChanges: ReadonlyArray; selectionRanges: SelectionRanges; } ) => void; export class ChangeObserver { emitter: Emitter; constructor() { this.emitter = new Emitter(); } onChange(bufferId: BufferId, callback: ChangeObserverCallback): Disposable { return this.emitter.on(`buffer-${bufferId}-change`, callback); } changed( bufferId: BufferId, textChanges: Change[], selectionRanges: MemoSelectionRanges ) { this.emitter.emit(`buffer-${bufferId}-change`, { textChanges, selectionRanges: fromMemoSelectionRanges(selectionRanges) }); } } export function fromMemoSelectionRanges( ranges: MemoSelectionRanges ): SelectionRanges { const local = new Map(); for (const setId in ranges.local) { local.set(setId, ranges.local[setId]); } const remote = new Map(); for (const replicaId in ranges.remote) { remote.set(replicaId, ranges.remote[replicaId]); } return { local, remote }; } export interface Disposable { dispose(): void; } export class CompositeDisposable implements Disposable { private disposables: Disposable[]; private disposed: boolean; constructor() { this.disposables = []; this.disposed = false; } add(disposable: Disposable) { this.disposables.push(disposable); } dispose() { if (!this.disposed) { this.disposed = true; for (const disposable of this.disposables) { disposable.dispose(); } } } } export type EmitterCallback = (params: any) => void; export class Emitter { private callbacks: Map; constructor() { this.callbacks = new Map(); } emit(eventName: string, params: any) { const callbacks = this.callbacks.get(eventName); if (callbacks) { for (const callback of callbacks) { callback(params); } } } on(eventName: string, callback: EmitterCallback): Disposable { let callbacks = this.callbacks.get(eventName); if (!callbacks) { callbacks = []; this.callbacks.set(eventName, callbacks); } callbacks.push(callback); return { dispose: () => { if (callbacks) { const callbackIndex = callbacks.indexOf(callback); if (callbackIndex >= 0) callbacks.splice(callbackIndex, 1); } } }; } } ================================================ FILE: memo_js/test/tests.ts ================================================ import { BaseEntry as MemoBaseEntry, Change, GitProvider, FileStatus, FileType, Oid, Operation, OperationEnvelope, Path, Point, ReplicaId, SelectionRanges, WorkTree } from "../src/index"; import * as assert from "assert"; import * as uuid from "uuid/v4"; import * as uuidParse from "uuid-parse"; suite("WorkTree", () => { test("basic API interaction", async () => { const OID_0 = "0".repeat(40); const OID_1 = "1".repeat(40); const git = new TestGitProvider(); git.commit(OID_0, [ { depth: 1, name: "a", type: FileType.Directory }, { depth: 2, name: "b", type: FileType.Directory }, { depth: 3, name: "c", type: FileType.Text, text: "oid0 base text" }, { depth: 3, name: "d", type: FileType.Directory } ]); git.commit(OID_1, [ { depth: 1, name: "a", type: FileType.Directory }, { depth: 2, name: "b", type: FileType.Directory }, { depth: 3, name: "c", type: FileType.Text, text: "oid1 base text" } ]); const [tree1, initOps1] = await WorkTree.create(uuid(), OID_0, [], git); const [tree2, initOps2] = await WorkTree.create( uuid(), OID_0, await collectOps(initOps1), git ); assert.strictEqual((await collectOps(initOps2)).length, 0); assert.strictEqual(tree1.head(), OID_0); assert.strictEqual(tree2.head(), OID_0); const ops1 = []; const ops2 = []; ops1.push(tree1.createFile("e", FileType.Text).operation()); ops2.push(tree2.createFile("f", FileType.Text).operation()); await assert.rejects(() => tree2.openTextFile("e")); ops1.push(...(await collectOps(tree1.applyOps(ops2.splice(0, Infinity))))); ops2.push(...(await collectOps(tree2.applyOps(ops1.splice(0, Infinity))))); assert.strictEqual(ops1.length, 0); assert.strictEqual(ops2.length, 0); const tree1BufferC = await tree1.openTextFile("a/b/c"); assert.strictEqual(tree1BufferC.getPath(), "a/b/c"); assert.strictEqual(tree1BufferC.getText(), "oid0 base text"); assert.strictEqual(tree1BufferC.getDeferredOperationCount(), 0); const tree2BufferC = await tree2.openTextFile("a/b/c"); assert.strictEqual(tree2BufferC.getPath(), "a/b/c"); assert.strictEqual(tree2BufferC.getText(), "oid0 base text"); assert.strictEqual(tree1BufferC.getDeferredOperationCount(), 0); const tree1BufferChanges: Change[] = []; tree1BufferC.onChange(c => tree1BufferChanges.push(...c.textChanges)); ops1.push( tree1BufferC .edit( [ { start: point(0, 4), end: point(0, 5) }, { start: point(0, 9), end: point(0, 10) } ], "-" ) .operation() ); assert.strictEqual(tree1BufferC.getText(), "oid0-base-text"); const tree2BufferChanges: Change[] = []; tree2BufferC.onChange(c => tree2BufferChanges.push(...c.textChanges)); assert.deepStrictEqual(await collectOps(tree2.applyOps(ops1)), []); assert.strictEqual(tree1BufferC.getText(), "oid0-base-text"); assert.deepStrictEqual(tree1BufferChanges, []); assert.deepStrictEqual(tree2BufferChanges, [ { start: point(0, 4), end: point(0, 5), text: "-" }, { start: point(0, 9), end: point(0, 10), text: "-" } ]); ops1.length = 0; ops1.push(tree1.createFile("x", FileType.Directory).operation()); ops1.push(tree1.createFile("x/y", FileType.Directory).operation()); ops1.push(tree1.rename("x", "a/b/x").operation()); ops1.push(tree1.remove("a/b/d").operation()); assert.deepStrictEqual(await collectOps(tree2.applyOps(ops1)), []); assert.deepStrictEqual(await collectOps(tree1.applyOps(ops2)), []); ops1.length = 0; ops2.length = 0; assert.deepStrictEqual(tree1.entries(), tree2.entries()); assert.deepEqual(tree1.entries({ descendInto: [] }), [ { depth: 1, type: FileType.Directory, name: "a", path: "a", basePath: "a", status: FileStatus.Unchanged, visible: true }, { depth: 1, type: FileType.Text, name: "e", path: "e", basePath: null, status: FileStatus.New, visible: true }, { depth: 1, type: FileType.Text, name: "f", path: "f", basePath: null, status: FileStatus.New, visible: true } ]); assert.deepEqual( tree1.entries({ showDeleted: true, descendInto: ["a", "a/b"] }), [ { depth: 1, type: FileType.Directory, name: "a", path: "a", basePath: "a", status: FileStatus.Unchanged, visible: true }, { depth: 2, type: FileType.Directory, name: "b", path: "a/b", basePath: "a/b", status: FileStatus.Unchanged, visible: true }, { depth: 3, type: FileType.Text, name: "c", path: "a/b/c", basePath: "a/b/c", status: FileStatus.Modified, visible: true }, { depth: 3, type: FileType.Directory, name: "d", path: "a/b/d", basePath: "a/b/d", status: FileStatus.Removed, visible: false }, { depth: 3, type: FileType.Directory, name: "x", path: "a/b/x", basePath: null, status: FileStatus.New, visible: true }, { depth: 1, type: FileType.Text, name: "e", path: "e", basePath: null, status: FileStatus.New, visible: true }, { depth: 1, type: FileType.Text, name: "f", path: "f", basePath: null, status: FileStatus.New, visible: true } ] ); assert(tree1.exists("a/b/x")); assert(!tree1.exists("a/b/d")); tree1BufferChanges.length = 0; tree2BufferChanges.length = 0; ops1.push(...(await collectOps(tree1.reset(OID_1)))); assert.deepStrictEqual(await collect(tree2.applyOps(ops1)), []); assert.strictEqual(tree1.head(), OID_1); assert.strictEqual(tree2.head(), OID_1); assert.strictEqual(tree1BufferC.getText(), "oid1 base text"); assert.strictEqual(tree2BufferC.getText(), "oid1 base text"); assert.deepStrictEqual(tree1BufferChanges, [ { start: point(0, 3), end: point(0, 5), text: "1 " }, { start: point(0, 9), end: point(0, 10), text: " " } ]); assert.deepStrictEqual(tree2BufferChanges, [ { start: point(0, 3), end: point(0, 5), text: "1 " }, { start: point(0, 9), end: point(0, 10), text: " " } ]); tree1.remove("a/b/c"); assert(tree1BufferC.getPath() == null); await collectOps(tree1.reset(null)); assert.strictEqual(tree1.head(), null); }); test("base path", async () => { const OID_0 = "0".repeat(40); const git = new TestGitProvider(); git.commit(OID_0, [ { depth: 1, name: "a", type: FileType.Directory }, { depth: 2, name: "b", type: FileType.Directory }, { depth: 3, name: "c", type: FileType.Text, text: "oid0 base text" }, { depth: 3, name: "d", type: FileType.Directory } ]); const [tree, initOps] = await WorkTree.create(uuid(), OID_0, [], git); await collectOps(initOps); tree.rename("a/b/c", "e"); tree.remove("a/b/d"); tree.createFile("f", FileType.Text); assert.deepStrictEqual(tree.entries(), [ { depth: 1, name: "a", basePath: "a", path: "a", status: FileStatus.Unchanged, type: FileType.Directory, visible: true }, { depth: 2, name: "b", path: "a/b", basePath: "a/b", status: FileStatus.Unchanged, type: FileType.Directory, visible: true }, { depth: 1, name: "e", path: "e", basePath: "a/b/c", status: FileStatus.Renamed, type: FileType.Text, visible: true }, { depth: 1, name: "f", path: "f", basePath: null, status: FileStatus.New, type: FileType.Text, visible: true } ]); }); test("selections", async () => { const OID = "0".repeat(40); const git = new TestGitProvider(); git.commit(OID, [ { depth: 1, name: "a", type: FileType.Text, text: "abc" } ]); const replica1 = uuid(); const replica2 = uuid(); const [tree1, initOps1] = await WorkTree.create(replica1, OID, [], git); const [tree2, initOps2] = await WorkTree.create( replica2, OID, await collectOps(initOps1), git ); await collectOps(initOps2); const buffer1 = await tree1.openTextFile("a"); let selection2Changes: Array = []; const buffer2 = await tree2.openTextFile("a"); buffer2.onChange(c => selection2Changes.push(c.selectionRanges)); const [set, createSetOp] = buffer1.addSelectionSet([ { start: point(0, 0), end: point(0, 1) } ]); assert.deepEqual(mapToObject(buffer1.getSelectionRanges().local), { [set]: [{ start: point(0, 0), end: point(0, 1) }] }); assert.deepEqual(mapToObject(buffer1.getSelectionRanges().remote), {}); await collectOps(tree2.applyOps([createSetOp.operation()])); assert.deepEqual(mapToObject(buffer2.getSelectionRanges().local), {}); assert.deepEqual(mapToObject(buffer2.getSelectionRanges().remote), { [replica1]: [[{ start: point(0, 0), end: point(0, 1) }]] }); assert.equal(selection2Changes.length, 1); assert.deepEqual(last(selection2Changes), buffer2.getSelectionRanges()); const replaceSetOp = buffer1.replaceSelectionSet(set, [ { start: point(0, 2), end: point(0, 3) } ]); assert.deepEqual(mapToObject(buffer1.getSelectionRanges().local), { [set]: [{ start: point(0, 2), end: point(0, 3) }] }); assert.deepEqual(mapToObject(buffer1.getSelectionRanges().remote), {}); await collectOps(tree2.applyOps([replaceSetOp.operation()])); assert.deepEqual(mapToObject(buffer2.getSelectionRanges().local), {}); assert.deepEqual(mapToObject(buffer2.getSelectionRanges().remote), { [replica1]: [[{ start: point(0, 2), end: point(0, 3) }]] }); assert.equal(selection2Changes.length, 2); assert.deepEqual(last(selection2Changes), buffer2.getSelectionRanges()); const removeSetOp = buffer1.removeSelectionSet(set); assert.deepEqual(mapToObject(buffer1.getSelectionRanges().local), {}); assert.deepEqual(mapToObject(buffer1.getSelectionRanges().remote), {}); await collectOps(tree2.applyOps([removeSetOp.operation()])); assert.deepEqual(mapToObject(buffer2.getSelectionRanges().local), {}); assert.deepEqual(mapToObject(buffer2.getSelectionRanges().remote), {}); assert.equal(selection2Changes.length, 3); assert.deepEqual(last(selection2Changes), buffer2.getSelectionRanges()); }); test("active location", async () => { const oid = "0".repeat(40); const git = new TestGitProvider(); git.commit(oid, [ { depth: 1, name: "a", type: FileType.Text, text: "a" }, { depth: 1, name: "b", type: FileType.Text, text: "b" } ]); const replicaId1 = uuid(); const [tree1, initOps1] = await WorkTree.create(replicaId1, oid, [], git); const replicaId2 = uuid(); const [tree2, initOps2] = await WorkTree.create( replicaId2, oid, await collectOps(initOps1), git ); assert.strictEqual((await collectOps(initOps2)).length, 0); const buffer1 = await tree1.openTextFile("a"); const buffer2 = await tree2.openTextFile("b"); await collectOps( tree1.applyOps([tree2.setActiveLocation(buffer2).operation()]) ); await collectOps( tree2.applyOps([tree1.setActiveLocation(buffer1).operation()]) ); assert.deepStrictEqual(mapToObject(tree1.getReplicaLocations()), { [replicaId1]: "a", [replicaId2]: "b" }); assert.deepStrictEqual(mapToObject(tree2.getReplicaLocations()), { [replicaId1]: "a", [replicaId2]: "b" }); await collectOps( tree1.applyOps([tree2.setActiveLocation(null).operation()]) ); assert.deepStrictEqual(mapToObject(tree1.getReplicaLocations()), { [replicaId1]: "a" }); assert.deepStrictEqual(mapToObject(tree2.getReplicaLocations()), { [replicaId1]: "a" }); }); test("an invalid base commit oid throws an error instead of crashing", async () => { assert.rejects( () => WorkTree.create(uuid(), "12345678", [], new TestGitProvider()), /12345678/ ); }); test("opening a directory as a text file", async () => { const git = new TestGitProvider(); const [tree] = await WorkTree.create(uuid(), null, [], git); tree.createFile("dir", FileType.Directory); assert.rejects(tree.openTextFile("dir"), /text/i); }); test("the epoch head is available on operation envelopes", async () => { const OID = "0".repeat(40); const git = new TestGitProvider(); git.commit(OID, [{ depth: 1, name: "a", type: FileType.Directory }]); const [tree1] = await WorkTree.create(uuid(), null, [], git); const envelope1 = tree1.createFile("x", FileType.Text); assert.strictEqual(envelope1.epochHead(), null); const [envelope2] = await collect(tree1.reset(OID)); assert.strictEqual(envelope2.epochHead(), OID); const envelope3 = tree1.createFile("y", FileType.Text); assert.strictEqual(envelope3.epochHead(), OID); }); test("epoch id on operation envelopes", async () => { const git = new TestGitProvider(); const replicaId = uuid(); const [tree] = await WorkTree.create(replicaId, null, [], git); const envelope1 = tree.createFile("a", FileType.Text); const envelope1EpochId = parseEpochId(envelope1.epochId()); const envelope2 = tree.createFile("b", FileType.Text); const envelope2EpochId = parseEpochId(envelope2.epochId()); assert.deepStrictEqual(envelope1EpochId, envelope2EpochId); assert.equal(envelope1EpochId.replicaId, replicaId); const [envelope3] = await collect(tree.reset(null)); const envelope3EpochId = parseEpochId(envelope3.epochId()); assert.notDeepStrictEqual(envelope3EpochId, envelope2EpochId); assert(envelope3EpochId.timestamp > envelope2EpochId.timestamp); }); test("epoch id on WorkTree", async () => { const git = new TestGitProvider(); const replicaId = uuid(); const [tree] = await WorkTree.create(replicaId, null, [], git); const envelope = tree.createFile("a", FileType.Text); const envelopeEpochId = parseEpochId(envelope.epochId()); const treeEpochId = parseEpochId(tree.epochId()); assert.deepStrictEqual(envelopeEpochId, treeEpochId); }); test("replica id", async () => { const git = new TestGitProvider(); { const replicaId = uuid(); const [tree] = await WorkTree.create(replicaId, null, [], git); const envelope = tree.createFile("x", FileType.Text); assert.strictEqual(envelope.epochReplicaId(), replicaId); } { await assert.rejects( WorkTree.create("invalid-replica-id", null, [], git), /invalid-replica-id/ ); } }); test("isSelectionUpdate", async () => { const git = new TestGitProvider(); const [tree] = await WorkTree.create(uuid(), null, [], git); const envelope1 = tree.createFile("file", FileType.Text); assert(!envelope1.isSelectionUpdate()); const buffer1 = await tree.openTextFile("file"); const envelope2 = buffer1.edit( [{ start: point(0, 0), end: point(0, 0) }], "abc" ); assert(!envelope2.isSelectionUpdate()); const [, envelope3] = buffer1.addSelectionSet([]); assert(envelope3.isSelectionUpdate()); }); test("versions", async () => { const OID = "0".repeat(40); const git = new TestGitProvider(); git.commit(OID, [{ depth: 1, name: "a", type: FileType.Directory }]); const [tree1, initOps1] = await WorkTree.create(uuid(), OID, [], git); const [tree2, initOps2] = await WorkTree.create( uuid(), OID, await collectOps(initOps1), git ); assert.deepEqual(await collectOps(initOps2), []); assert(tree1.hasObserved(tree2.version())); assert(tree2.hasObserved(tree1.version())); let op1 = tree1.createFile("a/b.txt", FileType.Text); let op2 = tree2.createFile("a/c.txt", FileType.Text); assert(!tree1.hasObserved(tree2.version())); assert(!tree2.hasObserved(tree1.version())); await collectOps(tree1.applyOps([op2.operation()])); assert(tree1.hasObserved(tree2.version())); await collectOps(tree2.applyOps([op1.operation()])); assert(tree2.hasObserved(tree1.version())); }); test("buffer reuse", async () => { const git = new TestGitProvider(); const replicaId = uuid(); const [tree] = await WorkTree.create(replicaId, null, [], git); tree.createFile("a", FileType.Text); assert.equal(await tree.openTextFile("a"), await tree.openTextFile("a")); }); test("buffer.onChange disposal", async () => { const OID = "0".repeat(40); const git = new TestGitProvider(); git.commit(OID, [ { depth: 1, name: "a", type: FileType.Directory }, { depth: 2, name: "b", type: FileType.Directory }, { depth: 3, name: "c", type: FileType.Text, text: "oid0 base text" }, { depth: 3, name: "d", type: FileType.Directory } ]); const [tree1, initOps1] = await WorkTree.create(uuid(), OID, [], git); const [tree2, initOps2] = await WorkTree.create( uuid(), OID, await collectOps(initOps1), git ); tree1.applyOps(await collectOps(initOps2)); const buffer1 = await tree1.openTextFile("a/b/c"); let buffer1ChangeCount = 0; const disposable = buffer1.onChange(_ => buffer1ChangeCount++); const buffer2 = await tree2.openTextFile("a/b/c"); tree1.applyOps([ buffer2.edit([{ start: point(0, 0), end: point(0, 0) }], "x").operation() ]); assert.strictEqual(buffer1ChangeCount, 1); disposable.dispose(); tree1.applyOps([ buffer2.edit([{ start: point(0, 0), end: point(0, 0) }], "y").operation() ]); assert.strictEqual(buffer1ChangeCount, 1); }); test("throwing error when retrieving base entries", async () => { const git = { async *baseEntries(): AsyncIterable { await 0; throw new Error("baseEntries error"); }, async baseText(): Promise { await 0; throw new Error("baseText"); } }; const OID = "0".repeat(40); const [, initOps] = await WorkTree.create(uuid(), OID, [], git); assert.rejects(collectOps(initOps), /baseEntries error/); }); test("throwing error when retrieving base text", async () => { const git = { async *baseEntries(): AsyncIterable { await 0; yield { depth: 1, name: "a", type: FileType.Text, text: "base text" }; }, async baseText(): Promise { await 0; throw new Error("baseText"); } }; const OID = "0".repeat(40); const [tree, initOps] = await WorkTree.create(uuid(), OID, [], git); await collectOps(initOps); assert.rejects(tree.openTextFile("a"), /baseText/); }); }); type BaseEntry = | MemoBaseEntry & { type: FileType.Directory } | MemoBaseEntry & { type: FileType.Text; text: string }; async function collect(iterable: AsyncIterable): Promise { const items = []; for await (const item of iterable) { items.push(item); } return items; } async function collectOps( ops: AsyncIterable ): Promise { const envelopes = await collect(ops); return envelopes.map(envelope => envelope.operation()); } function point(row: number, column: number): Point { return { row, column }; } type ParsedEpochId = { timestamp: number; replicaId: ReplicaId }; function parseEpochId(epochId: Uint8Array): ParsedEpochId { const epochIdBuffer = Buffer.from(epochId); assert.equal(epochIdBuffer.length, 24); // Timestamp is a u64 but JavaScript doesn't support it, so we fail if its // high bits are not 0. assert.equal(epochIdBuffer.readUInt32BE(0), 0); const timestamp = epochIdBuffer.readUInt32BE(4); const replicaId = uuidParse.unparse(epochIdBuffer.slice(8)) as ReplicaId; return { timestamp, replicaId }; } function mapToObject(map: Map): { [key: string]: T } { const object: { [key: string]: T } = {}; map.forEach((value, key) => { object[key] = value; }); return object; } function last(array: ArrayLike): undefined | T { return array[array.length - 1]; } class TestGitProvider implements GitProvider { private entries: Map>; private text: Map>; constructor() { this.entries = new Map(); this.text = new Map(); } commit(oid: Oid, entries: ReadonlyArray) { this.entries.set(oid, entries); const textByPath = new Map(); const path = []; for (const entry of entries) { path.length = entry.depth - 1; path.push(entry.name); if (entry.type === FileType.Text) { textByPath.set(path.join("/"), entry.text); } } this.text.set(oid, textByPath); } async *baseEntries(oid: Oid): AsyncIterable { const entries = this.entries.get(oid); if (entries) { for (const entry of entries) { yield entry; } } else { throw new Error("yy"); } } async baseText(oid: Oid, path: Path): Promise { const textByPath = this.text.get(oid); if (textByPath != null) { const text = textByPath.get(path); if (text != null) { await Promise.resolve(); return text; } else { throw new Error(`No text found at path ${path}`); } } else { throw new Error(`No commit found with oid ${oid}`); } } } ================================================ FILE: memo_js/test/tsconfig.json ================================================ { "compilerOptions": { "outDir": "./dist/", "noImplicitAny": true, "strictNullChecks": true, "module": "esnext", "declarationMap": true, "declaration": true, "lib": ["es2015", "esnext"], "types": ["mocha", "node"] } } ================================================ FILE: memo_js/tsconfig.json ================================================ { "include": ["src/**/*.ts"], "compilerOptions": { "outDir": "./dist/", "noImplicitAny": true, "strictNullChecks": true, "module": "esnext", "declarationMap": true, "declaration": true, "lib": ["es2015", "esnext"], "types": ["node"] } } ================================================ FILE: memo_js/webpack.config.js ================================================ var path = require("path"); const baseConfig = { target: "node", module: { rules: [ { test: /\.ts$/, use: "ts-loader", exclude: /node_modules/ } ] }, resolve: { extensions: [".ts", ".js", ".wasm"] } }; const libConfig = { ...baseConfig, entry: "./src/index.ts", mode: "production", devtool: "source-map", output: { path: path.resolve(__dirname, "dist"), filename: "index.node.js", library: "memo", libraryTarget: "umd" } }; const testConfig = { ...baseConfig, entry: "./test/tests.ts", mode: "development", devtool: "cheap-eval-source-map", output: { path: path.resolve(__dirname, "test", "dist"), filename: "tests.js" } }; module.exports = env => { if (env && env.test) { return testConfig; } else { return libConfig; } }; ================================================ FILE: rust-toolchain ================================================ nightly-2019-01-26 ================================================ FILE: script/bench ================================================ #!/bin/bash set -e cd xray_core cargo bench "$@" ================================================ FILE: script/build ================================================ #!/bin/bash set -e cd xray_electron; yarn install --check-files; cd - cd xray_ui; yarn install; cd - cd xray_cli; cargo build "$@"; cd - cd xray_server; cargo build "$@"; cd - cd xray_browser; script/build; cd - cd memo_js; yarn install && script/build; cd - ================================================ FILE: script/cibuild ================================================ #!/bin/bash set -e script/build script/test script/bench ================================================ FILE: script/test ================================================ #!/bin/bash set -e cd xray_core; cargo test; cd - cd xray_wasm; script/test; cd - cd xray_ui; yarn test; cd - cd memo_core; cargo test; cd - cd memo_js; yarn test; cd - ================================================ FILE: xray_browser/README.md ================================================ # Xray Browser This directory packages Xray for use in a web browser. Because browsers don't provide access to the underlying system, when running in a browser, Xray depends on being able to connect to a shared workspace on a remote instance of the `xray_server` executable. This directory contains a [development web server](./script/server) that serves a browser-compatible user interface and proxies connections to `xray_server` over WebSockets. Assuming you have built Xray with `script/build --release` in the root of this repo, you can present a web-based UI for any Xray instance as follows. * Start an instance `xray_server` listening for incoming connections on port 8080: ```sh # Run in the root of the repository (--headless is optional) script/xray --listen=8080 --headless your_project_dir ``` * Start the development web server: ```sh xray_browser/script/server ``` ================================================ FILE: xray_browser/package.json ================================================ { "name": "xray_browser", "license": "MIT", "dependencies": { "xray_wasm": "../xray_wasm", "xray_ui": "../xray_ui" }, "devDependencies": { "express": "^4.16.3", "webpack": "^4.6.0", "webpack-cli": "^2.0.15", "webpack-dev-middleware": "^3.1.2", "ws": "^5.1.1" } } ================================================ FILE: xray_browser/script/build ================================================ #!/usr/bin/env bash set -e rm -rf dist mkdir -p dist cd ../xray_wasm && script/build && cd - yarn install --check-files node_modules/.bin/webpack --target=web --mode=development src/ui.js --output-filename=ui.js node_modules/.bin/webpack --target=webworker --mode=development src/worker.js --output-filename=worker.js cp static/index.html dist/index.html ================================================ FILE: xray_browser/script/server ================================================ #!/usr/bin/env node const assert = require("assert"); const http = require("http"); const express = require("express"); const ws = require("ws"); const { Socket } = require("net"); const path = require("path"); const webpack = require("webpack"); const webpackDev = require("webpack-dev-middleware"); express.static.mime.types["wasm"] = "application/wasm"; const xrayServerAddress = process.env.XRAY_SERVER_ADDRESS || "127.0.0.1"; const xrayServerPort = process.env.XRAY_SERVER_PORT || 8080; const webServerPort = process.env.PORT || 3000; const app = express(); const server = http.createServer(app); const webpackMode = process.env.NODE_ENV || "development"; const compiler = webpack([ { mode: webpackMode, entry: path.join(__dirname, "../src/ui.js"), output: { filename: "ui.js", path: path.join(__dirname, "dist") } }, { mode: webpackMode, target: "webworker", entry: path.join(__dirname, "../src/worker.js"), output: { filename: "worker.js", path: path.join(__dirname, "dist") } } ]); const websocketServer = new ws.Server({ server, path: "/ws" }); websocketServer.on("connection", async ws => { const connection = new Socket(); let incomingMessage = null; let remainingBytes = 0; connection.on("data", data => { let offset = 0; while (offset < data.length) { if (incomingMessage) { assert(remainingBytes !== 0, "remainingBytes should not be 0"); const copiedBytes = data.copy( incomingMessage, incomingMessage.length - remainingBytes, offset, offset + remainingBytes ); remainingBytes -= copiedBytes; offset += copiedBytes; } else { remainingBytes = data.readUInt32BE(offset); incomingMessage = Buffer.alloc(remainingBytes); offset += 4; } if (incomingMessage && remainingBytes === 0) { try { ws.send(incomingMessage); } catch (error) { console.error("Error sending to web socket:", error); } incomingMessage = null; } } }); await new Promise(resolve => { connection.connect( { host: xrayServerAddress, port: xrayServerPort }, resolve ); }); ws.on("message", message => { const bufferLengthHeader = Buffer.alloc(4); bufferLengthHeader.writeUInt32BE(message.length, 0); connection.write(Buffer.concat([bufferLengthHeader, message])); }); ws.on("close", () => connection.destroy()); }); app.use(webpackDev(compiler, { publicPath: "/" })); app.use("/", express.static(path.join(__dirname, "../static"))); server.listen(webServerPort, () => { console.log(`Using xray server: ${xrayServerAddress}:${xrayServerPort}`); console.log("Listening for HTTP connections on port " + webServerPort); }); ================================================ FILE: xray_browser/src/client.js ================================================ export default class XrayClient { constructor(worker) { this.worker = worker; } onMessage(callback) { this.worker.addEventListener("message", message => { callback(message.data); }); } sendMessage(message) { this.worker.postMessage(message); } } ================================================ FILE: xray_browser/src/ui.js ================================================ import { React, ReactDOM, App, buildViewRegistry } from "xray_ui"; import XrayClient from "./client"; const $ = React.createElement; const client = new XrayClient(new Worker("worker.js")); const websocketURL = new URL("/ws", window.location.href); websocketURL.protocol = "ws"; client.sendMessage({ type: "ConnectToWebsocket", url: websocketURL.href }); const viewRegistry = buildViewRegistry(client); let initialRender = true; client.onMessage(message => { switch (message.type) { case "UpdateWindow": viewRegistry.update(message); if (initialRender) { ReactDOM.render( $(App, { inBrowser: true, viewRegistry }), document.getElementById("app") ); initialRender = false; } break; default: console.warn("Received unexpected message", message); } }); ================================================ FILE: xray_browser/src/worker.js ================================================ import { xray as xrayPromise, JsSink } from "xray_wasm"; const encoder = new TextEncoder(); const decoder = new TextDecoder("utf-8"); const serverPromise = xrayPromise.then(xray => new Server(xray)); global.addEventListener("message", async event => { const message = event.data; const server = await serverPromise; server.handleMessage(message); }); class Server { constructor(xray) { this.xray = xray; this.xrayServer = xray.Server.new(); this.xrayServer.start_app( new JsSink({ send: buffer => { const message = JSON.parse(decoder.decode(buffer)); if (message.type === "OpenWindow") { this.startWindow(message.window_id); } else { throw new Error("Expected first message type to be OpenWindow"); } } }) ); } startWindow(windowId) { const channel = this.xray.Channel.new(); this.windowSender = channel.take_sender(); this.xrayServer.start_window( windowId, channel.take_receiver(), new JsSink({ send(buffer) { global.postMessage(JSON.parse(decoder.decode(buffer))); } }) ); } connectToWebsocket(url) { const socket = new WebSocket(url); socket.binaryType = "arraybuffer"; const channel = this.xray.Channel.new(); const sender = channel.take_sender(); socket.addEventListener("message", function(event) { const data = new Uint8Array(event.data); sender.send(data); }); this.xrayServer.connect_to_peer( channel.take_receiver(), new JsSink({ send(message) { socket.send(message); } }) ); } handleMessage(message) { switch (message.type) { case "ConnectToWebsocket": this.connectToWebsocket(message.url); break; case "Action": this.windowSender.send(encoder.encode(JSON.stringify(message))); break; default: console.error("Received unknown message", message); } } } ================================================ FILE: xray_browser/static/index.html ================================================

================================================ FILE: xray_cli/Cargo.toml ================================================ [package] name = "xray_cli" version = "0.1.0" authors = ["Nathan Sobo "] [dependencies] docopt = "0.8" serde = "1.0" serde_derive = "1.0" serde_json = "1.0" ================================================ FILE: xray_cli/README.md ================================================ # Xray CLI This crate is an executable that provides a command-line interface for Xray. It can spawn `xray_server` in headless mode or launch the `xray_electron` application. It sends commands to `xray_server` over a domain socket. ================================================ FILE: xray_cli/src/main.rs ================================================ extern crate docopt; #[macro_use] extern crate serde_derive; extern crate serde_json; use docopt::Docopt; use std::env; use std::fs; use std::io::{BufRead, BufReader, Write}; use std::net::SocketAddr; use std::os::unix::net::UnixStream; use std::path::{Path, PathBuf}; use std::process; use std::process::{Command, Stdio}; const USAGE: &'static str = " Xray Usage: xray [--socket-path=] [--headless] [--listen=] [--connect=
] [...] xray (-h | --help) Options: -h --help Show this screen. -H --headless Start Xray in headless mode. -l --listen= Listen for TCP connections on the specified port. -c --connect=
Connect to the specified address. "; type PortNumber = u16; #[derive(Debug, Serialize)] #[serde(tag = "type")] enum ServerRequest { StartCli { headless: bool }, OpenWorkspace { paths: Vec }, ConnectToPeer { address: SocketAddr }, TcpListen { port: PortNumber }, } #[derive(Deserialize)] #[serde(tag = "type")] enum ServerResponse { Ok, Error { description: String }, } #[derive(Debug, Deserialize)] struct Args { flag_socket_path: Option, flag_headless: Option, flag_listen: Option, flag_connect: Option, arg_path: Vec, } fn main() { process::exit(match launch() { Ok(()) => 0, Err(description) => { eprintln!("{}", description); 1 } }) } fn launch() -> Result<(), String> { let args: Args = Docopt::new(USAGE) .and_then(|d| d.deserialize()) .unwrap_or_else(|e| e.exit()); let headless = args.flag_headless.unwrap_or(false); const DEFAULT_SOCKET_PATH: &'static str = "/tmp/xray.sock"; let socket_path = PathBuf::from( args.flag_socket_path .as_ref() .map_or(DEFAULT_SOCKET_PATH, |path| path.as_str()), ); let mut socket = match UnixStream::connect(&socket_path) { Ok(socket) => socket, Err(_) => { let src_path = PathBuf::from(env::var("XRAY_SRC_PATH") .map_err(|_| "Must specify the XRAY_SRC_PATH environment variable")?); let server_bin_path; let node_env; if cfg!(debug_assertions) { server_bin_path = src_path.join("target/debug/xray_server"); node_env = "development"; } else { server_bin_path = src_path.join("target/release/xray_server"); node_env = "production"; } if headless { start_headless(&server_bin_path, &socket_path)? } else { start_electron(&src_path, &server_bin_path, &socket_path, &node_env)? } } }; send_message(&mut socket, ServerRequest::StartCli { headless })?; if let Some(address) = args.flag_connect { send_message(&mut socket, ServerRequest::ConnectToPeer { address })?; } else if args.arg_path.len() > 0 { let mut paths = Vec::new(); for path in args.arg_path { paths.push(fs::canonicalize(&path) .map_err(|error| format!("Invalid path {:?} - {}", path, error))?); } send_message(&mut socket, ServerRequest::OpenWorkspace { paths })?; } if let Some(port) = args.flag_listen { send_message(&mut socket, ServerRequest::TcpListen { port })?; } Ok(()) } fn start_headless(server_bin_path: &Path, socket_path: &Path) -> Result { let command = Command::new(server_bin_path) .env("XRAY_SOCKET_PATH", socket_path) .env("XRAY_HEADLESS", "1") .stdout(Stdio::piped()) .spawn() .map_err(|error| format!("Failed to open Xray app {}", error))?; let mut stdout = command.stdout.unwrap(); let mut reader = BufReader::new(&mut stdout); let mut line = String::new(); while line != "Listening\n" { reader .read_line(&mut line) .map_err(|_| String::from("Error reading app output"))?; } UnixStream::connect(socket_path).map_err(|_| String::from("Error connecting to socket")) } fn start_electron( src_path: &Path, server_bin_path: &Path, socket_path: &Path, node_env: &str, ) -> Result { let electron_app_path = Path::new(src_path).join("xray_electron"); let electron_bin_path = electron_app_path.join("node_modules/.bin/electron"); let command = Command::new(electron_bin_path) .arg(electron_app_path) .env("XRAY_SERVER_PATH", server_bin_path) .env("XRAY_SOCKET_PATH", socket_path) .env("XRAY_HEADLESS", "0") .env("NODE_ENV", node_env) .stdout(Stdio::piped()) .spawn() .map_err(|error| format!("Failed to open Xray app {}", error))?; let mut stdout = command.stdout.unwrap(); let mut reader = BufReader::new(&mut stdout); let mut line = String::new(); while line != "Listening\n" { reader .read_line(&mut line) .map_err(|_| String::from("Error reading app output"))?; } UnixStream::connect(socket_path).map_err(|_| String::from("Error connecting to socket")) } fn send_message(socket: &mut UnixStream, message: ServerRequest) -> Result<(), String> { let bytes = serde_json::to_vec(&message).expect("Error serializing message"); socket .write(&bytes) .expect("Error writing to server socket"); socket.write(b"\n").expect("Error writing to server socket"); let mut reader = BufReader::new(socket); let mut line = String::new(); reader .read_line(&mut line) .expect("Error reading server response"); match serde_json::from_str::(&line).expect("Error reading server response") { ServerResponse::Ok => Ok(()), ServerResponse::Error { description } => Err(description), } } ================================================ FILE: xray_core/Cargo.toml ================================================ [package] name = "xray_core" version = "0.1.0" authors = ["Nathan Sobo "] license = "MIT" [dependencies] bincode = "1.0" bytes = { version ="0.4", features = ["serde"] } futures = "0.1" lazy_static = "1.0" parking_lot = "0.5" seahash = "3.0" serde = "1.0" serde_derive = "1.0" serde_json = "1.0" smallvec = "0.6.0" [target.'cfg(target_arch = "wasm32")'.dependencies] wasm-bindgen = "0.2" [dev-dependencies] rand = "0.3" futures-cpupool = "0.1" tokio-core = "0.1" tokio-timer = "0.2" criterion = "0.2" [[bench]] name = "bench" harness = false ================================================ FILE: xray_core/README.md ================================================ # Xray Core This directory contains the native core of application as a pure Rust library that is agnostic to the details of the underlying platform. It is a dependency of the sibling `xray_server` crate, which provides it with network and file I/O as well as the ability to spawn futures in the foreground and on background threads. ================================================ FILE: xray_core/benches/bench.rs ================================================ extern crate xray_core; #[macro_use] extern crate criterion; use criterion::Criterion; use std::cell::RefCell; use std::rc::Rc; use xray_core::buffer::{Buffer, Point}; use xray_core::buffer_view::BufferView; fn add_selection(c: &mut Criterion) { c.bench_function("add_selection_below", |b| { b.iter_with_setup( || { let mut buffer_view = create_buffer_view(100); for i in 0..100 { buffer_view.add_selection(Point::new(i, 0), Point::new(i, 0)); } buffer_view }, |mut buffer_view| buffer_view.add_selection_below(), ) }); c.bench_function("add_selection_above", |b| { b.iter_with_setup( || { let mut buffer_view = create_buffer_view(100); for i in 0..100 { buffer_view.add_selection(Point::new(i, 0), Point::new(i, 0)); } buffer_view }, |mut buffer_view| buffer_view.add_selection_above(), ) }); } fn edit(c: &mut Criterion) { c.bench_function("edit", |b| { b.iter_with_setup( || { let mut buffer_view = create_buffer_view(50); for i in 0..50 { buffer_view.add_selection(Point::new(i, 0), Point::new(i, 0)); } buffer_view }, |mut buffer_view| { buffer_view.edit("a"); buffer_view.edit("b"); buffer_view.edit("c"); }, ) }); } fn create_buffer_view(lines: usize) -> BufferView { let mut buffer = Buffer::new(0); for i in 0..lines { let len = buffer.len(); buffer.edit( &[len..len], format!("Lorem ipsum dolor sit amet {}\n", i).as_str(), ); } BufferView::new(Rc::new(RefCell::new(buffer)), 0, None) } criterion_group!(benches, edit, add_selection); criterion_main!(benches); ================================================ FILE: xray_core/src/app.rs ================================================ use bytes::Bytes; use fs; use futures::unsync::mpsc::{self, UnboundedReceiver, UnboundedSender}; use futures::{future, Async, Future, IntoFuture, Stream}; use never::Never; use notify_cell::{NotifyCell, NotifyCellObserver}; use project::LocalProject; use rpc::{self, client, server}; use serde_json; use std::cell::RefCell; use std::collections::HashMap; use std::fmt; use std::io; use std::rc::Rc; use window::{ViewId, Window, WindowUpdateStream}; use workspace::{LocalWorkspace, RemoteWorkspace, Workspace, WorkspaceService, WorkspaceView}; use BackgroundExecutor; use ForegroundExecutor; use IntoShared; pub type WindowId = usize; type WorkspaceId = usize; type PeerId = usize; pub struct App { headless: bool, foreground: ForegroundExecutor, background: BackgroundExecutor, file_provider: Rc, commands_tx: UnboundedSender, commands_rx: Option>, peer_list: Rc>, next_workspace_id: WorkspaceId, workspaces: HashMap, next_window_id: WindowId, windows: HashMap, updates: NotifyCell<()>, } pub enum Command { OpenWindow(WindowId), } pub struct PeerList { foreground: ForegroundExecutor, next_peer_id: PeerId, peers: HashMap, opened_workspaces_tx: UnboundedSender, opened_workspaces_rx: Option>, updates: NotifyCell<()>, } struct Peer { foreground: ForegroundExecutor, service: client::FullUpdateService, } #[derive(Debug, PartialEq)] struct PeerState { workspaces: Vec, } #[derive(Debug, PartialEq)] struct WorkspaceDescriptor { id: WorkspaceId, } struct AppService { app: Rc>, updates: NotifyCellObserver<()>, } #[derive(Debug, Serialize, Deserialize)] pub struct ServiceState { workspace_ids: Vec, } #[derive(Serialize, Deserialize)] pub enum ServiceRequest { OpenWorkspace(WorkspaceId), } #[derive(Serialize, Deserialize)] pub enum ServiceResponse { OpenedWorkspace(rpc::ServiceId), } #[derive(Serialize, Deserialize)] pub enum ServiceError { WorkspaceNotFound(WorkspaceId), } enum WorkspaceEntry { Local(Rc>), Remote(Rc>), } enum WorkspaceOpenError { NotFound(WorkspaceId), RpcError(rpc::Error), } impl App { pub fn new( headless: bool, foreground: ForegroundExecutor, background: BackgroundExecutor, file_provider: T, ) -> Rc> { let (commands_tx, commands_rx) = mpsc::unbounded(); let peer_list = PeerList::new(foreground.clone()).into_shared(); let app = App { headless, foreground: foreground.clone(), background, file_provider: Rc::new(file_provider), commands_tx, commands_rx: Some(commands_rx), peer_list: peer_list.clone(), next_workspace_id: 0, workspaces: HashMap::new(), next_window_id: 1, windows: HashMap::new(), updates: NotifyCell::new(()), }.into_shared(); let app_clone = app.clone(); foreground .execute(Box::new( peer_list .borrow_mut() .take_opened_workspaces() .unwrap() .for_each(move |workspace| { let workspace = workspace.into_shared(); let mut app = app_clone.borrow_mut(); app.add_workspace(WorkspaceEntry::Remote(workspace.clone())); app.open_workspace_window(workspace); Ok(()) }), )) .unwrap(); app } pub fn commands(&mut self) -> Option> { self.commands_rx.take() } pub fn headless(&self) -> bool { self.headless } pub fn open_local_workspace(&mut self, roots: Vec) { let file_provider = self.file_provider.clone(); let workspace = LocalWorkspace::new(LocalProject::new(file_provider, roots)).into_shared(); self.add_workspace(WorkspaceEntry::Local(workspace.clone())); self.open_workspace_window(workspace); } fn add_workspace(&mut self, workspace: WorkspaceEntry) { let id = self.next_workspace_id; self.next_workspace_id += 1; self.workspaces.insert(id, workspace); self.updates.set(()); } fn open_workspace_window(&mut self, workspace: Rc>) { if !self.headless { let mut window = Window::new(Some(self.background.clone()), 0.0); let workspace_view_handle = window.add_view(WorkspaceView::new( self.foreground.clone(), workspace.clone(), )); window.set_root_view(workspace_view_handle); let window_id = self.next_window_id; self.next_window_id += 1; self.windows.insert(window_id, window); if self.commands_tx .unbounded_send(Command::OpenWindow(window_id)) .is_err() { let (commands_tx, commands_rx) = mpsc::unbounded(); commands_tx .unbounded_send(Command::OpenWindow(window_id)) .unwrap(); self.commands_tx = commands_tx; self.commands_rx = Some(commands_rx); } } } pub fn start_window(&mut self, id: &WindowId, height: f64) -> Result { let window = self.windows.get_mut(id).ok_or(())?; window.set_height(height); Ok(window.updates()) } pub fn dispatch_action( &mut self, window_id: WindowId, view_id: ViewId, action: serde_json::Value, ) { match self.windows.get_mut(&window_id) { Some(ref mut window) => window.dispatch_action(view_id, action), None => unimplemented!(), }; } pub fn close_window(&mut self, window_id: WindowId) -> Result<(), ()> { self.windows.remove(&window_id).map(|_| ()).ok_or(()) } pub fn connect_to_client(app: Rc>, incoming: S) -> server::Connection where S: 'static + Stream, { server::Connection::new(incoming, AppService::new(app.clone())) } pub fn connect_to_server( &self, incoming: S, ) -> Box> where S: 'static + Stream, { PeerList::connect_to_server(self.peer_list.clone(), incoming) } } impl PeerList { fn new(foreground: ForegroundExecutor) -> Self { let (tx, rx) = mpsc::unbounded(); PeerList { foreground, next_peer_id: 0, peers: HashMap::new(), opened_workspaces_tx: tx, opened_workspaces_rx: Some(rx), updates: NotifyCell::new(()), } } #[cfg(test)] fn state(&self) -> Vec { self.peers .iter() .filter_map(|(_, peer)| { peer.service.latest_state().ok().map(|state| PeerState { workspaces: state .workspace_ids .iter() .map(|id| WorkspaceDescriptor { id: *id }) .collect(), }) }) .collect() } #[cfg(test)] fn updates(&self) -> NotifyCellObserver<()> { self.updates.observe() } fn take_opened_workspaces(&mut self) -> Option> { self.opened_workspaces_rx.take() } fn connect_to_server( peer_list: Rc>, incoming: S, ) -> Box> where S: 'static + Stream, { Box::new( client::Connection::new(incoming).and_then(move |(connection, peer_service)| { let mut peer_list = peer_list.borrow_mut(); let peer_id = peer_list.next_peer_id; peer_list.next_peer_id += 1; let peer = Peer::new(peer_list.foreground.clone(), peer_service); let peer_updates = peer.updates()?; let peer_list_updates = peer_list.updates.clone(); peer_list .foreground .execute(Box::new(peer_updates.for_each(move |_| { peer_list_updates.set(()); Ok(()) }))) .unwrap(); peer_list.peers.insert(peer_id, peer); peer_list.updates.set(()); // TODO: Eliminate this once we have a UI for the PeerList. peer_list.open_first_workspace(peer_id); Ok(connection) }), ) } fn open_first_workspace(&self, peer_id: PeerId) { if let Some(peer) = self.peers.get(&peer_id) { let opened_workspaces_tx = self.opened_workspaces_tx.clone(); self.foreground .execute(Box::new(peer.open_first_workspace().then( move |result| match result { Ok(Some(workspace)) => { let _ = opened_workspaces_tx.unbounded_send(workspace); Ok(()) } Ok(None) => { eprintln!("No workspaces on remote peer {}", peer_id); Ok(()) } Err(error) => { eprintln!("Error opening remote workspace: {}", error); Ok(()) } }, ))) .unwrap(); } } } impl Peer { fn new(foreground: ForegroundExecutor, service: client::Service) -> Self { Self { foreground, service: client::FullUpdateService::new(service), } } fn updates(&self) -> Result>, rpc::Error> { Ok(Box::new(self.service.updates()?.map(|_| ()))) } fn open_first_workspace( &self, ) -> Box, Error = WorkspaceOpenError>> { match self.service.latest_state() { Ok(state) => if let Some(workspace_id) = state.workspace_ids.first() { self.open_workspace(*workspace_id) } else { Box::new(future::ok(None)) }, Err(error) => Box::new(future::err(error.into())), } } fn open_workspace( &self, workspace_id: WorkspaceId, ) -> Box, Error = WorkspaceOpenError>> { let foreground = self.foreground.clone(); let service = self.service.clone(); Box::new( self.service .request(ServiceRequest::OpenWorkspace(workspace_id)) .map_err(|e| e.into()) .and_then(move |response| { let response = response.map_err(|error| match error { ServiceError::WorkspaceNotFound(id) => WorkspaceOpenError::NotFound(id), }); match response? { ServiceResponse::OpenedWorkspace(service_id) => { let workspace_service = service .take_service(service_id) .map_err(|e| WorkspaceOpenError::from(e))?; let remote_workspace = RemoteWorkspace::new(foreground, workspace_service) .map_err(|e| WorkspaceOpenError::from(e))?; Ok(Some(remote_workspace)) } } }), ) } } impl AppService { fn new(app: Rc>) -> Self { let updates = app.borrow().updates.observe(); Self { app, updates } } fn state(&self) -> ServiceState { ServiceState { workspace_ids: self.app.borrow().workspaces.keys().cloned().collect(), } } } impl server::Service for AppService { type State = ServiceState; type Update = ServiceState; type Request = ServiceRequest; type Response = Result; fn init(&mut self, _connection: &server::Connection) -> Self::State { self.state() } fn poll_update(&mut self, _: &server::Connection) -> Async> { match self.updates.poll() { Ok(Async::Ready(Some(()))) => Async::Ready(Some(self.state())), Ok(Async::Ready(None)) | Err(_) => Async::Ready(None), Ok(Async::NotReady) => Async::NotReady, } } fn request( &mut self, request: Self::Request, connection: &server::Connection, ) -> Option>> { let response = match request { ServiceRequest::OpenWorkspace(workspace_id) => { let app = self.app.borrow(); if let Some(workspace) = app.workspaces.get(&workspace_id) { match workspace { &WorkspaceEntry::Local(ref workspace) => { let service_handle = connection.add_service(WorkspaceService::new(workspace.clone())); Ok(ServiceResponse::OpenedWorkspace( service_handle.service_id(), )) } &WorkspaceEntry::Remote(_) => { Err(ServiceError::WorkspaceNotFound(workspace_id)) } } } else { Err(ServiceError::WorkspaceNotFound(workspace_id)) } } }; Some(Box::new(Ok(response).into_future())) } } impl From for WorkspaceOpenError { fn from(error: rpc::Error) -> Self { WorkspaceOpenError::RpcError(error) } } impl fmt::Display for WorkspaceOpenError { fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { match *self { WorkspaceOpenError::RpcError(ref error) => write!(fmt, "rpc error: {}", error), WorkspaceOpenError::NotFound(workspace_id) => { write!(fmt, "workspace not found for id {}", workspace_id) } } } } #[cfg(test)] mod tests { use super::*; use fs::tests::{TestFileProvider, TestTree}; use futures::{unsync, Future, Sink}; use stream_ext::StreamExt; use tokio_core::reactor; #[test] fn test_remote_workspaces() { let mut reactor = reactor::Core::new().unwrap(); let executor = Rc::new(reactor.handle()); let server = App::new( true, executor.clone(), executor.clone(), TestFileProvider::new(), ); let client = App::new( false, executor.clone(), executor.clone(), TestFileProvider::new(), ); let peer_list = client.borrow().peer_list.clone(); let mut peer_list_updates = peer_list.borrow().updates(); assert_eq!(peer_list.borrow().state(), vec![]); connect(&mut reactor, server.clone(), client.clone()); peer_list_updates.wait_next(&mut reactor); assert_eq!( peer_list.borrow().state(), vec![PeerState { workspaces: vec![] }] ); server .borrow_mut() .open_local_workspace(Vec::::new()); peer_list_updates.wait_next(&mut reactor); assert_eq!( peer_list.borrow().state(), vec![PeerState { workspaces: vec![WorkspaceDescriptor { id: 0 }], }] ); } fn connect(reactor: &mut reactor::Core, server: Rc>, client: Rc>) { let (server_to_client_tx, server_to_client_rx) = unsync::mpsc::unbounded(); let server_to_client_rx = server_to_client_rx.map_err(|_| unreachable!()); let (client_to_server_tx, client_to_server_rx) = unsync::mpsc::unbounded(); let client_to_server_rx = client_to_server_rx.map_err(|_| unreachable!()); let server_outgoing = App::connect_to_client(server, client_to_server_rx); reactor.handle().spawn( server_to_client_tx .send_all(server_outgoing.map_err(|_| unreachable!())) .then(|_| Ok(())), ); let client_future = client.borrow().connect_to_server(server_to_client_rx); let client_outgoing = reactor.run(client_future).unwrap(); reactor.handle().spawn( client_to_server_tx .send_all(client_outgoing.map_err(|_| unreachable!())) .then(|_| Ok(())), ); } } ================================================ FILE: xray_core/src/buffer.rs ================================================ use super::rpc::{client, Error as RpcError}; use super::tree::{self, SeekBias, Tree}; use fs; use futures::{unsync, Future, Stream}; use notify_cell::{NotifyCell, NotifyCellObserver}; use seahash::SeaHasher; use serde::{self, Deserialize, Deserializer, Serialize, Serializer}; use std::cell::RefCell; use std::cmp::{self, Ordering}; use std::collections::{HashMap, HashSet}; use std::fmt; use std::hash::BuildHasherDefault; use std::iter; use std::marker; use std::mem; use std::ops::{Add, AddAssign, Range, Sub}; use std::rc::Rc; use std::sync::Arc; use ForegroundExecutor; use IntoShared; use UserId; pub type ReplicaId = usize; type LocalTimestamp = usize; type LamportTimestamp = usize; pub type SelectionSetId = usize; type SelectionSetVersion = usize; pub type BufferId = usize; #[derive(Clone, Debug, Serialize, Deserialize)] pub struct Version( #[serde(serialize_with = "serialize_arc", deserialize_with = "deserialize_arc")] Arc>, ); #[derive(Eq, PartialEq, Debug, Serialize, Deserialize)] pub enum Error { OffsetOutOfRange, InvalidAnchor, InvalidOperation, SelectionSetNotFound, IoError(String), RpcError(RpcError), } pub struct Buffer { id: BufferId, pub replica_id: ReplicaId, next_replica_id: Option, local_clock: LocalTimestamp, lamport_clock: LamportTimestamp, fragments: Tree, insertion_splits: HashMap>, anchor_cache: RefCell>>, offset_cache: RefCell>>, pub version: Version, client: Option>, operation_txs: Vec>>, updates: NotifyCell<()>, next_local_selection_set_id: SelectionSetId, selections: HashMap<(ReplicaId, SelectionSetId), SelectionSet, BuildHasherDefault>, file: Option>, } pub struct BufferSnapshot { fragments: Tree, } #[derive(Clone, Copy, Eq, PartialEq, Debug, Serialize, Hash)] pub struct Point { pub row: u32, pub column: u32, } #[derive(Clone, Eq, PartialEq, Debug, Hash, Serialize, Deserialize)] pub struct Anchor(AnchorInner); #[derive(Clone, Eq, PartialEq, Debug, Hash, Serialize, Deserialize)] enum AnchorInner { Start, End, Middle { insertion_id: EditId, offset: usize, bias: AnchorBias, }, } #[derive(Clone, Eq, PartialEq, Debug, Hash, Serialize, Deserialize)] enum AnchorBias { Left, Right, } #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] pub struct Selection { pub start: Anchor, pub end: Anchor, pub reversed: bool, pub goal_column: Option, } struct SelectionSet { user_id: UserId, selections: Vec, version: SelectionSetVersion, } #[derive(Serialize, Deserialize)] pub struct SelectionSetState { user_id: UserId, selections: Vec, } pub struct Iter<'a> { fragment_cursor: tree::Cursor<'a, Fragment>, fragment_offset: usize, } pub struct BackwardIter<'a> { fragment_cursor: tree::Cursor<'a, Fragment>, fragment_offset: usize, } #[derive(Clone, Eq, PartialEq, Debug, Serialize, Deserialize)] pub struct Insertion { id: EditId, parent_id: EditId, offset_in_parent: usize, replica_id: ReplicaId, #[serde(serialize_with = "serialize_arc", deserialize_with = "deserialize_arc")] text: Arc, timestamp: LamportTimestamp, } #[derive(Serialize, Deserialize)] pub struct Deletion { start_id: EditId, start_offset: usize, end_id: EditId, end_offset: usize, version_in_range: Version, } #[derive(Clone, Eq, PartialEq, Debug, Serialize, Deserialize)] pub struct Text { code_units: Vec, nodes: Vec, } #[derive(Clone, Eq, PartialEq, Debug, Serialize, Deserialize)] struct LineNode { len: u32, longest_row: u32, longest_row_len: u32, offset: usize, rows: u32, } struct LineNodeProbe<'a> { offset_range: &'a Range, row: u32, left_ancestor_end_offset: usize, right_ancestor_start_offset: usize, node: &'a LineNode, left_child: Option<&'a LineNode>, right_child: Option<&'a LineNode>, } #[derive(Hash, Eq, PartialEq, Clone, Copy, Debug, Serialize, Deserialize)] pub struct EditId { replica_id: ReplicaId, timestamp: LocalTimestamp, } #[derive(Ord, PartialOrd, Eq, PartialEq, Clone, Debug, Serialize, Deserialize)] struct FragmentId( #[serde(serialize_with = "serialize_arc")] #[serde(deserialize_with = "deserialize_arc")] Arc>, ); #[derive(Eq, PartialEq, Clone, Debug)] struct Fragment { id: FragmentId, insertion: Insertion, start_offset: usize, end_offset: usize, deletions: HashSet, } #[derive(Eq, PartialEq, Clone, Debug)] pub struct FragmentSummary { extent: usize, extent_2d: Point, max_fragment_id: FragmentId, first_row_len: u32, longest_row: u32, longest_row_len: u32, } #[derive(Ord, PartialOrd, Eq, PartialEq, Clone, Copy, Debug)] struct CharacterCount(usize); #[derive(Eq, PartialEq, Clone, Debug, Serialize, Deserialize)] struct InsertionSplit { extent: usize, fragment_id: FragmentId, } #[derive(Eq, PartialEq, Clone, Debug)] struct InsertionSplitSummary { extent: usize, } #[derive(Ord, PartialOrd, Eq, PartialEq, Clone, Copy, Debug)] struct InsertionOffset(usize); #[derive(Debug, Serialize, Deserialize)] pub enum Operation { Edit { id: EditId, start_id: EditId, start_offset: usize, end_id: EditId, end_offset: usize, version_in_range: Version, timestamp: LamportTimestamp, #[serde(serialize_with = "serialize_option_arc")] #[serde(deserialize_with = "deserialize_option_arc")] new_text: Option>, }, } impl Version { fn new() -> Self { Version(Arc::new(HashMap::new())) } fn set(&mut self, replica_id: ReplicaId, timestamp: LocalTimestamp) { let map = Arc::make_mut(&mut self.0); *map.entry(replica_id).or_insert(0) = timestamp; } fn include(&mut self, insertion: &Insertion) { let map = Arc::make_mut(&mut self.0); let value = map.entry(insertion.id.replica_id).or_insert(0); *value = cmp::max(*value, insertion.id.timestamp); } fn includes(&self, insertion: &Insertion) -> bool { if let Some(timestamp) = self.0.get(&insertion.id.replica_id) { *timestamp >= insertion.id.timestamp } else { false } } } pub mod rpc { use super::{ Buffer, BufferId, EditId, FragmentId, Insertion, InsertionSplit, Operation, ReplicaId, SelectionSetId, SelectionSetState, SelectionSetVersion, Version, }; use futures::{Async, Future, Stream}; use never::Never; use notify_cell::NotifyCellObserver; use rpc; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use std::cell::RefCell; use std::collections::{HashMap, HashSet}; use std::rc::Rc; use std::sync::Arc; #[derive(Serialize, Deserialize)] pub struct State { pub(super) id: BufferId, pub(super) replica_id: ReplicaId, pub(super) fragments: Vec, pub(super) insertions: HashMap, pub(super) insertion_splits: HashMap>, pub(super) version: Version, pub(super) selections: HashMap<(ReplicaId, SelectionSetId), SelectionSetState>, } #[derive(Serialize, Deserialize)] pub enum Request { Operation( #[serde(serialize_with = "serialize_op", deserialize_with = "deserialize_op")] Arc, ), UpdateSelectionSet(SelectionSetId, SelectionSetState), RemoveSelectionSet(SelectionSetId), Save, } #[derive(Serialize, Deserialize)] pub enum Response { Saved, Error(super::Error), } #[derive(Serialize, Deserialize)] pub enum Update { Operation( #[serde(serialize_with = "serialize_op", deserialize_with = "deserialize_op")] Arc, ), Selections { updated: HashMap<(ReplicaId, SelectionSetId), SelectionSetState>, removed: HashSet<(ReplicaId, SelectionSetId)>, }, } #[derive(Serialize, Deserialize)] pub(super) struct Fragment { pub id: FragmentId, pub insertion_id: EditId, pub start_offset: usize, pub end_offset: usize, pub deletions: HashSet, } pub struct Service { replica_id: ReplicaId, buffer_updates: NotifyCellObserver<()>, outgoing_ops: Box, Error = ()>>, selection_set_versions: HashMap<(ReplicaId, SelectionSetId), SelectionSetVersion>, buffer: Rc>, } impl Service { pub fn new(buffer: Rc>) -> Self { let replica_id = buffer .borrow_mut() .next_replica_id() .expect("Cannot replicate a remote buffer"); let outgoing_ops = buffer .borrow_mut() .outgoing_ops() .filter(move |op| op.replica_id() != replica_id); let buffer_updates = buffer.borrow().updates(); let selection_set_versions = buffer .borrow() .selections .iter() .map(|(key, set)| (*key, set.version)) .collect(); Self { replica_id, buffer_updates, outgoing_ops: Box::new(outgoing_ops), selection_set_versions, buffer, } } fn poll_outgoing_op(&mut self) -> Async> { self.outgoing_ops .poll() .expect("Receiving on a channel cannot produce an error") .map(|option| option.map(|update| Update::Operation(update))) } fn poll_outgoing_selection_updates(&mut self) -> Async> { loop { match self.buffer_updates .poll() .expect("Polling a NotifyCellObserver cannot produce an error") { Async::NotReady => return Async::NotReady, Async::Ready(None) => unreachable!(), Async::Ready(Some(())) => { let mut removed = HashSet::new(); let mut updated = HashMap::new(); let buffer = self.buffer.borrow(); self.selection_set_versions .retain(|id, last_polled_version| { if let Some(selection_set) = buffer.selections.get(id) { if selection_set.version > *last_polled_version { *last_polled_version = selection_set.version; updated.insert(*id, selection_set.state()); } true } else { removed.insert(*id); false } }); for ((replica_id, set_id), selection_set) in &buffer.selections { if *replica_id != self.replica_id { self.selection_set_versions .entry((*replica_id, *set_id)) .or_insert_with(|| { updated .insert((*replica_id, *set_id), selection_set.state()); selection_set.version }); } } if updated.len() > 0 || removed.len() > 0 { return Async::Ready(Some(Update::Selections { updated, removed })); } } } } } } impl rpc::server::Service for Service { type State = State; type Update = Update; type Request = Request; type Response = Response; fn init(&mut self, _: &rpc::server::Connection) -> Self::State { let buffer = self.buffer.borrow_mut(); let mut state = State { id: buffer.id, replica_id: self.replica_id, fragments: Vec::new(), insertions: HashMap::new(), insertion_splits: HashMap::new(), version: buffer.version.clone(), selections: HashMap::new(), }; for fragment in buffer.fragments.iter() { state .insertions .entry(fragment.insertion.id) .or_insert_with(|| fragment.insertion.clone()); state.fragments.push(Fragment { id: fragment.id.clone(), insertion_id: fragment.insertion.id, start_offset: fragment.start_offset, end_offset: fragment.end_offset, deletions: fragment.deletions.clone(), }); } for (insertion_id, splits) in &buffer.insertion_splits { state .insertion_splits .insert(*insertion_id, splits.iter().cloned().collect()); } state.selections = HashMap::new(); for (id, selection_set) in &buffer.selections { state.selections.insert(*id, selection_set.state()); } state } fn poll_update(&mut self, _: &rpc::server::Connection) -> Async> { match self.poll_outgoing_op() { Async::Ready(Some(update)) => Async::Ready(Some(update)), Async::Ready(None) => match self.poll_outgoing_selection_updates() { Async::Ready(Some(update)) => Async::Ready(Some(update)), Async::Ready(None) => Async::Ready(None), Async::NotReady => Async::NotReady, }, Async::NotReady => match self.poll_outgoing_selection_updates() { Async::Ready(Some(update)) => Async::Ready(Some(update)), Async::Ready(None) | Async::NotReady => Async::NotReady, }, } } fn request( &mut self, request: Self::Request, _connection: &rpc::server::Connection, ) -> Option>> { match request { Request::Operation(op) => { let mut buffer = self.buffer.borrow_mut(); buffer.broadcast_op(&op); if buffer.integrate_op(op).is_err() { unimplemented!("Invalid op: terminate the service and respond with error?"); } None } Request::UpdateSelectionSet(set_id, state) => { self.buffer.borrow_mut().update_remote_selection_set( self.replica_id, set_id, state, ); None } Request::RemoveSelectionSet(set_id) => { self.buffer .borrow_mut() .remove_remote_selection_set(self.replica_id, set_id); None } Request::Save => Some(Box::new(self.buffer.borrow().save().then(|result| { match result { Ok(_) => Ok(Response::Saved), Err(error) => Ok(Response::Error(error)), } }))), } } } impl Drop for Service { fn drop(&mut self) { self.buffer .borrow_mut() .remove_remote_selection_sets(self.replica_id); } } fn serialize_op(op: &Arc, serializer: S) -> Result { op.serialize(serializer) } fn deserialize_op<'de, D: Deserializer<'de>>( deserializer: D, ) -> Result, D::Error> { Ok(Arc::new(Operation::deserialize(deserializer)?)) } } impl Buffer { pub fn new(id: BufferId) -> Self { let mut fragments = Tree::new(); // Push start sentinel. let sentinel_id = EditId { replica_id: 0, timestamp: 0, }; fragments.push(Fragment::new( FragmentId::min_value(), Insertion { id: sentinel_id, parent_id: EditId { replica_id: 0, timestamp: 0, }, offset_in_parent: 0, replica_id: 0, text: Arc::new(Text::new(vec![])), timestamp: 0, }, )); let mut insertion_splits = HashMap::new(); insertion_splits.insert( sentinel_id, Tree::from_item(InsertionSplit { fragment_id: FragmentId::min_value(), extent: 0, }), ); Self { id, replica_id: 1, next_replica_id: Some(2), local_clock: 0, lamport_clock: 0, fragments, insertion_splits, anchor_cache: RefCell::new(HashMap::default()), offset_cache: RefCell::new(HashMap::default()), version: Version::new(), client: None, operation_txs: Vec::new(), updates: NotifyCell::new(()), selections: HashMap::default(), next_local_selection_set_id: 0, file: None, } } pub fn remote( foreground: ForegroundExecutor, client: client::Service, ) -> Result>, RpcError> { let state = client.state()?; let incoming_updates = client.updates()?; let mut insertions = HashMap::new(); for (edit_id, insertion) in state.insertions { insertions.insert(edit_id, insertion); } let mut fragments = Tree::new(); fragments.extend(state.fragments.into_iter().map(|fragment| Fragment { id: fragment.id, insertion: insertions.get(&fragment.insertion_id).unwrap().clone(), start_offset: fragment.start_offset, end_offset: fragment.end_offset, deletions: fragment.deletions, })); let mut insertion_splits = HashMap::new(); for (insertion_id, splits) in state.insertion_splits { let mut split_tree = Tree::new(); split_tree.extend(splits); insertion_splits.insert(insertion_id, split_tree); } let mut selection_sets = HashMap::default(); for (id, state) in state.selections { selection_sets.insert( id, SelectionSet { user_id: state.user_id, selections: state.selections, version: 0, }, ); } let buffer = Buffer { id: state.id, replica_id: state.replica_id, next_replica_id: None, local_clock: 0, lamport_clock: 0, fragments, insertion_splits, anchor_cache: RefCell::new(HashMap::default()), offset_cache: RefCell::new(HashMap::default()), version: state.version, client: Some(client), operation_txs: Vec::new(), updates: NotifyCell::new(()), selections: selection_sets, next_local_selection_set_id: 0, file: None, }.into_shared(); let buffer_weak = Rc::downgrade(&buffer); foreground .execute(Box::new(incoming_updates.for_each(move |update| { if let Some(buffer) = buffer_weak.upgrade() { let mut buffer = buffer.borrow_mut(); match update { rpc::Update::Operation(operation) => { if buffer.integrate_op(operation).is_err() { unimplemented!("Invalid op"); } } rpc::Update::Selections { updated, removed } => { for ((replica_id, set_id), state) in updated { debug_assert!(replica_id != buffer.replica_id); buffer.update_remote_selection_set(replica_id, set_id, state); } for (replica_id, set_id) in removed { debug_assert!(replica_id != buffer.replica_id); buffer.remove_remote_selection_set(replica_id, set_id); } } } } Ok(()) }))) .unwrap(); Ok(buffer) } pub fn id(&self) -> BufferId { self.id } pub fn next_replica_id(&mut self) -> Result { let replica_id = self.next_replica_id.ok_or(())?; self.next_replica_id = Some(replica_id + 1); Ok(replica_id) } pub fn file_id(&self) -> Option { self.file.as_ref().map(|file| file.id()) } pub fn set_file(&mut self, file: Box) { self.file = Some(file); } pub fn save(&self) -> Option>> { use std::error; if let Some(ref client) = self.client { Some(Box::new(client.request(rpc::Request::Save).then( |response| match response { Ok(rpc::Response::Saved) => Ok(()), Ok(rpc::Response::Error(error)) => Err(error), Err(error) => Err(Error::RpcError(error)), }, ))) } else { self.file.as_ref().map(|file| { Box::new( file.write_snapshot(self.snapshot()).map_err(|error| { Error::IoError(error::Error::description(&error).to_owned()) }), ) as Box> }) } } pub fn len(&self) -> usize { self.fragments.len::().0 } pub fn len_for_row(&self, row: u32) -> Result { let row_start_offset = self.offset_for_point(Point::new(row, 0))?; let row_end_offset = if row >= self.max_point().row { self.len() } else { self.offset_for_point(Point::new(row + 1, 0))? - 1 }; Ok((row_end_offset - row_start_offset) as u32) } pub fn longest_row(&self) -> u32 { self.fragments.summary().longest_row } pub fn max_point(&self) -> Point { self.fragments.len::() } pub fn line(&self, row: u32) -> Result, Error> { let mut iterator = self.iter_starting_at_point(Point::new(row, 0)).peekable(); if iterator.peek().is_none() { Err(Error::OffsetOutOfRange) } else { Ok(iterator.take_while(|c| *c != u16::from(b'\n')).collect()) } } pub fn snapshot(&self) -> BufferSnapshot { BufferSnapshot { fragments: self.fragments.clone(), } } pub fn to_u16_chars(&self) -> Vec { let mut result = Vec::with_capacity(self.len()); result.extend(self.iter()); result } #[cfg(test)] pub fn to_string(&self) -> String { String::from_utf16_lossy(self.iter().collect::>().as_slice()) } pub fn iter(&self) -> Iter { Iter::new(self) } pub fn iter_starting_at_point(&self, point: Point) -> Iter { Iter::starting_at_point(self, point) } pub fn backward_iter_starting_at_point(&self, point: Point) -> BackwardIter { BackwardIter::starting_at_point(self, point) } pub fn edit<'a, I, T>(&mut self, old_ranges: I, new_text: T) -> Vec> where I: IntoIterator>, T: Into, { let new_text = new_text.into(); let new_text = if new_text.len() > 0 { Some(Arc::new(new_text)) } else { None }; self.anchor_cache.borrow_mut().clear(); self.offset_cache.borrow_mut().clear(); let ops = self.splice_fragments( old_ranges .into_iter() .filter(|old_range| new_text.is_some() || old_range.end > old_range.start), new_text.clone(), ); for op in &ops { self.broadcast_op(op); } self.version.set(self.replica_id, self.local_clock); self.updates.set(()); ops } pub fn add_selection_set( &mut self, user_id: UserId, selections: Vec, ) -> SelectionSetId { let id = self.next_local_selection_set_id; let set = SelectionSet { version: 0, selections, user_id, }; if let Some(ref client) = self.client { client.request(rpc::Request::UpdateSelectionSet(id, set.state())); } self.next_local_selection_set_id += 1; self.selections.insert((self.replica_id, id), set); self.updates.set(()); id } pub fn remove_selection_set(&mut self, id: SelectionSetId) -> Result<(), ()> { if let Some(ref client) = self.client { client.request(rpc::Request::RemoveSelectionSet(id)); } self.selections.remove(&(self.replica_id, id)).ok_or(())?; self.updates.set(()); Ok(()) } pub fn selections(&self, set_id: SelectionSetId) -> Result<&[Selection], ()> { self.selections .get(&(self.replica_id, set_id)) .ok_or(()) .map(|set| set.selections.as_slice()) } pub fn insert_selections(&mut self, set_id: SelectionSetId, f: F) -> Result<(), Error> where F: FnOnce(&Buffer, &[Selection]) -> Vec, { self.mutate_selections(set_id, |buffer, old_selections| { let mut new_selections = f(buffer, old_selections); new_selections.sort_unstable_by(|a, b| buffer.cmp_anchors(&a.start, &b.start).unwrap()); let mut selections = Vec::with_capacity(old_selections.len() + new_selections.len()); { let mut old_selections = old_selections.drain(..).peekable(); let mut new_selections = new_selections.drain(..).peekable(); loop { if old_selections.peek().is_some() { if new_selections.peek().is_some() { match buffer .cmp_anchors( &old_selections.peek().unwrap().start, &new_selections.peek().unwrap().start, ) .unwrap() { Ordering::Less => { selections.push(old_selections.next().unwrap()); } Ordering::Equal => { selections.push(old_selections.next().unwrap()); selections.push(new_selections.next().unwrap()); } Ordering::Greater => { selections.push(new_selections.next().unwrap()); } } } else { selections.push(old_selections.next().unwrap()); } } else if new_selections.peek().is_some() { selections.push(new_selections.next().unwrap()); } else { break; } } } *old_selections = selections; }) } pub fn mutate_selections(&mut self, set_id: SelectionSetId, f: F) -> Result<(), Error> where F: FnOnce(&Buffer, &mut Vec), { let id = (self.replica_id, set_id); let mut set = self.selections .remove(&id) .ok_or(Error::SelectionSetNotFound)?; f(self, &mut set.selections); self.merge_selections(&mut set.selections); set.version += 1; if let Some(ref client) = self.client { client.request(rpc::Request::UpdateSelectionSet(id.1, set.state())); } self.selections.insert(id, set); self.updates.set(()); Ok(()) } fn merge_selections(&mut self, selections: &mut Vec) { let mut new_selections = Vec::with_capacity(selections.len()); { let mut old_selections = selections.drain(..); if let Some(mut prev_selection) = old_selections.next() { for selection in old_selections { if self.cmp_anchors(&prev_selection.end, &selection.start) .unwrap() >= Ordering::Equal { if self.cmp_anchors(&selection.end, &prev_selection.end) .unwrap() > Ordering::Equal { prev_selection.end = selection.end; } } else { new_selections.push(mem::replace(&mut prev_selection, selection)); } } new_selections.push(prev_selection); } } *selections = new_selections; } pub fn remote_selections(&self) -> impl Iterator { let local_replica_id = self.replica_id; self.selections .iter() .filter_map(move |((replica_id, _), set)| { if *replica_id != local_replica_id { Some((set.user_id, set.selections.as_slice())) } else { None } }) } pub fn updates(&self) -> NotifyCellObserver<()> { self.updates.observe() } fn broadcast_op(&mut self, op: &Arc) { for i in (0..self.operation_txs.len()).rev() { if self.operation_txs[i].unbounded_send(op.clone()).is_err() { self.operation_txs.swap_remove(i); } } if let Some(ref client) = self.client { client.request(rpc::Request::Operation(op.clone())); } } fn integrate_op(&mut self, op: Arc) -> Result<(), Error> { match op.as_ref() { &Operation::Edit { ref id, ref start_id, ref start_offset, ref end_id, ref end_offset, ref new_text, ref version_in_range, ref timestamp, } => self.integrate_edit( *id, *start_id, *start_offset, *end_id, *end_offset, new_text.as_ref().cloned(), version_in_range, *timestamp, )?, } self.anchor_cache.borrow_mut().clear(); self.offset_cache.borrow_mut().clear(); self.updates.set(()); Ok(()) } fn integrate_edit( &mut self, id: EditId, start_id: EditId, start_offset: usize, end_id: EditId, end_offset: usize, new_text: Option>, version_in_range: &Version, timestamp: LamportTimestamp, ) -> Result<(), Error> { let mut new_text = new_text.as_ref().cloned(); let start_fragment_id = self.resolve_fragment_id(start_id, start_offset)?; let end_fragment_id = self.resolve_fragment_id(end_id, end_offset)?; let old_fragments = self.fragments.clone(); let mut cursor = old_fragments.cursor(); let mut new_fragments = cursor.slice(&start_fragment_id, SeekBias::Left); if start_offset == cursor.item().unwrap().end_offset { new_fragments.push(cursor.item().unwrap().clone()); cursor.next(); } while let Some(fragment) = cursor.item() { if new_text.is_none() && fragment.id > end_fragment_id { break; } if fragment.id == start_fragment_id || fragment.id == end_fragment_id { let split_start = if start_fragment_id == fragment.id { start_offset } else { fragment.start_offset }; let split_end = if end_fragment_id == fragment.id { end_offset } else { fragment.end_offset }; let (before_range, within_range, after_range) = self.split_fragment( cursor.prev_item().unwrap(), fragment, split_start..split_end, ); let insertion = new_text.take().map(|new_text| { self.build_fragment_to_insert( id, before_range.as_ref().or(cursor.prev_item()).unwrap(), within_range.as_ref().or(after_range.as_ref()), new_text, timestamp, ) }); if let Some(fragment) = before_range { new_fragments.push(fragment); } if let Some(fragment) = insertion { new_fragments.push(fragment); } if let Some(mut fragment) = within_range { if version_in_range.includes(&fragment.insertion) { fragment.deletions.insert(id); } new_fragments.push(fragment); } if let Some(fragment) = after_range { new_fragments.push(fragment); } } else { if new_text.is_some() && should_insert_before(&fragment.insertion, timestamp, id.replica_id) { new_fragments.push(self.build_fragment_to_insert( id, cursor.prev_item().unwrap(), Some(fragment), new_text.take().unwrap(), timestamp, )); } let mut fragment = fragment.clone(); if version_in_range.includes(&fragment.insertion) { fragment.deletions.insert(id); } new_fragments.push(fragment); } cursor.next(); } if let Some(new_text) = new_text { new_fragments.push(self.build_fragment_to_insert( id, cursor.prev_item().unwrap(), None, new_text, timestamp, )); } new_fragments .push_tree(cursor.slice(&old_fragments.len::(), SeekBias::Right)); self.fragments = new_fragments; self.lamport_clock = cmp::max(self.lamport_clock, timestamp) + 1; Ok(()) } fn update_remote_selection_set( &mut self, replica_id: ReplicaId, set_id: SelectionSetId, state: SelectionSetState, ) { let set = self.selections .entry((replica_id, set_id)) .or_insert(SelectionSet { user_id: state.user_id, selections: Vec::new(), version: 0, }); set.version += 1; set.selections = state.selections; self.updates.set(()); } fn remove_remote_selection_set(&mut self, replica_id: ReplicaId, set_id: SelectionSetId) { self.selections.remove(&(replica_id, set_id)); self.updates.set(()); } fn remove_remote_selection_sets(&mut self, id: ReplicaId) { self.selections .retain(|(replica_id, _), _| *replica_id != id); self.updates.set(()); } fn resolve_fragment_id(&self, edit_id: EditId, offset: usize) -> Result { let split_tree = self.insertion_splits .get(&edit_id) .ok_or(Error::InvalidOperation)?; let mut cursor = split_tree.cursor(); cursor.seek(&InsertionOffset(offset), SeekBias::Left); Ok(cursor .item() .ok_or(Error::InvalidOperation)? .fragment_id .clone()) } fn outgoing_ops(&mut self) -> unsync::mpsc::UnboundedReceiver> { let (tx, rx) = unsync::mpsc::unbounded(); self.operation_txs.push(tx); rx } fn splice_fragments<'a, I>( &mut self, mut old_ranges: I, new_text: Option>, ) -> Vec> where I: Iterator>, { let mut cur_range = old_ranges.next(); if cur_range.is_none() { return Vec::new(); } let replica_id = self.replica_id; let mut ops = Vec::with_capacity(old_ranges.size_hint().0); let old_fragments = self.fragments.clone(); let mut cursor = old_fragments.cursor(); let mut new_fragments = Tree::new(); new_fragments.push_tree(cursor.slice( &CharacterCount(cur_range.as_ref().unwrap().start), SeekBias::Right, )); self.local_clock += 1; self.lamport_clock += 1; let mut start_id = None; let mut start_offset = None; let mut end_id = None; let mut end_offset = None; let mut version_in_range = Version::new(); while cur_range.is_some() && cursor.item().is_some() { let mut fragment = cursor.item().unwrap().clone(); let mut fragment_start = cursor.start::().0; let mut fragment_end = fragment_start + fragment.len(); let old_split_tree = self.insertion_splits .remove(&fragment.insertion.id) .unwrap(); let mut splits_cursor = old_split_tree.cursor(); let mut new_split_tree = splits_cursor.slice(&InsertionOffset(fragment.start_offset), SeekBias::Right); // Find all splices that start or end within the current fragment. Then, split the // fragment and reassemble it in both trees accounting for the deleted and the newly // inserted text. while cur_range.map_or(false, |range| range.start < fragment_end) { let range = cur_range.clone().unwrap(); if range.start > fragment_start { let mut prefix = fragment.clone(); prefix.end_offset = prefix.start_offset + (range.start - fragment_start); prefix.id = FragmentId::between(&new_fragments.last().unwrap().id, &fragment.id); fragment.start_offset = prefix.end_offset; new_fragments.push(prefix.clone()); new_split_tree.push(InsertionSplit { extent: prefix.end_offset - prefix.start_offset, fragment_id: prefix.id, }); fragment_start = range.start; } if range.end == fragment_start { end_id = Some(new_fragments.last().unwrap().insertion.id); end_offset = Some(new_fragments.last().unwrap().end_offset); } else if range.end == fragment_end { end_id = Some(fragment.insertion.id); end_offset = Some(fragment.end_offset); } if range.start == fragment_start { let local_timestamp = self.local_clock; let lamport_timestamp = self.lamport_clock; start_id = Some(new_fragments.last().unwrap().insertion.id); start_offset = Some(new_fragments.last().unwrap().end_offset); if let Some(new_text) = new_text.clone() { let new_fragment = self.build_fragment_to_insert( EditId { replica_id, timestamp: local_timestamp, }, new_fragments.last().unwrap(), Some(&fragment), new_text, lamport_timestamp, ); new_fragments.push(new_fragment); } } if range.end < fragment_end { if range.end > fragment_start { let mut prefix = fragment.clone(); prefix.end_offset = prefix.start_offset + (range.end - fragment_start); prefix.id = FragmentId::between(&new_fragments.last().unwrap().id, &fragment.id); if fragment.is_visible() { prefix.deletions.insert(EditId { replica_id, timestamp: self.local_clock, }); } fragment.start_offset = prefix.end_offset; new_fragments.push(prefix.clone()); new_split_tree.push(InsertionSplit { extent: prefix.end_offset - prefix.start_offset, fragment_id: prefix.id, }); fragment_start = range.end; end_id = Some(fragment.insertion.id); end_offset = Some(fragment.start_offset); version_in_range.include(&fragment.insertion); } } else { version_in_range.include(&fragment.insertion); if fragment.is_visible() { fragment.deletions.insert(EditId { replica_id, timestamp: self.local_clock, }); } } // If the splice ends inside this fragment, we can advance to the next splice and // check if it also intersects the current fragment. Otherwise we break out of the // loop and find the first fragment that the splice does not contain fully. if range.end <= fragment_end { ops.push(Arc::new(Operation::Edit { id: EditId { replica_id, timestamp: self.local_clock, }, start_id: start_id.unwrap(), start_offset: start_offset.unwrap(), end_id: end_id.unwrap(), end_offset: end_offset.unwrap(), new_text: new_text.clone(), timestamp: self.lamport_clock, version_in_range, })); start_id = None; start_offset = None; end_id = None; end_offset = None; version_in_range = Version::new(); cur_range = old_ranges.next(); if cur_range.is_some() { self.local_clock += 1; self.lamport_clock += 1; } } else { break; } } new_split_tree.push(InsertionSplit { extent: fragment.end_offset - fragment.start_offset, fragment_id: fragment.id.clone(), }); splits_cursor.next(); new_split_tree.push_tree( splits_cursor.slice(&old_split_tree.len::(), SeekBias::Right), ); self.insertion_splits .insert(fragment.insertion.id, new_split_tree); new_fragments.push(fragment); // Scan forward until we find a fragment that is not fully contained by the current splice. cursor.next(); if let Some(range) = cur_range.clone() { while let Some(mut fragment) = cursor.item().cloned() { fragment_start = cursor.start::().0; fragment_end = fragment_start + fragment.len(); if range.start < fragment_start && range.end >= fragment_end { if fragment.is_visible() { fragment.deletions.insert(EditId { replica_id, timestamp: self.local_clock, }); } version_in_range.include(&fragment.insertion); new_fragments.push(fragment.clone()); cursor.next(); if range.end == fragment_end { end_id = Some(fragment.insertion.id); end_offset = Some(fragment.end_offset); ops.push(Arc::new(Operation::Edit { id: EditId { replica_id, timestamp: self.local_clock, }, start_id: start_id.unwrap(), start_offset: start_offset.unwrap(), end_id: end_id.unwrap(), end_offset: end_offset.unwrap(), new_text: new_text.clone(), timestamp: self.lamport_clock, version_in_range, })); start_id = None; start_offset = None; end_id = None; end_offset = None; version_in_range = Version::new(); cur_range = old_ranges.next(); if cur_range.is_some() { self.local_clock += 1; self.lamport_clock += 1; } break; } } else { break; } } // If the splice we are currently evaluating starts after the end of the fragment // that the cursor is parked at, we should seek to the next splice's start range // and push all the fragments in between into the new tree. if cur_range.map_or(false, |range| range.start > fragment_end) { new_fragments.push_tree(cursor.slice( &CharacterCount(cur_range.as_ref().unwrap().start), SeekBias::Right, )); } } } // Handle range that is at the end of the buffer if it exists. There should never be // multiple because ranges must be disjoint. if cur_range.is_some() { debug_assert_eq!(old_ranges.next(), None); let local_timestamp = self.local_clock; let lamport_timestamp = self.lamport_clock; let id = EditId { replica_id, timestamp: local_timestamp, }; ops.push(Arc::new(Operation::Edit { id, start_id: new_fragments.last().unwrap().insertion.id, start_offset: new_fragments.last().unwrap().end_offset, end_id: new_fragments.last().unwrap().insertion.id, end_offset: new_fragments.last().unwrap().end_offset, new_text: new_text.clone(), timestamp: lamport_timestamp, version_in_range: Version::new(), })); if let Some(new_text) = new_text { let new_fragment = self.build_fragment_to_insert( id, new_fragments.last().unwrap(), None, new_text, lamport_timestamp, ); new_fragments.push(new_fragment); } } else { new_fragments .push_tree(cursor.slice(&old_fragments.len::(), SeekBias::Right)); } self.fragments = new_fragments; ops } fn split_fragment( &mut self, prev_fragment: &Fragment, fragment: &Fragment, range: Range, ) -> (Option, Option, Option) { debug_assert!(range.start >= fragment.start_offset); debug_assert!(range.start <= fragment.end_offset); debug_assert!(range.end <= fragment.end_offset); debug_assert!(range.end >= fragment.start_offset); if range.end == fragment.start_offset { (None, None, Some(fragment.clone())) } else if range.start == fragment.end_offset { (Some(fragment.clone()), None, None) } else if range.start == fragment.start_offset && range.end == fragment.end_offset { (None, Some(fragment.clone()), None) } else { let mut prefix = fragment.clone(); let after_range = if range.end < fragment.end_offset { let mut suffix = prefix.clone(); suffix.start_offset = range.end; prefix.end_offset = range.end; prefix.id = FragmentId::between(&prev_fragment.id, &suffix.id); Some(suffix) } else { None }; let within_range = if range.start != range.end { let mut suffix = prefix.clone(); suffix.start_offset = range.start; prefix.end_offset = range.start; prefix.id = FragmentId::between(&prev_fragment.id, &suffix.id); Some(suffix) } else { None }; let before_range = if range.start > fragment.start_offset { Some(prefix) } else { None }; let old_split_tree = self.insertion_splits .remove(&fragment.insertion.id) .unwrap(); let mut cursor = old_split_tree.cursor(); let mut new_split_tree = cursor.slice(&InsertionOffset(fragment.start_offset), SeekBias::Right); if let Some(ref fragment) = before_range { new_split_tree.push(InsertionSplit { extent: range.start - fragment.start_offset, fragment_id: fragment.id.clone(), }) } if let Some(ref fragment) = within_range { new_split_tree.push(InsertionSplit { extent: range.end - range.start, fragment_id: fragment.id.clone(), }) } if let Some(ref fragment) = after_range { new_split_tree.push(InsertionSplit { extent: fragment.end_offset - range.end, fragment_id: fragment.id.clone(), }) } cursor.next(); new_split_tree .push_tree(cursor.slice(&old_split_tree.len::(), SeekBias::Right)); self.insertion_splits .insert(fragment.insertion.id, new_split_tree); (before_range, within_range, after_range) } } fn build_fragment_to_insert( &mut self, edit_id: EditId, prev_fragment: &Fragment, next_fragment: Option<&Fragment>, text: Arc, timestamp: LamportTimestamp, ) -> Fragment { let new_fragment_id = FragmentId::between( &prev_fragment.id, next_fragment .map(|f| &f.id) .unwrap_or(&FragmentId::max_value()), ); let mut split_tree = Tree::new(); split_tree.push(InsertionSplit { extent: text.len(), fragment_id: new_fragment_id.clone(), }); self.insertion_splits.insert(edit_id, split_tree); Fragment::new( new_fragment_id, Insertion { id: edit_id, parent_id: prev_fragment.insertion.id, offset_in_parent: prev_fragment.end_offset, replica_id: self.replica_id, text, timestamp, }, ) } pub fn anchor_before_offset(&self, offset: usize) -> Result { self.anchor_for_offset(offset, AnchorBias::Left) } pub fn anchor_after_offset(&self, offset: usize) -> Result { self.anchor_for_offset(offset, AnchorBias::Right) } fn anchor_for_offset(&self, offset: usize, bias: AnchorBias) -> Result { let max_offset = self.len(); if offset > max_offset { return Err(Error::OffsetOutOfRange); } let seek_bias; match bias { AnchorBias::Left => { if offset == 0 { return Ok(Anchor(AnchorInner::Start)); } else { seek_bias = SeekBias::Left; } } AnchorBias::Right => { if offset == max_offset { return Ok(Anchor(AnchorInner::End)); } else { seek_bias = SeekBias::Right; } } }; let mut cursor = self.fragments.cursor(); cursor.seek(&CharacterCount(offset), seek_bias); let fragment = cursor.item().unwrap(); let offset_in_fragment = offset - cursor.start::().0; let offset_in_insertion = fragment.start_offset + offset_in_fragment; let point = cursor.start::() + &fragment.point_for_offset(offset_in_fragment)?; let anchor = Anchor(AnchorInner::Middle { insertion_id: fragment.insertion.id, offset: offset_in_insertion, bias, }); self.cache_position(Some(anchor.clone()), offset, point); Ok(anchor) } pub fn anchor_before_point(&self, point: Point) -> Result { self.anchor_for_point(point, AnchorBias::Left) } pub fn anchor_after_point(&self, point: Point) -> Result { self.anchor_for_point(point, AnchorBias::Right) } fn anchor_for_point(&self, point: Point, bias: AnchorBias) -> Result { let max_point = self.max_point(); if point > max_point { return Err(Error::OffsetOutOfRange); } let seek_bias; match bias { AnchorBias::Left => { if point.is_zero() { return Ok(Anchor(AnchorInner::Start)); } else { seek_bias = SeekBias::Left; } } AnchorBias::Right => { if point == max_point { return Ok(Anchor(AnchorInner::End)); } else { seek_bias = SeekBias::Right; } } }; let mut cursor = self.fragments.cursor(); cursor.seek(&point, seek_bias); let fragment = cursor.item().unwrap(); let offset_in_fragment = fragment.offset_for_point(point - &cursor.start::())?; let offset_in_insertion = fragment.start_offset + offset_in_fragment; let anchor = Anchor(AnchorInner::Middle { insertion_id: fragment.insertion.id, offset: offset_in_insertion, bias, }); let offset = cursor.start::().0 + offset_in_fragment; self.cache_position(Some(anchor.clone()), offset, point); Ok(anchor) } pub fn offset_for_anchor(&self, anchor: &Anchor) -> Result { Ok(self.position_for_anchor(anchor)?.0) } pub fn point_for_anchor(&self, anchor: &Anchor) -> Result { Ok(self.position_for_anchor(anchor)?.1) } fn position_for_anchor(&self, anchor: &Anchor) -> Result<(usize, Point), Error> { match &anchor.0 { &AnchorInner::Start => Ok((0, Point { row: 0, column: 0 })), &AnchorInner::End => Ok((self.len(), self.fragments.len::())), &AnchorInner::Middle { ref insertion_id, offset, ref bias, } => { let cached_position = { let anchor_cache = self.anchor_cache.try_borrow().ok(); anchor_cache .as_ref() .and_then(|cache| cache.get(anchor).cloned()) }; if let Some(cached_position) = cached_position { Ok(cached_position) } else { let seek_bias = match bias { &AnchorBias::Left => SeekBias::Left, &AnchorBias::Right => SeekBias::Right, }; let splits = self.insertion_splits .get(&insertion_id) .ok_or(Error::InvalidAnchor)?; let mut splits_cursor = splits.cursor(); splits_cursor.seek(&InsertionOffset(offset), seek_bias); splits_cursor .item() .ok_or(Error::InvalidAnchor) .and_then(|split| { let mut fragments_cursor = self.fragments.cursor(); fragments_cursor.seek(&split.fragment_id, SeekBias::Left); fragments_cursor .item() .ok_or(Error::InvalidAnchor) .and_then(|fragment| { let overshoot = if fragment.is_visible() { offset - fragment.start_offset } else { 0 }; let offset = fragments_cursor.start::().0 + overshoot; let point = fragments_cursor.start::() + &fragment.point_for_offset(overshoot)?; self.cache_position(Some(anchor.clone()), offset, point); Ok((offset, point)) }) }) } } } } fn offset_for_point(&self, point: Point) -> Result { let cached_offset = { let offset_cache = self.offset_cache.try_borrow().ok(); offset_cache .as_ref() .and_then(|cache| cache.get(&point).cloned()) }; if let Some(cached_offset) = cached_offset { Ok(cached_offset) } else { let mut fragments_cursor = self.fragments.cursor(); fragments_cursor.seek(&point, SeekBias::Left); fragments_cursor .item() .ok_or(Error::OffsetOutOfRange) .map(|fragment| { let overshoot = fragment .offset_for_point(point - &fragments_cursor.start::()) .unwrap(); let offset = &fragments_cursor.start::().0 + &overshoot; self.cache_position(None, offset, point); offset }) } } pub fn cmp_anchors(&self, a: &Anchor, b: &Anchor) -> Result { let a_offset = self.offset_for_anchor(a)?; let b_offset = self.offset_for_anchor(b)?; Ok(a_offset.cmp(&b_offset)) } fn cache_position(&self, anchor: Option, offset: usize, point: Point) { anchor.map(|anchor| { if let Ok(mut anchor_cache) = self.anchor_cache.try_borrow_mut() { anchor_cache.insert(anchor, (offset, point)); } }); if let Ok(mut offset_cache) = self.offset_cache.try_borrow_mut() { offset_cache.insert(point, offset); } } } impl BufferSnapshot { pub fn iter<'a>(&'a self) -> impl 'a + Iterator { self.fragments.iter().filter_map(|fragment| { if fragment.is_visible() { let range = fragment.start_offset..fragment.end_offset; Some(&fragment.insertion.text.code_units[range]) } else { None } }) } #[cfg(test)] pub fn to_string(&self) -> String { String::from_utf16_lossy(&self.iter().flat_map(|c| c).cloned().collect::>()) } } impl Point { pub fn new(row: u32, column: u32) -> Self { Point { row, column } } #[cfg(test)] pub fn zero() -> Self { Point::new(0, 0) } pub fn is_zero(&self) -> bool { self.row == 0 && self.column == 0 } } impl tree::Dimension for Point { type Summary = FragmentSummary; fn from_summary(summary: &Self::Summary) -> Self { summary.extent_2d } } impl<'a> Add<&'a Self> for Point { type Output = Point; fn add(self, other: &'a Self) -> Self::Output { if other.row == 0 { Point::new(self.row, self.column + other.column) } else { Point::new(self.row + other.row, other.column) } } } impl<'a> Sub<&'a Self> for Point { type Output = Point; fn sub(self, other: &'a Self) -> Self::Output { debug_assert!(*other <= self); if self.row == other.row { Point::new(0, self.column - other.column) } else { Point::new(self.row - other.row, self.column) } } } impl AddAssign for Point { fn add_assign(&mut self, other: Self) { if other.row == 0 { self.column += other.column; } else { self.row += other.row; self.column = other.column; } } } impl PartialOrd for Point { fn partial_cmp(&self, other: &Point) -> Option { Some(self.cmp(other)) } } impl Ord for Point { #[cfg(target_pointer_width = "64")] fn cmp(&self, other: &Point) -> Ordering { let a = (self.row as usize) << 32 | self.column as usize; let b = (other.row as usize) << 32 | other.column as usize; a.cmp(&b) } #[cfg(target_pointer_width = "32")] fn cmp(&self, other: &Point) -> Ordering { match self.row.cmp(&other.row) { Ordering::Equal => self.column.cmp(&other.column), comparison @ _ => comparison, } } } impl SelectionSet { fn state(&self) -> SelectionSetState { SelectionSetState { user_id: self.user_id, selections: self.selections.clone(), } } } impl<'a> Iter<'a> { fn new(buffer: &'a Buffer) -> Self { let mut fragment_cursor = buffer.fragments.cursor(); fragment_cursor.seek(&CharacterCount(0), SeekBias::Right); Self { fragment_cursor, fragment_offset: 0, } } fn starting_at_point(buffer: &'a Buffer, point: Point) -> Self { let mut fragment_cursor = buffer.fragments.cursor(); fragment_cursor.seek(&point, SeekBias::Right); let fragment_offset = if let Some(fragment) = fragment_cursor.item() { let point_in_fragment = point - &fragment_cursor.start::(); fragment.offset_for_point(point_in_fragment).unwrap() } else { 0 }; Self { fragment_cursor, fragment_offset, } } } impl<'a> Iterator for Iter<'a> { type Item = u16; fn next(&mut self) -> Option { if let Some(fragment) = self.fragment_cursor.item() { if let Some(c) = fragment.get_code_unit(self.fragment_offset) { self.fragment_offset += 1; return Some(c); } } loop { self.fragment_cursor.next(); if let Some(fragment) = self.fragment_cursor.item() { if let Some(c) = fragment.get_code_unit(0) { self.fragment_offset = 1; return Some(c); } } else { break; } } None } } impl<'a> BackwardIter<'a> { fn starting_at_point(buffer: &'a Buffer, point: Point) -> Self { let mut fragment_cursor = buffer.fragments.cursor(); fragment_cursor.seek(&point, SeekBias::Left); let fragment_offset = if let Some(fragment) = fragment_cursor.item() { let point_in_fragment = point - &fragment_cursor.start::(); fragment.offset_for_point(point_in_fragment).unwrap() } else { 0 }; Self { fragment_cursor, fragment_offset, } } } impl<'a> Iterator for BackwardIter<'a> { type Item = u16; fn next(&mut self) -> Option { if let Some(fragment) = self.fragment_cursor.item() { if self.fragment_offset > 0 { self.fragment_offset -= 1; if let Some(c) = fragment.get_code_unit(self.fragment_offset) { return Some(c); } } } loop { self.fragment_cursor.prev(); if let Some(fragment) = self.fragment_cursor.item() { if fragment.len() > 0 { self.fragment_offset = fragment.len() - 1; return fragment.get_code_unit(self.fragment_offset); } } else { break; } } None } } impl Selection { pub fn head(&self) -> &Anchor { if self.reversed { &self.start } else { &self.end } } pub fn set_head(&mut self, buffer: &Buffer, cursor: Anchor) { if buffer.cmp_anchors(&cursor, self.tail()).unwrap() < Ordering::Equal { if !self.reversed { mem::swap(&mut self.start, &mut self.end); self.reversed = true; } self.start = cursor; } else { if self.reversed { mem::swap(&mut self.start, &mut self.end); self.reversed = false; } self.end = cursor; } } pub fn tail(&self) -> &Anchor { if self.reversed { &self.end } else { &self.start } } pub fn is_empty(&self, buffer: &Buffer) -> bool { buffer.cmp_anchors(&self.start, &self.end).unwrap() == Ordering::Equal } pub fn anchor_range(&self) -> Range { self.start.clone()..self.end.clone() } } impl Text { fn new(code_units: Vec) -> Self { fn build_tree(index: usize, line_lengths: &[u32], mut tree: &mut [LineNode]) { if line_lengths.is_empty() { return; } let mid = if line_lengths.len() == 1 { 0 } else { let depth = log2_fast(line_lengths.len()); let max_elements = (1 << (depth)) - 1; let right_subtree_elements = 1 << (depth - 1); cmp::min(line_lengths.len() - right_subtree_elements, max_elements) }; let len = line_lengths[mid]; let lower = &line_lengths[0..mid]; let upper = &line_lengths[mid + 1..]; let left_child_index = index * 2 + 1; let right_child_index = index * 2 + 2; build_tree(left_child_index, lower, &mut tree); build_tree(right_child_index, upper, &mut tree); tree[index] = { let mut left_child_longest_row = 0; let mut left_child_longest_row_len = 0; let mut left_child_offset = 0; let mut left_child_rows = 0; if let Some(left_child) = tree.get(left_child_index) { left_child_longest_row = left_child.longest_row; left_child_longest_row_len = left_child.longest_row_len; left_child_offset = left_child.offset; left_child_rows = left_child.rows; } let mut right_child_longest_row = 0; let mut right_child_longest_row_len = 0; let mut right_child_offset = 0; let mut right_child_rows = 0; if let Some(right_child) = tree.get(right_child_index) { right_child_longest_row = right_child.longest_row; right_child_longest_row_len = right_child.longest_row_len; right_child_offset = right_child.offset; right_child_rows = right_child.rows; } let mut longest_row = 0; let mut longest_row_len = 0; if left_child_longest_row_len > longest_row_len { longest_row = left_child_longest_row; longest_row_len = left_child_longest_row_len; } if len > longest_row_len { longest_row = left_child_rows; longest_row_len = len; } if right_child_longest_row_len > longest_row_len { longest_row = left_child_rows + right_child_longest_row + 1; longest_row_len = right_child_longest_row_len; } LineNode { len, longest_row, longest_row_len, offset: left_child_offset + len as usize + right_child_offset + 1, rows: left_child_rows + right_child_rows + 1, } }; } let mut line_lengths = Vec::new(); let mut prev_offset = 0; for (offset, code_unit) in code_units.iter().enumerate() { if code_unit == &u16::from(b'\n') { line_lengths.push((offset - prev_offset) as u32); prev_offset = offset + 1; } } line_lengths.push((code_units.len() - prev_offset) as u32); let mut nodes = Vec::new(); nodes.resize( line_lengths.len(), LineNode { len: 0, longest_row_len: 0, longest_row: 0, offset: 0, rows: 0, }, ); build_tree(0, &line_lengths, &mut nodes); Self { code_units, nodes } } fn len(&self) -> usize { self.code_units.len() } fn longest_row_in_range(&self, target_range: Range) -> Result<(u32, u32), Error> { let mut longest_row = 0; let mut longest_row_len = 0; self.search(|probe| { if target_range.start <= probe.offset_range.end && probe.right_ancestor_start_offset <= target_range.end { if let Some(right_child) = probe.right_child { if right_child.longest_row_len >= longest_row_len { longest_row = probe.row + 1 + right_child.longest_row; longest_row_len = right_child.longest_row_len; } } } if target_range.start < probe.offset_range.start { if probe.offset_range.end < target_range.end && probe.node.len >= longest_row_len { longest_row = probe.row; longest_row_len = probe.node.len; } Ordering::Less } else if target_range.start > probe.offset_range.end { Ordering::Greater } else { let node_end = cmp::min(probe.offset_range.end, target_range.end); let node_len = (node_end - target_range.start) as u32; if node_len >= longest_row_len { longest_row = probe.row; longest_row_len = node_len; } Ordering::Equal } }).ok_or(Error::OffsetOutOfRange)?; self.search(|probe| { if target_range.end >= probe.offset_range.start && probe.left_ancestor_end_offset >= target_range.start { if let Some(left_child) = probe.left_child { if left_child.longest_row_len > longest_row_len { let left_ancestor_row = probe.row - left_child.rows; longest_row = left_ancestor_row + left_child.longest_row; longest_row_len = left_child.longest_row_len; } } } if target_range.end < probe.offset_range.start { Ordering::Less } else if target_range.end > probe.offset_range.end { if target_range.start < probe.offset_range.start && probe.node.len > longest_row_len { longest_row = probe.row; longest_row_len = probe.node.len; } Ordering::Greater } else { let node_start = cmp::max(target_range.start, probe.offset_range.start); let node_len = (target_range.end - node_start) as u32; if node_len > longest_row_len { longest_row = probe.row; longest_row_len = node_len; } Ordering::Equal } }).ok_or(Error::OffsetOutOfRange)?; Ok((longest_row, longest_row_len)) } fn point_for_offset(&self, offset: usize) -> Result { let search_result = self.search(|probe| { if offset < probe.offset_range.start { Ordering::Less } else if offset > probe.offset_range.end { Ordering::Greater } else { Ordering::Equal } }); if let Some((offset_range, row, _)) = search_result { Ok(Point::new(row, (offset - offset_range.start) as u32)) } else { Err(Error::OffsetOutOfRange) } } fn offset_for_point(&self, point: Point) -> Result { if let Some((offset_range, _, node)) = self.search(|probe| point.row.cmp(&probe.row)) { if point.column <= node.len { Ok(offset_range.start + point.column as usize) } else { Err(Error::OffsetOutOfRange) } } else { Err(Error::OffsetOutOfRange) } } fn search(&self, mut f: F) -> Option<(Range, u32, &LineNode)> where F: FnMut(LineNodeProbe) -> Ordering, { let mut left_ancestor_end_offset = 0; let mut left_ancestor_row = 0; let mut right_ancestor_start_offset = self.nodes[0].offset; let mut cur_node_index = 0; while let Some(cur_node) = self.nodes.get(cur_node_index) { let left_child = self.nodes.get(cur_node_index * 2 + 1); let right_child = self.nodes.get(cur_node_index * 2 + 2); let cur_offset_range = { let start = left_ancestor_end_offset + left_child.map_or(0, |node| node.offset); let end = start + cur_node.len as usize; start..end }; let cur_row = left_ancestor_row + left_child.map_or(0, |node| node.rows); match f(LineNodeProbe { offset_range: &cur_offset_range, row: cur_row, left_ancestor_end_offset, right_ancestor_start_offset, node: cur_node, left_child, right_child, }) { Ordering::Less => { cur_node_index = cur_node_index * 2 + 1; right_ancestor_start_offset = cur_offset_range.start; } Ordering::Equal => return Some((cur_offset_range, cur_row, cur_node)), Ordering::Greater => { cur_node_index = cur_node_index * 2 + 2; left_ancestor_end_offset = cur_offset_range.end + 1; left_ancestor_row = cur_row + 1; } } } None } } impl<'a> From<&'a str> for Text { fn from(s: &'a str) -> Self { Self::new(s.encode_utf16().collect()) } } impl<'a> From> for Text { fn from(s: Vec) -> Self { Self::new(s) } } #[inline(always)] fn log2_fast(x: usize) -> usize { 8 * mem::size_of::() - (x.leading_zeros() as usize) - 1 } lazy_static! { static ref FRAGMENT_ID_MIN_VALUE: FragmentId = FragmentId(Arc::new(vec![0 as u16])); static ref FRAGMENT_ID_MAX_VALUE: FragmentId = FragmentId(Arc::new(vec![u16::max_value()])); } impl FragmentId { fn min_value() -> Self { FRAGMENT_ID_MIN_VALUE.clone() } fn max_value() -> Self { FRAGMENT_ID_MAX_VALUE.clone() } fn between(left: &Self, right: &Self) -> Self { Self::between_with_max(left, right, u16::max_value()) } fn between_with_max(left: &Self, right: &Self, max_value: u16) -> Self { let mut new_entries = Vec::new(); let left_entries = left.0.iter().cloned().chain(iter::repeat(0)); let right_entries = right.0.iter().cloned().chain(iter::repeat(max_value)); for (l, r) in left_entries.zip(right_entries) { let interval = r - l; if interval > 1 { new_entries.push(l + interval / 2); break; } else { new_entries.push(l); } } FragmentId(Arc::new(new_entries)) } } impl tree::Dimension for FragmentId { type Summary = FragmentSummary; fn from_summary(summary: &Self::Summary) -> Self { summary.max_fragment_id.clone() } } impl<'a> Add<&'a Self> for FragmentId { type Output = FragmentId; fn add(self, other: &'a Self) -> Self::Output { cmp::max(&self, other).clone() } } impl AddAssign for FragmentId { fn add_assign(&mut self, other: Self) { if *self < other { *self = other } } } fn serialize_option_arc(option: &Option>, serializer: S) -> Result where T: Serialize, S: Serializer, { if let &Some(ref arc) = option { serializer.serialize_some(arc.as_ref()) } else { serializer.serialize_none() } } fn deserialize_option_arc<'de, T, D>(deserializer: D) -> Result>, D::Error> where T: Deserialize<'de>, D: Deserializer<'de>, { struct OptionArcVisitor(marker::PhantomData); impl<'de, T: Deserialize<'de>> serde::de::Visitor<'de> for OptionArcVisitor { type Value = Option>; fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { write!(formatter, "an Option>") } fn visit_none(self) -> Result where E: serde::de::Error, { Ok(None) } fn visit_some(self, deserializer: D) -> Result where D: Deserializer<'de>, { Ok(Some(Arc::new(T::deserialize(deserializer)?))) } } let visitor = OptionArcVisitor(marker::PhantomData); deserializer.deserialize_option(visitor) } fn serialize_arc(arc: &Arc, serializer: S) -> Result where T: Serialize, S: Serializer, { arc.serialize(serializer) } fn deserialize_arc<'de, T, D>(deserializer: D) -> Result, D::Error> where T: Deserialize<'de>, D: Deserializer<'de>, { Ok(Arc::new(T::deserialize(deserializer)?)) } impl Fragment { fn new(id: FragmentId, insertion: Insertion) -> Self { let end_offset = insertion.text.len(); Self { id, insertion, start_offset: 0, end_offset, deletions: HashSet::new(), } } fn get_code_unit(&self, offset: usize) -> Option { if offset < self.len() { Some(self.insertion.text.code_units[self.start_offset + offset].clone()) } else { None } } fn len(&self) -> usize { if self.is_visible() { self.end_offset - self.start_offset } else { 0 } } fn is_visible(&self) -> bool { self.deletions.is_empty() } fn point_for_offset(&self, offset: usize) -> Result { let text = &self.insertion.text; let offset_in_insertion = self.start_offset + offset; Ok( text.point_for_offset(offset_in_insertion)? - &text.point_for_offset(self.start_offset)?, ) } fn offset_for_point(&self, point: Point) -> Result { let text = &self.insertion.text; let point_in_insertion = text.point_for_offset(self.start_offset)? + &point; Ok(text.offset_for_point(point_in_insertion)? - self.start_offset) } } impl tree::Item for Fragment { type Summary = FragmentSummary; fn summarize(&self) -> Self::Summary { if self.is_visible() { let fragment_2d_start = self.insertion .text .point_for_offset(self.start_offset) .unwrap(); let fragment_2d_end = self.insertion .text .point_for_offset(self.end_offset) .unwrap(); let first_row_len = if fragment_2d_start.row == fragment_2d_end.row { (self.end_offset - self.start_offset) as u32 } else { let first_row_end = self.insertion .text .offset_for_point(Point::new(fragment_2d_start.row + 1, 0)) .unwrap() - 1; (first_row_end - self.start_offset) as u32 }; let (longest_row, longest_row_len) = self.insertion .text .longest_row_in_range(self.start_offset..self.end_offset) .unwrap(); FragmentSummary { extent: self.len(), extent_2d: fragment_2d_end - &fragment_2d_start, max_fragment_id: self.id.clone(), first_row_len, longest_row: longest_row - fragment_2d_start.row, longest_row_len, } } else { FragmentSummary { extent: 0, extent_2d: Point { row: 0, column: 0 }, max_fragment_id: self.id.clone(), first_row_len: 0, longest_row: 0, longest_row_len: 0, } } } } impl<'a> AddAssign<&'a FragmentSummary> for FragmentSummary { fn add_assign(&mut self, other: &Self) { let last_row_len = self.extent_2d.column + other.first_row_len; if last_row_len > self.longest_row_len { self.longest_row = self.extent_2d.row; self.longest_row_len = last_row_len; } if other.longest_row_len > self.longest_row_len { self.longest_row = self.extent_2d.row + other.longest_row; self.longest_row_len = other.longest_row_len; } self.extent += other.extent; self.extent_2d += other.extent_2d; if self.max_fragment_id < other.max_fragment_id { self.max_fragment_id = other.max_fragment_id.clone(); } } } impl Default for FragmentSummary { fn default() -> Self { FragmentSummary { extent: 0, extent_2d: Point { row: 0, column: 0 }, max_fragment_id: FragmentId::min_value(), first_row_len: 0, longest_row: 0, longest_row_len: 0, } } } impl tree::Dimension for CharacterCount { type Summary = FragmentSummary; fn from_summary(summary: &Self::Summary) -> Self { CharacterCount(summary.extent) } } impl<'a> Add<&'a Self> for CharacterCount { type Output = CharacterCount; fn add(self, other: &'a Self) -> Self::Output { CharacterCount(self.0 + other.0) } } impl AddAssign for CharacterCount { fn add_assign(&mut self, other: Self) { self.0 += other.0; } } impl tree::Item for InsertionSplit { type Summary = InsertionSplitSummary; fn summarize(&self) -> Self::Summary { InsertionSplitSummary { extent: self.extent, } } } impl<'a> AddAssign<&'a InsertionSplitSummary> for InsertionSplitSummary { fn add_assign(&mut self, other: &Self) { self.extent += other.extent; } } impl Default for InsertionSplitSummary { fn default() -> Self { InsertionSplitSummary { extent: 0 } } } impl tree::Dimension for InsertionOffset { type Summary = InsertionSplitSummary; fn from_summary(summary: &Self::Summary) -> Self { InsertionOffset(summary.extent) } } impl<'a> Add<&'a Self> for InsertionOffset { type Output = InsertionOffset; fn add(self, other: &'a Self) -> Self::Output { InsertionOffset(self.0 + other.0) } } impl AddAssign for InsertionOffset { fn add_assign(&mut self, other: Self) { self.0 += other.0; } } impl Operation { fn replica_id(&self) -> ReplicaId { match *self { Operation::Edit { ref id, .. } => id.replica_id, } } } fn should_insert_before( insertion: &Insertion, other_timestamp: LamportTimestamp, other_replica_id: ReplicaId, ) -> bool { match insertion.timestamp.cmp(&other_timestamp) { Ordering::Less => true, Ordering::Equal => insertion.id.replica_id < other_replica_id, Ordering::Greater => false, } } #[cfg(test)] mod tests { extern crate rand; use self::rand::{Rng, SeedableRng, StdRng}; use super::*; use rpc; use std::time::Duration; use tokio_core::reactor; use IntoShared; #[test] fn test_edit() { let mut buffer = Buffer::new(0); buffer.edit(&[0..0], "abc"); assert_eq!(buffer.to_string(), "abc"); buffer.edit(&[3..3], "def"); assert_eq!(buffer.to_string(), "abcdef"); buffer.edit(&[0..0], "ghi"); assert_eq!(buffer.to_string(), "ghiabcdef"); buffer.edit(&[5..5], "jkl"); assert_eq!(buffer.to_string(), "ghiabjklcdef"); buffer.edit(&[6..7], ""); assert_eq!(buffer.to_string(), "ghiabjlcdef"); buffer.edit(&[4..9], "mno"); assert_eq!(buffer.to_string(), "ghiamnoef"); } #[test] fn test_random_edits() { for seed in 0..100 { println!("{:?}", seed); let mut rng = StdRng::from_seed(&[seed]); let mut buffer = Buffer::new(0); let mut reference_string = String::new(); for _i in 0..10 { let mut old_ranges: Vec> = Vec::new(); for _ in 0..5 { let last_end = old_ranges.last().map_or(0, |last_range| last_range.end + 1); if last_end > buffer.len() { break; } let end = rng.gen_range::(last_end, buffer.len() + 1); let start = rng.gen_range::(last_end, end + 1); old_ranges.push(start..end); } let new_text = RandomCharIter(rng) .take(rng.gen_range(0, 10)) .collect::(); buffer.edit(&old_ranges, new_text.as_str()); for old_range in old_ranges.iter().rev() { reference_string = [ &reference_string[0..old_range.start], new_text.as_str(), &reference_string[old_range.end..], ].concat(); } assert_eq!(buffer.to_string(), reference_string); } } } #[test] fn test_len_for_row() { let mut buffer = Buffer::new(0); buffer.edit(&[0..0], "abcd\nefg\nhij"); buffer.edit(&[12..12], "kl\nmno"); buffer.edit(&[18..18], "\npqrs\n"); buffer.edit(&[18..21], "\nPQ"); assert_eq!(buffer.len_for_row(0), Ok(4)); assert_eq!(buffer.len_for_row(1), Ok(3)); assert_eq!(buffer.len_for_row(2), Ok(5)); assert_eq!(buffer.len_for_row(3), Ok(3)); assert_eq!(buffer.len_for_row(4), Ok(4)); assert_eq!(buffer.len_for_row(5), Ok(0)); assert_eq!(buffer.len_for_row(6), Err(Error::OffsetOutOfRange)); } #[test] fn test_longest_row() { let mut buffer = Buffer::new(0); assert_eq!(buffer.longest_row(), 0); buffer.edit(&[0..0], "abcd\nefg\nhij"); assert_eq!(buffer.longest_row(), 0); buffer.edit(&[12..12], "kl\nmno"); assert_eq!(buffer.longest_row(), 2); buffer.edit(&[18..18], "\npqrs"); assert_eq!(buffer.longest_row(), 2); buffer.edit(&[10..12], ""); assert_eq!(buffer.longest_row(), 0); buffer.edit(&[24..24], "tuv"); assert_eq!(buffer.longest_row(), 4); } #[test] fn iter_starting_at_point() { let mut buffer = Buffer::new(0); buffer.edit(&[0..0], "abcd\nefgh\nij"); buffer.edit(&[12..12], "kl\nmno"); buffer.edit(&[18..18], "\npqrs"); buffer.edit(&[18..21], "\nPQ"); let iter = buffer.iter_starting_at_point(Point::new(0, 0)); assert_eq!( String::from_utf16_lossy(&iter.collect::>()), "abcd\nefgh\nijkl\nmno\nPQrs" ); let iter = buffer.iter_starting_at_point(Point::new(1, 0)); assert_eq!( String::from_utf16_lossy(&iter.collect::>()), "efgh\nijkl\nmno\nPQrs" ); let iter = buffer.iter_starting_at_point(Point::new(2, 0)); assert_eq!( String::from_utf16_lossy(&iter.collect::>()), "ijkl\nmno\nPQrs" ); let iter = buffer.iter_starting_at_point(Point::new(3, 0)); assert_eq!( String::from_utf16_lossy(&iter.collect::>()), "mno\nPQrs" ); let iter = buffer.iter_starting_at_point(Point::new(4, 0)); assert_eq!( String::from_utf16_lossy(&iter.collect::>()), "PQrs" ); let iter = buffer.iter_starting_at_point(Point::new(5, 0)); assert_eq!(String::from_utf16_lossy(&iter.collect::>()), ""); // Regression test: let mut buffer = Buffer::new(0); buffer.edit(&[0..0], "[workspace]\nmembers = [\n \"xray_core\",\n \"xray_server\",\n \"xray_cli\",\n \"xray_wasm\",\n]\n"); buffer.edit(&[60..60], "\n"); let iter = buffer.iter_starting_at_point(Point::new(6, 0)); assert_eq!( String::from_utf16_lossy(&iter.collect::>()), " \"xray_wasm\",\n]\n" ); } #[test] fn backward_iter_starting_at_point() { let mut buffer = Buffer::new(0); buffer.edit(&[0..0], "abcd\nefgh\nij"); buffer.edit(&[12..12], "kl\nmno"); buffer.edit(&[18..18], "\npqrs"); buffer.edit(&[18..21], "\nPQ"); let iter = buffer.backward_iter_starting_at_point(Point::new(0, 0)); assert_eq!(String::from_utf16_lossy(&iter.collect::>()), ""); let iter = buffer.backward_iter_starting_at_point(Point::new(0, 3)); assert_eq!(String::from_utf16_lossy(&iter.collect::>()), "cba"); let iter = buffer.backward_iter_starting_at_point(Point::new(1, 4)); assert_eq!( String::from_utf16_lossy(&iter.collect::>()), "hgfe\ndcba" ); let iter = buffer.backward_iter_starting_at_point(Point::new(3, 2)); assert_eq!( String::from_utf16_lossy(&iter.collect::>()), "nm\nlkji\nhgfe\ndcba" ); let iter = buffer.backward_iter_starting_at_point(Point::new(4, 4)); assert_eq!( String::from_utf16_lossy(&iter.collect::>()), "srQP\nonm\nlkji\nhgfe\ndcba" ); let iter = buffer.backward_iter_starting_at_point(Point::new(5, 0)); assert_eq!( String::from_utf16_lossy(&iter.collect::>()), "srQP\nonm\nlkji\nhgfe\ndcba" ); } #[test] fn test_point_for_offset() { let text = Text::from("abc\ndefgh\nijklm\nopq"); assert_eq!(text.point_for_offset(0), Ok(Point { row: 0, column: 0 })); assert_eq!(text.point_for_offset(1), Ok(Point { row: 0, column: 1 })); assert_eq!(text.point_for_offset(2), Ok(Point { row: 0, column: 2 })); assert_eq!(text.point_for_offset(3), Ok(Point { row: 0, column: 3 })); assert_eq!(text.point_for_offset(4), Ok(Point { row: 1, column: 0 })); assert_eq!(text.point_for_offset(5), Ok(Point { row: 1, column: 1 })); assert_eq!(text.point_for_offset(9), Ok(Point { row: 1, column: 5 })); assert_eq!(text.point_for_offset(10), Ok(Point { row: 2, column: 0 })); assert_eq!(text.point_for_offset(14), Ok(Point { row: 2, column: 4 })); assert_eq!(text.point_for_offset(15), Ok(Point { row: 2, column: 5 })); assert_eq!(text.point_for_offset(16), Ok(Point { row: 3, column: 0 })); assert_eq!(text.point_for_offset(17), Ok(Point { row: 3, column: 1 })); assert_eq!(text.point_for_offset(19), Ok(Point { row: 3, column: 3 })); assert_eq!(text.point_for_offset(20), Err(Error::OffsetOutOfRange)); let text = Text::from("abc"); assert_eq!(text.point_for_offset(0), Ok(Point { row: 0, column: 0 })); assert_eq!(text.point_for_offset(1), Ok(Point { row: 0, column: 1 })); assert_eq!(text.point_for_offset(2), Ok(Point { row: 0, column: 2 })); assert_eq!(text.point_for_offset(3), Ok(Point { row: 0, column: 3 })); assert_eq!(text.point_for_offset(4), Err(Error::OffsetOutOfRange)); } #[test] fn test_offset_for_point() { let text = Text::from("abc\ndefgh"); assert_eq!(text.offset_for_point(Point { row: 0, column: 0 }), Ok(0)); assert_eq!(text.offset_for_point(Point { row: 0, column: 1 }), Ok(1)); assert_eq!(text.offset_for_point(Point { row: 0, column: 2 }), Ok(2)); assert_eq!(text.offset_for_point(Point { row: 0, column: 3 }), Ok(3)); assert_eq!( text.offset_for_point(Point { row: 0, column: 4 }), Err(Error::OffsetOutOfRange) ); assert_eq!(text.offset_for_point(Point { row: 1, column: 0 }), Ok(4)); assert_eq!(text.offset_for_point(Point { row: 1, column: 1 }), Ok(5)); assert_eq!(text.offset_for_point(Point { row: 1, column: 5 }), Ok(9)); assert_eq!( text.offset_for_point(Point { row: 1, column: 6 }), Err(Error::OffsetOutOfRange) ); let text = Text::from("abc"); assert_eq!(text.offset_for_point(Point { row: 0, column: 0 }), Ok(0)); assert_eq!(text.offset_for_point(Point { row: 0, column: 1 }), Ok(1)); assert_eq!(text.offset_for_point(Point { row: 0, column: 2 }), Ok(2)); assert_eq!(text.offset_for_point(Point { row: 0, column: 3 }), Ok(3)); assert_eq!( text.offset_for_point(Point { row: 0, column: 4 }), Err(Error::OffsetOutOfRange) ); } #[test] fn test_longest_row_in_range() { for seed in 0..100 { println!("{:?}", seed); let mut rng = StdRng::from_seed(&[seed]); let string = RandomCharIter(rng) .take(rng.gen_range(1, 10)) .collect::(); let text = Text::from(string.as_ref()); for _i in 0..10 { let end = rng.gen_range(1, string.len() + 1); let start = rng.gen_range(0, end); let mut cur_row = string[0..start].chars().filter(|c| *c == '\n').count() as u32; let mut cur_row_len = 0; let mut expected_longest_row = cur_row; let mut expected_longest_row_len = cur_row_len; for ch in string[start..end].chars() { if ch == '\n' { if cur_row_len > expected_longest_row_len { expected_longest_row = cur_row; expected_longest_row_len = cur_row_len; } cur_row += 1; cur_row_len = 0; } else { cur_row_len += 1; } } if cur_row_len > expected_longest_row_len { expected_longest_row = cur_row; expected_longest_row_len = cur_row_len; } assert_eq!( text.longest_row_in_range(start..end), Ok((expected_longest_row, expected_longest_row_len)) ); } } } #[test] fn fragment_ids() { for seed in 0..10 { use self::rand::{Rng, SeedableRng, StdRng}; let mut rng = StdRng::from_seed(&[seed]); let mut ids = vec![FragmentId(Arc::new(vec![0])), FragmentId(Arc::new(vec![4]))]; for _i in 0..100 { let index = rng.gen_range::(1, ids.len()); let left = ids[index - 1].clone(); let right = ids[index].clone(); ids.insert(index, FragmentId::between_with_max(&left, &right, 4)); let mut sorted_ids = ids.clone(); sorted_ids.sort(); assert_eq!(ids, sorted_ids); } } } #[test] fn test_anchors() { let mut buffer = Buffer::new(0); buffer.edit(&[0..0], "abc"); let left_anchor = buffer.anchor_before_offset(2).unwrap(); let right_anchor = buffer.anchor_after_offset(2).unwrap(); buffer.edit(&[1..1], "def\n"); assert_eq!(buffer.to_string(), "adef\nbc"); assert_eq!(buffer.offset_for_anchor(&left_anchor).unwrap(), 6); assert_eq!(buffer.offset_for_anchor(&right_anchor).unwrap(), 6); assert_eq!( buffer.point_for_anchor(&left_anchor).unwrap(), Point { row: 1, column: 1 } ); assert_eq!( buffer.point_for_anchor(&right_anchor).unwrap(), Point { row: 1, column: 1 } ); buffer.edit(&[2..3], ""); assert_eq!(buffer.to_string(), "adf\nbc"); assert_eq!(buffer.offset_for_anchor(&left_anchor).unwrap(), 5); assert_eq!(buffer.offset_for_anchor(&right_anchor).unwrap(), 5); assert_eq!( buffer.point_for_anchor(&left_anchor).unwrap(), Point { row: 1, column: 1 } ); assert_eq!( buffer.point_for_anchor(&right_anchor).unwrap(), Point { row: 1, column: 1 } ); buffer.edit(&[5..5], "ghi\n"); assert_eq!(buffer.to_string(), "adf\nbghi\nc"); assert_eq!(buffer.offset_for_anchor(&left_anchor).unwrap(), 5); assert_eq!(buffer.offset_for_anchor(&right_anchor).unwrap(), 9); assert_eq!( buffer.point_for_anchor(&left_anchor).unwrap(), Point { row: 1, column: 1 } ); assert_eq!( buffer.point_for_anchor(&right_anchor).unwrap(), Point { row: 2, column: 0 } ); buffer.edit(&[7..9], ""); assert_eq!(buffer.to_string(), "adf\nbghc"); assert_eq!(buffer.offset_for_anchor(&left_anchor).unwrap(), 5); assert_eq!(buffer.offset_for_anchor(&right_anchor).unwrap(), 7); assert_eq!( buffer.point_for_anchor(&left_anchor).unwrap(), Point { row: 1, column: 1 } ); assert_eq!( buffer.point_for_anchor(&right_anchor).unwrap(), Point { row: 1, column: 3 } ); // Ensure anchoring to a point is equivalent to anchoring to an offset. assert_eq!( buffer.anchor_before_point(Point { row: 0, column: 0 }), buffer.anchor_before_offset(0) ); assert_eq!( buffer.anchor_before_point(Point { row: 0, column: 1 }), buffer.anchor_before_offset(1) ); assert_eq!( buffer.anchor_before_point(Point { row: 0, column: 2 }), buffer.anchor_before_offset(2) ); assert_eq!( buffer.anchor_before_point(Point { row: 0, column: 3 }), buffer.anchor_before_offset(3) ); assert_eq!( buffer.anchor_before_point(Point { row: 1, column: 0 }), buffer.anchor_before_offset(4) ); assert_eq!( buffer.anchor_before_point(Point { row: 1, column: 1 }), buffer.anchor_before_offset(5) ); assert_eq!( buffer.anchor_before_point(Point { row: 1, column: 2 }), buffer.anchor_before_offset(6) ); assert_eq!( buffer.anchor_before_point(Point { row: 1, column: 3 }), buffer.anchor_before_offset(7) ); assert_eq!( buffer.anchor_before_point(Point { row: 1, column: 4 }), buffer.anchor_before_offset(8) ); // Comparison between anchors. let anchor_at_offset_0 = buffer.anchor_before_offset(0).unwrap(); let anchor_at_offset_1 = buffer.anchor_before_offset(1).unwrap(); let anchor_at_offset_2 = buffer.anchor_before_offset(2).unwrap(); assert_eq!( buffer.cmp_anchors(&anchor_at_offset_0, &anchor_at_offset_0), Ok(Ordering::Equal) ); assert_eq!( buffer.cmp_anchors(&anchor_at_offset_1, &anchor_at_offset_1), Ok(Ordering::Equal) ); assert_eq!( buffer.cmp_anchors(&anchor_at_offset_2, &anchor_at_offset_2), Ok(Ordering::Equal) ); assert_eq!( buffer.cmp_anchors(&anchor_at_offset_0, &anchor_at_offset_1), Ok(Ordering::Less) ); assert_eq!( buffer.cmp_anchors(&anchor_at_offset_1, &anchor_at_offset_2), Ok(Ordering::Less) ); assert_eq!( buffer.cmp_anchors(&anchor_at_offset_0, &anchor_at_offset_2), Ok(Ordering::Less) ); assert_eq!( buffer.cmp_anchors(&anchor_at_offset_1, &anchor_at_offset_0), Ok(Ordering::Greater) ); assert_eq!( buffer.cmp_anchors(&anchor_at_offset_2, &anchor_at_offset_1), Ok(Ordering::Greater) ); assert_eq!( buffer.cmp_anchors(&anchor_at_offset_2, &anchor_at_offset_0), Ok(Ordering::Greater) ); } #[test] fn anchors_at_start_and_end() { let mut buffer = Buffer::new(0); let before_start_anchor = buffer.anchor_before_offset(0).unwrap(); let after_end_anchor = buffer.anchor_after_offset(0).unwrap(); buffer.edit(&[0..0], "abc"); assert_eq!(buffer.to_string(), "abc"); assert_eq!(buffer.offset_for_anchor(&before_start_anchor).unwrap(), 0); assert_eq!(buffer.offset_for_anchor(&after_end_anchor).unwrap(), 3); let after_start_anchor = buffer.anchor_after_offset(0).unwrap(); let before_end_anchor = buffer.anchor_before_offset(3).unwrap(); buffer.edit(&[3..3], "def"); buffer.edit(&[0..0], "ghi"); assert_eq!(buffer.to_string(), "ghiabcdef"); assert_eq!(buffer.offset_for_anchor(&before_start_anchor).unwrap(), 0); assert_eq!(buffer.offset_for_anchor(&after_start_anchor).unwrap(), 3); assert_eq!(buffer.offset_for_anchor(&before_end_anchor).unwrap(), 6); assert_eq!(buffer.offset_for_anchor(&after_end_anchor).unwrap(), 9); } #[test] fn test_snapshot() { let mut buffer = Buffer::new(0); buffer.edit(&[0..0], "abcdefghi"); buffer.edit(&[3..6], "DEF"); let snapshot = buffer.snapshot(); assert_eq!(buffer.to_string(), String::from("abcDEFghi")); assert_eq!(snapshot.to_string(), String::from("abcDEFghi")); buffer.edit(&[0..1], "A"); buffer.edit(&[8..9], "I"); assert_eq!(buffer.to_string(), String::from("AbcDEFghI")); assert_eq!(snapshot.to_string(), String::from("abcDEFghi")); } #[test] fn test_random_concurrent_edits() { for seed in 0..100 { println!("{:?}", seed); let mut rng = StdRng::from_seed(&[seed]); let site_range = 0..5; let mut buffers = Vec::new(); let mut queues = Vec::new(); for i in site_range.clone() { let mut buffer = Buffer::new(0); buffer.replica_id = i + 1; buffers.push(buffer); queues.push(Vec::new()); } let mut edit_count = 10; loop { let replica_index = rng.gen_range::(site_range.start, site_range.end); let buffer = &mut buffers[replica_index]; if edit_count > 0 && rng.gen() { let mut old_ranges: Vec> = Vec::new(); for _ in 0..5 { let last_end = old_ranges.last().map_or(0, |last_range| last_range.end + 1); if last_end > buffer.len() { break; } let end = rng.gen_range::(last_end, buffer.len() + 1); let start = rng.gen_range::(last_end, end + 1); old_ranges.push(start..end); } let new_text = RandomCharIter(rng) .take(rng.gen_range(0, 10)) .collect::(); for op in buffer.edit(&old_ranges, new_text.as_str()) { for (index, queue) in queues.iter_mut().enumerate() { if index != replica_index { queue.push(op.clone()); } } } edit_count -= 1; } else if !queues[replica_index].is_empty() { buffer .integrate_op(queues[replica_index].remove(0)) .unwrap(); } if edit_count == 0 && queues.iter().all(|q| q.is_empty()) { break; } } for buffer in &buffers[1..] { assert_eq!(buffer.to_string(), buffers[0].to_string()); } } } #[test] fn test_edit_replication() { let local_buffer = Buffer::new(0).into_shared(); local_buffer.borrow_mut().edit(&[0..0], "abcdef"); local_buffer.borrow_mut().edit(&[2..4], "ghi"); let mut reactor = reactor::Core::new().unwrap(); let foreground = Rc::new(reactor.handle()); let client_1 = rpc::tests::connect(&mut reactor, super::rpc::Service::new(local_buffer.clone())); let remote_buffer_1 = Buffer::remote(foreground.clone(), client_1).unwrap(); let client_2 = rpc::tests::connect(&mut reactor, super::rpc::Service::new(local_buffer.clone())); let remote_buffer_2 = Buffer::remote(foreground, client_2).unwrap(); assert_eq!( remote_buffer_1.borrow().to_string(), local_buffer.borrow().to_string() ); assert_eq!( remote_buffer_2.borrow().to_string(), local_buffer.borrow().to_string() ); local_buffer.borrow_mut().edit(&[3..6], "jk"); remote_buffer_1.borrow_mut().edit(&[7..7], "lmn"); let anchor = remote_buffer_1.borrow().anchor_before_offset(8).unwrap(); let mut remaining_tries = 10; while remote_buffer_1.borrow().to_string() != local_buffer.borrow().to_string() || remote_buffer_2.borrow().to_string() != local_buffer.borrow().to_string() { remaining_tries -= 1; assert!( remaining_tries > 0, "Ran out of patience waiting for buffers to converge" ); reactor.turn(Some(Duration::from_millis(0))); } assert_eq!(local_buffer.borrow().offset_for_anchor(&anchor).unwrap(), 7); assert_eq!( remote_buffer_1.borrow().offset_for_anchor(&anchor).unwrap(), 7 ); assert_eq!( remote_buffer_2.borrow().offset_for_anchor(&anchor).unwrap(), 7 ); } #[test] fn test_selection_replication() { use stream_ext::StreamExt; let mut buffer_1 = Buffer::new(0); buffer_1.edit(&[0..0], "abcdef"); let sels = vec![empty_selection(&buffer_1, 1), empty_selection(&buffer_1, 3)]; buffer_1.add_selection_set(0, sels); let sels = vec![empty_selection(&buffer_1, 2), empty_selection(&buffer_1, 4)]; let buffer_1_set_id = buffer_1.add_selection_set(0, sels); let buffer_1 = buffer_1.into_shared(); let mut reactor = reactor::Core::new().unwrap(); let foreground = Rc::new(reactor.handle()); let buffer_2 = Buffer::remote( foreground.clone(), rpc::tests::connect(&mut reactor, super::rpc::Service::new(buffer_1.clone())), ).unwrap(); assert_eq!(selections(&buffer_1), selections(&buffer_2)); let buffer_3 = Buffer::remote( foreground, rpc::tests::connect(&mut reactor, super::rpc::Service::new(buffer_1.clone())), ).unwrap(); assert_eq!(selections(&buffer_1), selections(&buffer_3)); let mut buffer_1_updates = buffer_1.borrow().updates(); let mut buffer_2_updates = buffer_2.borrow().updates(); let mut buffer_3_updates = buffer_3.borrow().updates(); buffer_1 .borrow_mut() .mutate_selections(buffer_1_set_id, |buffer, selections| { for selection in selections { selection.start = buffer .anchor_before_offset( buffer.offset_for_anchor(&selection.start).unwrap() + 1, ) .unwrap(); } }) .unwrap(); buffer_2_updates.wait_next(&mut reactor).unwrap(); assert_eq!(selections(&buffer_1), selections(&buffer_3)); buffer_3_updates.wait_next(&mut reactor).unwrap(); assert_eq!(selections(&buffer_1), selections(&buffer_3)); buffer_1 .borrow_mut() .remove_selection_set(buffer_1_set_id) .unwrap(); buffer_1_updates.wait_next(&mut reactor).unwrap(); buffer_2_updates.wait_next(&mut reactor).unwrap(); assert_eq!(selections(&buffer_1), selections(&buffer_2)); buffer_3_updates.wait_next(&mut reactor).unwrap(); assert_eq!(selections(&buffer_1), selections(&buffer_3)); let sels = vec![empty_selection(&buffer_2.borrow(), 1)]; let buffer_2_set_id = buffer_2.borrow_mut().add_selection_set(0, sels); buffer_2_updates.wait_next(&mut reactor).unwrap(); buffer_1_updates.wait_next(&mut reactor).unwrap(); assert_eq!(selections(&buffer_1), selections(&buffer_2)); buffer_3_updates.wait_next(&mut reactor).unwrap(); assert_eq!(selections(&buffer_1), selections(&buffer_3)); buffer_2 .borrow_mut() .mutate_selections(buffer_2_set_id, |buffer, selections| { for selection in selections { selection.start = buffer .anchor_before_offset( buffer.offset_for_anchor(&selection.start).unwrap() + 1, ) .unwrap(); } }) .unwrap(); buffer_1_updates.wait_next(&mut reactor).unwrap(); assert_eq!(selections(&buffer_2), selections(&buffer_1)); buffer_3_updates.wait_next(&mut reactor).unwrap(); assert_eq!(selections(&buffer_2), selections(&buffer_3)); buffer_2 .borrow_mut() .remove_selection_set(buffer_2_set_id) .unwrap(); buffer_2_updates.wait_next(&mut reactor).unwrap(); buffer_1_updates.wait_next(&mut reactor).unwrap(); assert_eq!(selections(&buffer_1), selections(&buffer_2)); buffer_3_updates.wait_next(&mut reactor).unwrap(); assert_eq!(selections(&buffer_1), selections(&buffer_3)); drop(buffer_3); buffer_1_updates.wait_next(&mut reactor).unwrap(); for (replica_id, _, _) in selections(&buffer_1) { assert_eq!(buffer_1.borrow().replica_id, replica_id); } } struct RandomCharIter(T); impl Iterator for RandomCharIter { type Item = char; fn next(&mut self) -> Option { if self.0.gen_weighted_bool(5) { Some('\n') } else { Some(self.0.gen_range(b'a', b'z' + 1).into()) } } } fn selections(buffer: &Rc>) -> Vec<(ReplicaId, SelectionSetId, Selection)> { let buffer = buffer.borrow(); let mut selections = Vec::new(); for ((replica_id, set_id), selection_set) in &buffer.selections { for selection in selection_set.selections.iter() { selections.push((*replica_id, *set_id, selection.clone())); } } selections.sort_by(|a, b| match a.0.cmp(&b.0) { Ordering::Equal => a.1.cmp(&b.1), comparison @ _ => comparison, }); selections } fn empty_selection(buffer: &Buffer, offset: usize) -> Selection { let anchor = buffer.anchor_before_offset(offset).unwrap(); Selection { start: anchor.clone(), end: anchor, reversed: false, goal_column: None, } } } ================================================ FILE: xray_core/src/buffer_view.rs ================================================ use buffer::{self, Buffer, BufferId, Point, Selection, SelectionSetId}; use futures::{Future, Poll, Stream}; use movement; use notify_cell::NotifyCell; use serde_json; use std::cell::{Cell, Ref, RefCell}; use std::cmp::{self, Ordering}; use std::ops::Range; use std::rc::Rc; use window::{View, WeakViewHandle, Window}; use UserId; pub trait BufferViewDelegate { fn set_active_buffer_view(&mut self, buffer_view: WeakViewHandle); } pub struct BufferView { user_id: UserId, buffer: Rc>, updates_tx: NotifyCell<()>, updates_rx: Box>, dropped: NotifyCell, selection_set_id: SelectionSetId, height: Option, width: Option, line_height: f64, scroll_top: f64, vertical_margin: u32, horizontal_margin: u32, vertical_autoscroll: Option, horizontal_autoscroll: Cell>>, delegate: Option>, } #[derive(Debug, Eq, PartialEq, Serialize)] struct SelectionProps { pub user_id: UserId, pub start: Point, pub end: Point, pub reversed: bool, pub remote: bool, } #[derive(Debug, Deserialize)] #[serde(tag = "type")] enum BufferViewAction { UpdateScrollTop { delta: f64, }, SetDimensions { width: u64, height: u64, }, Edit { text: String, }, Backspace, Delete, MoveUp, MoveDown, MoveLeft, MoveRight, MoveToBeginningOfWord, MoveToEndOfWord, MoveToBeginningOfLine, MoveToEndOfLine, MoveToTop, MoveToBottom, SelectUp, SelectDown, SelectLeft, SelectRight, SelectToBeginningOfWord, SelectToEndOfWord, SelectToBeginningOfLine, SelectToEndOfLine, SelectToTop, SelectToBottom, SelectWord, SelectLine, AddSelectionAbove, AddSelectionBelow, SetCursorPosition { row: u32, column: u32, autoscroll: bool, }, } struct AutoScrollRequest { range: Range, center: bool, } impl BufferView { pub fn new( buffer: Rc>, user_id: UserId, delegate: Option>, ) -> Self { let selection_set_id = { let mut buffer = buffer.borrow_mut(); let start = buffer.anchor_before_offset(0).unwrap(); let end = buffer.anchor_before_offset(0).unwrap(); buffer.add_selection_set( user_id, vec![Selection { start, end, reversed: false, goal_column: None, }], ) }; let updates_tx = NotifyCell::new(()); let updates_rx = Box::new(updates_tx.observe().select(buffer.borrow().updates())); Self { user_id, updates_tx, updates_rx, buffer, selection_set_id, dropped: NotifyCell::new(false), height: None, width: None, line_height: 10.0, scroll_top: 0.0, vertical_margin: 2, horizontal_margin: 4, vertical_autoscroll: None, horizontal_autoscroll: Cell::new(None), delegate, } } pub fn set_height(&mut self, height: f64) -> &mut Self { debug_assert!(height >= 0_f64); self.height = Some(height); self.autoscroll_to_cursor(false); self.updated(); self } pub fn set_width(&mut self, width: f64) -> &mut Self { debug_assert!(width >= 0_f64); self.width = Some(width); self.autoscroll_to_cursor(false); self.updated(); self } pub fn set_line_height(&mut self, line_height: f64) -> &mut Self { debug_assert!(line_height > 0_f64); self.line_height = line_height; self.autoscroll_to_cursor(false); self.updated(); self } pub fn set_scroll_top(&mut self, scroll_top: f64) -> &mut Self { debug_assert!(scroll_top >= 0_f64); self.scroll_top = scroll_top; self.vertical_autoscroll = None; self.horizontal_autoscroll.replace(None); self.updated(); self } fn scroll_top(&self) -> f64 { let max_scroll_top = f64::from(self.buffer.borrow().max_point().row) * self.line_height; self.scroll_top.min(max_scroll_top) } fn scroll_bottom(&self) -> f64 { self.scroll_top() + self.height.unwrap_or(0.0) } pub fn save(&self) -> Option>> { self.buffer.borrow().save() } pub fn edit(&mut self, text: &str) { { let mut offset_ranges = Vec::new(); { let buffer = self.buffer.borrow(); for selection in self.selections().iter() { let start = buffer.offset_for_anchor(&selection.start).unwrap(); let end = buffer.offset_for_anchor(&selection.end).unwrap(); offset_ranges.push(start..end); } } let mut buffer = self.buffer.borrow_mut(); buffer.edit(&offset_ranges, text); let text_char_length = text.chars().count(); let mut delta = 0_isize; buffer .mutate_selections(self.selection_set_id, |buffer, selections| { *selections = offset_ranges .into_iter() .map(|range| { let start = range.start as isize; let end = range.end as isize; let anchor = buffer .anchor_before_offset((start + delta) as usize + text_char_length) .unwrap(); let deleted_count = end - start; delta += text_char_length as isize - deleted_count; Selection { start: anchor.clone(), end: anchor, reversed: false, goal_column: None, } }) .collect(); }) .unwrap(); } self.autoscroll_to_cursor(false); self.updated(); } pub fn backspace(&mut self) { if self.all_selections_are_empty() { self.select_left(); } self.edit(""); } pub fn delete(&mut self) { if self.all_selections_are_empty() { self.select_right(); } self.edit(""); } fn all_selections_are_empty(&self) -> bool { let buffer = self.buffer.borrow(); self.selections() .iter() .all(|selection| selection.is_empty(&buffer)) } pub fn set_selected_anchor_range( &mut self, range: Range, ) -> Result<(), buffer::Error> { { let mut buffer = self.buffer.borrow_mut(); // Ensure the supplied anchors are valid to preserve invariants. buffer.offset_for_anchor(&range.start)?; buffer.offset_for_anchor(&range.end)?; buffer.mutate_selections(self.selection_set_id, |_, selections| { selections.clear(); selections.push(Selection { start: range.start, end: range.end, reversed: false, goal_column: None, }); })?; } self.autoscroll_to_selection(true); self.updated(); Ok(()) } pub fn set_cursor_position(&mut self, position: Point, autoscroll: bool) { self.buffer .borrow_mut() .mutate_selections(self.selection_set_id, |buffer, selections| { // TODO: Clip point or return a result. let anchor = buffer.anchor_before_point(position).unwrap(); selections.clear(); selections.push(Selection { start: anchor.clone(), end: anchor, reversed: false, goal_column: None, }); }) .unwrap(); if autoscroll { self.autoscroll_to_cursor(false); } } pub fn add_selection(&mut self, start: Point, end: Point) { debug_assert!(start <= end); // TODO: Reverse selection if end < start self.buffer .borrow_mut() .mutate_selections(self.selection_set_id, |buffer, selections| { // TODO: Clip points or return a result. let start_anchor = buffer.anchor_before_point(start).unwrap(); let end_anchor = buffer.anchor_before_point(end).unwrap(); let index = match selections.binary_search_by(|probe| { buffer.cmp_anchors(&probe.start, &start_anchor).unwrap() }) { Ok(index) => index, Err(index) => index, }; selections.insert( index, Selection { start: start_anchor, end: end_anchor, reversed: false, goal_column: None, }, ); }) .unwrap(); self.autoscroll_to_cursor(false); } pub fn add_selection_above(&mut self) { self.buffer .borrow_mut() .insert_selections(self.selection_set_id, |buffer, selections| { let mut new_selections = Vec::new(); for selection in selections.iter() { let selection_start = buffer.point_for_anchor(&selection.start).unwrap(); let selection_end = buffer.point_for_anchor(&selection.end).unwrap(); if selection_start.row != selection_end.row { continue; } let goal_column = selection.goal_column.unwrap_or(selection_end.column); let mut row = selection_start.row; while row > 0 { row -= 1; let max_column = buffer.len_for_row(row).unwrap(); let start_column; let end_column; let add_selection; if selection_start == selection_end { start_column = cmp::min(goal_column, max_column); end_column = cmp::min(goal_column, max_column); add_selection = selection_end.column == 0 || end_column > 0; } else { start_column = cmp::min(selection_start.column, max_column); end_column = cmp::min(goal_column, max_column); add_selection = start_column != end_column; } if add_selection { new_selections.push(Selection { start: buffer .anchor_before_point(Point::new(row, start_column)) .unwrap(), end: buffer .anchor_before_point(Point::new(row, end_column)) .unwrap(), reversed: selection.reversed, goal_column: Some(goal_column), }); break; } } } new_selections }) .unwrap(); self.autoscroll_to_cursor(false); } pub fn add_selection_below(&mut self) { self.buffer .borrow_mut() .insert_selections(self.selection_set_id, |buffer, selections| { let max_row = buffer.max_point().row; let mut new_selections = Vec::new(); for selection in selections.iter() { let selection_start = buffer.point_for_anchor(&selection.start).unwrap(); let selection_end = buffer.point_for_anchor(&selection.end).unwrap(); if selection_start.row != selection_end.row { continue; } let goal_column = selection.goal_column.unwrap_or(selection_end.column); let mut row = selection_start.row; while row < max_row { row += 1; let max_column = buffer.len_for_row(row).unwrap(); let start_column; let end_column; let add_selection; if selection_start == selection_end { start_column = cmp::min(goal_column, max_column); end_column = cmp::min(goal_column, max_column); add_selection = selection_end.column == 0 || end_column > 0; } else { start_column = cmp::min(selection_start.column, max_column); end_column = cmp::min(goal_column, max_column); add_selection = start_column != end_column; } if add_selection { new_selections.push(Selection { start: buffer .anchor_before_point(Point::new(row, start_column)) .unwrap(), end: buffer .anchor_before_point(Point::new(row, end_column)) .unwrap(), reversed: selection.reversed, goal_column: Some(goal_column), }); break; } } } new_selections }) .unwrap(); self.autoscroll_to_cursor(false); } pub fn move_left(&mut self) { self.buffer .borrow_mut() .mutate_selections(self.selection_set_id, |buffer, selections| { for selection in selections.iter_mut() { let start = buffer.point_for_anchor(&selection.start).unwrap(); let end = buffer.point_for_anchor(&selection.end).unwrap(); if start != end { selection.end = selection.start.clone(); } else { let cursor = buffer .anchor_before_point(movement::left(&buffer, start)) .unwrap(); selection.start = cursor.clone(); selection.end = cursor; } selection.reversed = false; selection.goal_column = None; } }) .unwrap(); self.autoscroll_to_cursor(false); } pub fn select_left(&mut self) { self.buffer .borrow_mut() .mutate_selections(self.selection_set_id, |buffer, selections| { for selection in selections.iter_mut() { let head = buffer.point_for_anchor(selection.head()).unwrap(); let cursor = buffer .anchor_before_point(movement::left(&buffer, head)) .unwrap(); selection.set_head(&buffer, cursor); selection.goal_column = None; } }) .unwrap(); self.autoscroll_to_cursor(false); } pub fn move_right(&mut self) { self.buffer .borrow_mut() .mutate_selections(self.selection_set_id, |buffer, selections| { for selection in selections.iter_mut() { let start = buffer.point_for_anchor(&selection.start).unwrap(); let end = buffer.point_for_anchor(&selection.end).unwrap(); if start != end { selection.start = selection.end.clone(); } else { let cursor = buffer .anchor_before_point(movement::right(&buffer, end)) .unwrap(); selection.start = cursor.clone(); selection.end = cursor; } selection.reversed = false; selection.goal_column = None; } }) .unwrap(); self.autoscroll_to_cursor(false); } pub fn select_right(&mut self) { self.buffer .borrow_mut() .mutate_selections(self.selection_set_id, |buffer, selections| { for selection in selections.iter_mut() { let head = buffer.point_for_anchor(selection.head()).unwrap(); let cursor = buffer .anchor_before_point(movement::right(&buffer, head)) .unwrap(); selection.set_head(&buffer, cursor); selection.goal_column = None; } }) .unwrap(); self.autoscroll_to_cursor(false); } pub fn move_up(&mut self) { self.buffer .borrow_mut() .mutate_selections(self.selection_set_id, |buffer, selections| { for selection in selections.iter_mut() { let start = buffer.point_for_anchor(&selection.start).unwrap(); let end = buffer.point_for_anchor(&selection.end).unwrap(); if start != end { selection.goal_column = None; } let (start, goal_column) = movement::up(&buffer, start, selection.goal_column); let cursor = buffer.anchor_before_point(start).unwrap(); selection.start = cursor.clone(); selection.end = cursor; selection.goal_column = goal_column; selection.reversed = false; } }) .unwrap(); self.autoscroll_to_cursor(false); } pub fn select_up(&mut self) { self.buffer .borrow_mut() .mutate_selections(self.selection_set_id, |buffer, selections| { for selection in selections.iter_mut() { let head = buffer.point_for_anchor(selection.head()).unwrap(); let (head, goal_column) = movement::up(&buffer, head, selection.goal_column); selection.set_head(&buffer, buffer.anchor_before_point(head).unwrap()); selection.goal_column = goal_column; } }) .unwrap(); self.autoscroll_to_cursor(false); } pub fn move_down(&mut self) { self.buffer .borrow_mut() .mutate_selections(self.selection_set_id, |buffer, selections| { for selection in selections.iter_mut() { let start = buffer.point_for_anchor(&selection.start).unwrap(); let end = buffer.point_for_anchor(&selection.end).unwrap(); if start != end { selection.goal_column = None; } let (start, goal_column) = movement::down(&buffer, end, selection.goal_column); let cursor = buffer.anchor_before_point(start).unwrap(); selection.start = cursor.clone(); selection.end = cursor; selection.goal_column = goal_column; selection.reversed = false; } }) .unwrap(); self.autoscroll_to_cursor(false); } pub fn select_down(&mut self) { self.buffer .borrow_mut() .mutate_selections(self.selection_set_id, |buffer, selections| { for selection in selections.iter_mut() { let head = buffer.point_for_anchor(selection.head()).unwrap(); let (head, goal_column) = movement::down(&buffer, head, selection.goal_column); selection.set_head(&buffer, buffer.anchor_before_point(head).unwrap()); selection.goal_column = goal_column; } }) .unwrap(); self.autoscroll_to_cursor(false); } pub fn move_to_beginning_of_word(&mut self) { self.buffer .borrow_mut() .mutate_selections(self.selection_set_id, |buffer, selections| { for selection in selections.iter_mut() { let old_head = buffer.point_for_anchor(selection.head()).unwrap(); let new_head = movement::beginning_of_word(buffer, old_head); let anchor = buffer.anchor_before_point(new_head).unwrap(); selection.start = anchor.clone(); selection.end = anchor; selection.goal_column = None; selection.reversed = false; } }) .unwrap(); self.autoscroll_to_cursor(false); } pub fn move_to_end_of_word(&mut self) { self.buffer .borrow_mut() .mutate_selections(self.selection_set_id, |buffer, selections| { for selection in selections.iter_mut() { let old_head = buffer.point_for_anchor(selection.head()).unwrap(); let new_head = movement::end_of_word(buffer, old_head); let anchor = buffer.anchor_before_point(new_head).unwrap(); selection.start = anchor.clone(); selection.end = anchor; selection.goal_column = None; selection.reversed = false; } }) .unwrap(); self.autoscroll_to_cursor(false); } pub fn select_to_beginning_of_word(&mut self) { self.buffer .borrow_mut() .mutate_selections(self.selection_set_id, |buffer, selections| { for selection in selections.iter_mut() { let old_head = buffer.point_for_anchor(selection.head()).unwrap(); let new_head = movement::beginning_of_word(buffer, old_head); let anchor = buffer.anchor_before_point(new_head).unwrap(); selection.set_head(buffer, anchor); selection.goal_column = None; } }) .unwrap(); self.autoscroll_to_cursor(false); } pub fn select_to_end_of_word(&mut self) { self.buffer .borrow_mut() .mutate_selections(self.selection_set_id, |buffer, selections| { for selection in selections.iter_mut() { let old_head = buffer.point_for_anchor(selection.head()).unwrap(); let new_head = movement::end_of_word(buffer, old_head); let anchor = buffer.anchor_before_point(new_head).unwrap(); selection.set_head(buffer, anchor); selection.goal_column = None; } }) .unwrap(); self.autoscroll_to_cursor(false); } pub fn select_word(&mut self) { self.buffer .borrow_mut() .mutate_selections(self.selection_set_id, |buffer, selections| { for selection in selections.iter_mut() { let old_head = buffer.point_for_anchor(selection.head()).unwrap(); let new_start = movement::beginning_of_word(buffer, old_head); let new_end = movement::end_of_word(buffer, new_start); selection.start = buffer.anchor_before_point(new_start).unwrap(); selection.end = buffer.anchor_before_point(new_end).unwrap(); selection.reversed = false; selection.goal_column = None; } }) .unwrap(); } pub fn move_to_beginning_of_line(&mut self) { self.buffer .borrow_mut() .mutate_selections(self.selection_set_id, |buffer, selections| { for selection in selections.iter_mut() { let old_head = buffer.point_for_anchor(selection.head()).unwrap(); let new_head = movement::beginning_of_line(old_head); let anchor = buffer.anchor_before_point(new_head).unwrap(); selection.start = anchor.clone(); selection.end = anchor; selection.goal_column = None; selection.reversed = false; } }) .unwrap(); self.autoscroll_to_cursor(false); } pub fn move_to_end_of_line(&mut self) { self.buffer .borrow_mut() .mutate_selections(self.selection_set_id, |buffer, selections| { for selection in selections.iter_mut() { let old_head = buffer.point_for_anchor(selection.head()).unwrap(); let new_head = movement::end_of_line(buffer, old_head); let anchor = buffer.anchor_before_point(new_head).unwrap(); selection.start = anchor.clone(); selection.end = anchor; selection.goal_column = None; selection.reversed = false; } }) .unwrap(); self.autoscroll_to_cursor(false); } pub fn select_to_beginning_of_line(&mut self) { self.buffer .borrow_mut() .mutate_selections(self.selection_set_id, |buffer, selections| { for selection in selections.iter_mut() { let old_head = buffer.point_for_anchor(selection.head()).unwrap(); let new_head = movement::beginning_of_line(old_head); let anchor = buffer.anchor_before_point(new_head).unwrap(); selection.set_head(buffer, anchor); selection.goal_column = None; } }) .unwrap(); self.autoscroll_to_cursor(false); } pub fn select_to_end_of_line(&mut self) { self.buffer .borrow_mut() .mutate_selections(self.selection_set_id, |buffer, selections| { for selection in selections.iter_mut() { let old_head = buffer.point_for_anchor(selection.head()).unwrap(); let new_head = movement::end_of_line(buffer, old_head); let anchor = buffer.anchor_before_point(new_head).unwrap(); selection.set_head(buffer, anchor); selection.goal_column = None; } }) .unwrap(); self.autoscroll_to_cursor(false); } pub fn select_line(&mut self) { self.buffer .borrow_mut() .mutate_selections(self.selection_set_id, |buffer, selections| { let max_point = buffer.max_point(); for selection in selections.iter_mut() { let old_head = buffer.point_for_anchor(selection.head()).unwrap(); let new_start = movement::beginning_of_line(old_head); let new_end = cmp::min(Point::new(new_start.row + 1, 0), max_point); selection.start = buffer.anchor_before_point(new_start).unwrap(); selection.end = buffer.anchor_before_point(new_end).unwrap(); selection.reversed = false; selection.goal_column = None; } }) .unwrap(); } pub fn move_to_top(&mut self) { self.buffer .borrow_mut() .mutate_selections(self.selection_set_id, |buffer, selections| { for selection in selections.iter_mut() { let anchor = buffer.anchor_before_point(Point::new(0, 0)).unwrap(); selection.start = anchor.clone(); selection.end = anchor; selection.goal_column = None; selection.reversed = false; } }) .unwrap(); self.autoscroll_to_cursor(false); } pub fn move_to_bottom(&mut self) { self.buffer .borrow_mut() .mutate_selections(self.selection_set_id, |buffer, selections| { for selection in selections.iter_mut() { let anchor = buffer.anchor_before_point(buffer.max_point()).unwrap(); selection.start = anchor.clone(); selection.end = anchor; selection.goal_column = None; selection.reversed = false; } }) .unwrap(); self.autoscroll_to_cursor(false); } pub fn select_to_top(&mut self) { self.buffer .borrow_mut() .mutate_selections(self.selection_set_id, |buffer, selections| { for selection in selections.iter_mut() { let anchor = buffer.anchor_before_point(Point::new(0, 0)).unwrap(); selection.set_head(buffer, anchor); selection.goal_column = None; } }) .unwrap(); self.autoscroll_to_cursor(false); } pub fn select_to_bottom(&mut self) { self.buffer .borrow_mut() .mutate_selections(self.selection_set_id, |buffer, selections| { for selection in selections.iter_mut() { let anchor = buffer.anchor_before_point(buffer.max_point()).unwrap(); selection.set_head(buffer, anchor); selection.goal_column = None; } }) .unwrap(); self.autoscroll_to_cursor(false); } pub fn selections(&self) -> Ref<[Selection]> { Ref::map(self.buffer.borrow(), |buffer| { buffer.selections(self.selection_set_id).unwrap() }) } pub fn buffer_id(&self) -> BufferId { self.buffer.borrow().id() } fn render_selections(&self, range: Range) -> Vec { let buffer = self.buffer.borrow(); let mut rendered_selections = Vec::new(); for (user_id, selections) in buffer.remote_selections() { for selection in self.query_selections(selections, &range) { rendered_selections.push(SelectionProps { user_id, start: buffer.point_for_anchor(&selection.start).unwrap(), end: buffer.point_for_anchor(&selection.end).unwrap(), reversed: selection.reversed, remote: true, }); } } for selection in self.query_selections(&buffer.selections(self.selection_set_id).unwrap(), &range) { rendered_selections.push(SelectionProps { user_id: self.user_id, start: buffer.point_for_anchor(&selection.start).unwrap(), end: buffer.point_for_anchor(&selection.end).unwrap(), reversed: selection.reversed, remote: false, }); } rendered_selections } fn query_selections<'a>( &self, selections: &'a [Selection], range: &Range, ) -> &'a [Selection] { let buffer = self.buffer.borrow(); let start = buffer.anchor_before_point(range.start).unwrap(); let start_index = match selections .binary_search_by(|probe| buffer.cmp_anchors(&probe.start, &start).unwrap()) { Ok(index) => index, Err(index) => { if index > 0 && buffer .cmp_anchors(&selections[index - 1].end, &start) .unwrap() == Ordering::Greater { index - 1 } else { index } } }; if range.end > buffer.max_point() { &selections[start_index..] } else { let end = buffer.anchor_after_point(range.end).unwrap(); let end_index = match selections .binary_search_by(|probe| buffer.cmp_anchors(&probe.start, &end).unwrap()) { Ok(index) => index, Err(index) => index, }; &selections[start_index..end_index] } } fn autoscroll_to_cursor(&mut self, center: bool) { let anchor = { let selections = self.selections(); let selection = selections.last().unwrap(); if selection.reversed { selection.start.clone() } else { selection.end.clone() } }; self.autoscroll_to_range(anchor.clone()..anchor, center) .unwrap(); } fn autoscroll_to_selection(&mut self, center: bool) { let range = { let selections = self.selections(); let selection = selections.last().unwrap(); selection.start.clone()..selection.end.clone() }; self.autoscroll_to_range(range, center).unwrap(); } fn flush_vertical_autoscroll_to_selection(&mut self) { if let Some(request) = self.vertical_autoscroll.take() { self.autoscroll_to_range(request.range, request.center) .unwrap(); } } fn autoscroll_to_range( &mut self, range: Range, center: bool, ) -> Result<(), buffer::Error> { // Ensure points are valid even if we can't autoscroll immediately because // flush_vertical_autoscroll_to_selection unwraps. let (start, end) = { let buffer = self.buffer.borrow(); let start = buffer.point_for_anchor(&range.start)?; let end = buffer.point_for_anchor(&range.end)?; (start, end) }; if let Some(height) = self.height { let desired_top; let desired_bottom; if center { let center_position = ((start.row + end.row) as f64 / 2_f64) * self.line_height; desired_top = 0_f64.max(center_position - height / 2_f64); desired_bottom = center_position + height / 2_f64; } else { desired_top = start.row.saturating_sub(self.vertical_margin) as f64 * self.line_height; desired_bottom = end.row.saturating_add(self.vertical_margin) as f64 * self.line_height; } if self.scroll_top() > desired_top { self.set_scroll_top(desired_top); } else if self.scroll_bottom() < desired_bottom { self.set_scroll_top(desired_bottom - height); } self.horizontal_autoscroll.replace(Some(range)); } else { self.vertical_autoscroll = Some(AutoScrollRequest { range, center }); self.horizontal_autoscroll.replace(None); } Ok(()) } fn updated(&mut self) { self.updates_tx.set(()); } } impl View for BufferView { fn component_name(&self) -> &'static str { "BufferView" } fn will_mount(&mut self, window: &mut Window, self_handle: WeakViewHandle) { self.height = Some(window.height()); self.flush_vertical_autoscroll_to_selection(); if let Some(ref delegate) = self.delegate { delegate.map(|delegate| delegate.set_active_buffer_view(self_handle)); } } fn render(&self) -> serde_json::Value { let buffer = self.buffer.borrow(); let start = Point::new((self.scroll_top() / self.line_height).floor() as u32, 0); let end = Point::new((self.scroll_bottom() / self.line_height).ceil() as u32, 0); let mut lines = Vec::new(); let mut cur_line = Vec::new(); let mut cur_row = start.row; for c in buffer.iter_starting_at_point(start) { if c == u16::from(b'\n') { lines.push(String::from_utf16_lossy(&cur_line)); cur_line = Vec::new(); cur_row += 1; if cur_row >= end.row { break; } } else { cur_line.push(c); } } if cur_row < end.row { lines.push(String::from_utf16_lossy(&cur_line)); } let longest_row = buffer.longest_row(); let longest_line = if start.row <= longest_row && longest_row <= end.row { lines[(longest_row - start.row) as usize].clone() } else { String::from_utf16_lossy(&buffer.line(buffer.longest_row()).unwrap()) }; let horizontal_autoscroll = self.horizontal_autoscroll.take().map(|range| { let scroll_start = buffer.point_for_anchor(&range.start).unwrap(); let scroll_end = buffer.point_for_anchor(&range.end).unwrap(); let start_line = if start.row <= scroll_start.row && scroll_start.row <= end.row { lines[(scroll_start.row - start.row) as usize].clone() } else { String::from_utf16_lossy(&buffer.line(scroll_start.row).unwrap()) }; let end_line = if start.row <= scroll_end.row && scroll_end.row <= end.row { lines[(scroll_end.row - start.row) as usize].clone() } else { String::from_utf16_lossy(&buffer.line(scroll_end.row).unwrap()) }; json!({ "start": scroll_start, "start_line": start_line, "end": scroll_end, "end_line": end_line, }) }); json!({ "first_visible_row": start.row, "total_row_count": buffer.max_point().row + 1, "lines": lines, "longest_line": longest_line, "scroll_top": self.scroll_top(), "horizontal_autoscroll": horizontal_autoscroll, "horizontal_margin": self.horizontal_margin, "height": self.height, "width": self.width, "line_height": self.line_height, "selections": self.render_selections(start..end), }) } fn dispatch_action(&mut self, action: serde_json::Value, _: &mut Window) { match serde_json::from_value(action) { Ok(BufferViewAction::UpdateScrollTop { delta }) => { let mut scroll_top = self.scroll_top() + delta; if scroll_top < 0.0 { scroll_top = 0.0; } self.set_scroll_top(scroll_top); } Ok(BufferViewAction::SetDimensions { width, height }) => { self.set_width(width as f64); self.set_height(height as f64); } Ok(BufferViewAction::Edit { text }) => self.edit(text.as_str()), Ok(BufferViewAction::Backspace) => self.backspace(), Ok(BufferViewAction::Delete) => self.delete(), Ok(BufferViewAction::MoveUp) => self.move_up(), Ok(BufferViewAction::MoveDown) => self.move_down(), Ok(BufferViewAction::MoveLeft) => self.move_left(), Ok(BufferViewAction::MoveRight) => self.move_right(), Ok(BufferViewAction::MoveToBeginningOfWord) => self.move_to_beginning_of_word(), Ok(BufferViewAction::MoveToEndOfWord) => self.move_to_end_of_word(), Ok(BufferViewAction::MoveToBeginningOfLine) => self.move_to_beginning_of_line(), Ok(BufferViewAction::MoveToEndOfLine) => self.move_to_end_of_line(), Ok(BufferViewAction::MoveToTop) => self.move_to_top(), Ok(BufferViewAction::MoveToBottom) => self.move_to_bottom(), Ok(BufferViewAction::SelectUp) => self.select_up(), Ok(BufferViewAction::SelectDown) => self.select_down(), Ok(BufferViewAction::SelectLeft) => self.select_left(), Ok(BufferViewAction::SelectRight) => self.select_right(), Ok(BufferViewAction::SelectToBeginningOfWord) => self.select_to_beginning_of_word(), Ok(BufferViewAction::SelectToEndOfWord) => self.select_to_end_of_word(), Ok(BufferViewAction::SelectToBeginningOfLine) => self.select_to_beginning_of_line(), Ok(BufferViewAction::SelectToEndOfLine) => self.select_to_end_of_line(), Ok(BufferViewAction::SelectToTop) => self.select_to_top(), Ok(BufferViewAction::SelectToBottom) => self.select_to_bottom(), Ok(BufferViewAction::SelectWord) => self.select_word(), Ok(BufferViewAction::SelectLine) => self.select_line(), Ok(BufferViewAction::AddSelectionAbove) => self.add_selection_above(), Ok(BufferViewAction::AddSelectionBelow) => self.add_selection_below(), Ok(BufferViewAction::SetCursorPosition { row, column, autoscroll, }) => self.set_cursor_position(Point::new(row, column), autoscroll), Err(action) => eprintln!("Unrecognized action {:?}", action), } } } impl Stream for BufferView { type Item = (); type Error = (); fn poll(&mut self) -> Poll, Self::Error> { self.updates_rx.poll() } } impl Drop for BufferView { fn drop(&mut self) { self.buffer .borrow_mut() .remove_selection_set(self.selection_set_id) .unwrap(); self.dropped.set(true); } } #[cfg(test)] mod tests { use super::*; use IntoShared; #[test] fn test_cursor_movement() { let mut editor = BufferView::new(Rc::new(RefCell::new(Buffer::new(0))), 0, None); editor.buffer.borrow_mut().edit(&[0..0], "abc"); editor.buffer.borrow_mut().edit(&[3..3], "\n"); editor.buffer.borrow_mut().edit(&[4..4], "\ndef"); assert_eq!(render_selections(&editor), vec![empty_selection(0, 0)]); editor.move_right(); assert_eq!(render_selections(&editor), vec![empty_selection(0, 1)]); // Wraps across lines moving right for _ in 0..3 { editor.move_right(); } assert_eq!(render_selections(&editor), vec![empty_selection(1, 0)]); // Stops at end for _ in 0..4 { editor.move_right(); } assert_eq!(render_selections(&editor), vec![empty_selection(2, 3)]); // Wraps across lines moving left for _ in 0..4 { editor.move_left(); } assert_eq!(render_selections(&editor), vec![empty_selection(1, 0)]); // Stops at start for _ in 0..4 { editor.move_left(); } assert_eq!(render_selections(&editor), vec![empty_selection(0, 0)]); // Moves down and up at column 0 editor.move_down(); assert_eq!(render_selections(&editor), vec![empty_selection(1, 0)]); editor.move_up(); assert_eq!(render_selections(&editor), vec![empty_selection(0, 0)]); // Maintains a goal column when moving down // This means we'll jump to the column we started with even after crossing a shorter line editor.move_right(); editor.move_right(); editor.move_down(); assert_eq!(render_selections(&editor), vec![empty_selection(1, 0)]); editor.move_down(); assert_eq!(render_selections(&editor), vec![empty_selection(2, 2)]); // Jumps to end when moving down on the last line. editor.move_down(); assert_eq!(render_selections(&editor), vec![empty_selection(2, 3)]); // Stops at end editor.move_down(); assert_eq!(render_selections(&editor), vec![empty_selection(2, 3)]); // Resets the goal column when moving horizontally editor.move_left(); editor.move_left(); editor.move_up(); assert_eq!(render_selections(&editor), vec![empty_selection(1, 0)]); editor.move_up(); assert_eq!(render_selections(&editor), vec![empty_selection(0, 1)]); // Jumps to start when moving up on the first line editor.move_up(); assert_eq!(render_selections(&editor), vec![empty_selection(0, 0)]); // Preserves goal column after jumping to start/end editor.move_down(); editor.move_down(); assert_eq!(render_selections(&editor), vec![empty_selection(2, 1)]); editor.move_down(); assert_eq!(render_selections(&editor), vec![empty_selection(2, 3)]); editor.move_up(); editor.move_up(); assert_eq!(render_selections(&editor), vec![empty_selection(0, 1)]); } #[test] fn test_selection_movement() { let mut editor = BufferView::new(Rc::new(RefCell::new(Buffer::new(0))), 0, None); editor.buffer.borrow_mut().edit(&[0..0], "abc"); editor.buffer.borrow_mut().edit(&[3..3], "\n"); editor.buffer.borrow_mut().edit(&[4..4], "\ndef"); assert_eq!(render_selections(&editor), vec![empty_selection(0, 0)]); editor.select_right(); assert_eq!(render_selections(&editor), vec![selection((0, 0), (0, 1))]); // Selecting right wraps across newlines for _ in 0..3 { editor.select_right(); } assert_eq!(render_selections(&editor), vec![selection((0, 0), (1, 0))]); // Moving right with a non-empty selection clears the selection editor.move_right(); assert_eq!(render_selections(&editor), vec![empty_selection(1, 0)]); editor.move_right(); assert_eq!(render_selections(&editor), vec![empty_selection(2, 0)]); // Selecting left wraps across newlines editor.select_left(); assert_eq!( render_selections(&editor), vec![rev_selection((1, 0), (2, 0))] ); editor.select_left(); assert_eq!( render_selections(&editor), vec![rev_selection((0, 3), (2, 0))] ); // Moving left with a non-empty selection clears the selection editor.move_left(); assert_eq!(render_selections(&editor), vec![empty_selection(0, 3)]); // Reverse is updated correctly when selecting left and right editor.select_left(); assert_eq!( render_selections(&editor), vec![rev_selection((0, 2), (0, 3))] ); editor.select_right(); assert_eq!(render_selections(&editor), vec![empty_selection(0, 3)]); editor.select_right(); assert_eq!(render_selections(&editor), vec![selection((0, 3), (1, 0))]); editor.select_left(); assert_eq!(render_selections(&editor), vec![empty_selection(0, 3)]); editor.select_left(); assert_eq!( render_selections(&editor), vec![rev_selection((0, 2), (0, 3))] ); // Selecting vertically moves the head and updates the reversed property editor.select_left(); assert_eq!( render_selections(&editor), vec![rev_selection((0, 1), (0, 3))] ); editor.select_down(); assert_eq!(render_selections(&editor), vec![selection((0, 3), (1, 0))]); editor.select_down(); assert_eq!(render_selections(&editor), vec![selection((0, 3), (2, 1))]); editor.select_up(); editor.select_up(); assert_eq!( render_selections(&editor), vec![rev_selection((0, 1), (0, 3))] ); // Favors selection end when moving down editor.move_down(); editor.move_down(); assert_eq!(render_selections(&editor), vec![empty_selection(2, 3)]); // Favors selection start when moving up editor.move_left(); editor.move_left(); editor.select_right(); editor.select_right(); assert_eq!(render_selections(&editor), vec![selection((2, 1), (2, 3))]); editor.move_up(); editor.move_up(); assert_eq!(render_selections(&editor), vec![empty_selection(0, 1)]); } #[test] fn test_move_to_beginning_or_end_of_word() { let mut editor = BufferView::new(Rc::new(RefCell::new(Buffer::new(0))), 0, None); editor.buffer.borrow_mut().edit(&[0..0], "abc def\nghi.jkl"); assert_eq!(render_selections(&editor), vec![empty_selection(0, 0)]); editor.move_to_end_of_word(); assert_eq!(render_selections(&editor), vec![empty_selection(0, 3)]); editor.move_to_end_of_word(); assert_eq!(render_selections(&editor), vec![empty_selection(0, 4)]); editor.move_to_end_of_word(); assert_eq!(render_selections(&editor), vec![empty_selection(0, 7)]); editor.move_to_end_of_word(); assert_eq!(render_selections(&editor), vec![empty_selection(1, 0)]); editor.move_to_end_of_word(); assert_eq!(render_selections(&editor), vec![empty_selection(1, 3)]); editor.move_to_end_of_word(); assert_eq!(render_selections(&editor), vec![empty_selection(1, 4)]); editor.move_to_end_of_word(); assert_eq!(render_selections(&editor), vec![empty_selection(1, 7)]); editor.move_to_end_of_word(); assert_eq!(render_selections(&editor), vec![empty_selection(1, 7)]); editor.move_to_beginning_of_word(); assert_eq!(render_selections(&editor), vec![empty_selection(1, 4)]); editor.move_to_beginning_of_word(); assert_eq!(render_selections(&editor), vec![empty_selection(1, 3)]); editor.move_to_beginning_of_word(); assert_eq!(render_selections(&editor), vec![empty_selection(1, 0)]); editor.move_to_beginning_of_word(); assert_eq!(render_selections(&editor), vec![empty_selection(0, 7)]); editor.move_to_beginning_of_word(); assert_eq!(render_selections(&editor), vec![empty_selection(0, 4)]); editor.move_to_beginning_of_word(); assert_eq!(render_selections(&editor), vec![empty_selection(0, 3)]); editor.move_to_beginning_of_word(); assert_eq!(render_selections(&editor), vec![empty_selection(0, 0)]); editor.move_to_beginning_of_word(); assert_eq!(render_selections(&editor), vec![empty_selection(0, 0)]); } #[test] fn test_select_to_beginning_or_end_of_word() { let mut editor = BufferView::new(Rc::new(RefCell::new(Buffer::new(0))), 0, None); editor.buffer.borrow_mut().edit(&[0..0], "abc def\nghi.jkl"); assert_eq!(render_selections(&editor), vec![empty_selection(0, 0)]); editor.select_to_end_of_word(); assert_eq!(render_selections(&editor), vec![selection((0, 0), (0, 3))]); editor.select_to_end_of_word(); assert_eq!(render_selections(&editor), vec![selection((0, 0), (0, 4))]); editor.select_to_end_of_word(); assert_eq!(render_selections(&editor), vec![selection((0, 0), (0, 7))]); editor.select_to_end_of_word(); assert_eq!(render_selections(&editor), vec![selection((0, 0), (1, 0))]); editor.select_to_end_of_word(); assert_eq!(render_selections(&editor), vec![selection((0, 0), (1, 3))]); editor.select_to_end_of_word(); assert_eq!(render_selections(&editor), vec![selection((0, 0), (1, 4))]); editor.select_to_end_of_word(); assert_eq!(render_selections(&editor), vec![selection((0, 0), (1, 7))]); editor.select_to_end_of_word(); assert_eq!(render_selections(&editor), vec![selection((0, 0), (1, 7))]); editor.move_right(); assert_eq!(render_selections(&editor), vec![empty_selection(1, 7)]); editor.select_to_beginning_of_word(); assert_eq!( render_selections(&editor), vec![rev_selection((1, 4), (1, 7))] ); editor.select_to_beginning_of_word(); assert_eq!( render_selections(&editor), vec![rev_selection((1, 3), (1, 7))] ); editor.select_to_beginning_of_word(); assert_eq!( render_selections(&editor), vec![rev_selection((1, 0), (1, 7))] ); editor.select_to_beginning_of_word(); assert_eq!( render_selections(&editor), vec![rev_selection((0, 7), (1, 7))] ); editor.select_to_beginning_of_word(); assert_eq!( render_selections(&editor), vec![rev_selection((0, 4), (1, 7))] ); editor.select_to_beginning_of_word(); assert_eq!( render_selections(&editor), vec![rev_selection((0, 3), (1, 7))] ); editor.select_to_beginning_of_word(); assert_eq!( render_selections(&editor), vec![rev_selection((0, 0), (1, 7))] ); editor.select_to_beginning_of_word(); assert_eq!( render_selections(&editor), vec![rev_selection((0, 0), (1, 7))] ); } #[test] fn test_move_to_beginning_or_end_of_line() { let mut editor = BufferView::new(Rc::new(RefCell::new(Buffer::new(0))), 0, None); editor .buffer .borrow_mut() .edit(&[0..0], "abcdef\nghijklmno"); assert_eq!(render_selections(&editor), vec![empty_selection(0, 0)]); editor.move_to_end_of_line(); assert_eq!(render_selections(&editor), vec![empty_selection(0, 6)]); editor.move_to_end_of_line(); assert_eq!(render_selections(&editor), vec![empty_selection(0, 6)]); editor.move_right(); editor.move_to_end_of_line(); assert_eq!(render_selections(&editor), vec![empty_selection(1, 9)]); editor.move_to_beginning_of_line(); assert_eq!(render_selections(&editor), vec![empty_selection(1, 0)]); editor.move_to_beginning_of_line(); assert_eq!(render_selections(&editor), vec![empty_selection(1, 0)]); editor.move_left(); editor.move_to_beginning_of_line(); assert_eq!(render_selections(&editor), vec![empty_selection(0, 0)]); } #[test] fn test_select_to_beginning_or_end_of_line() { let mut editor = BufferView::new(Rc::new(RefCell::new(Buffer::new(0))), 0, None); editor .buffer .borrow_mut() .edit(&[0..0], "abcdef\nghijklmno"); assert_eq!(render_selections(&editor), vec![empty_selection(0, 0)]); editor.select_to_end_of_line(); assert_eq!(render_selections(&editor), vec![selection((0, 0), (0, 6))]); editor.select_to_end_of_line(); assert_eq!(render_selections(&editor), vec![selection((0, 0), (0, 6))]); editor.move_right(); editor.move_right(); editor.select_to_end_of_line(); assert_eq!(render_selections(&editor), vec![selection((1, 0), (1, 9))]); editor.move_right(); editor.select_to_beginning_of_line(); assert_eq!( render_selections(&editor), vec![rev_selection((1, 0), (1, 9))] ); editor.select_to_beginning_of_line(); assert_eq!( render_selections(&editor), vec![rev_selection((1, 0), (1, 9))] ); editor.move_left(); editor.move_left(); editor.select_to_beginning_of_line(); assert_eq!( render_selections(&editor), vec![rev_selection((0, 0), (0, 6))] ); } #[test] fn test_select_word() { let mut editor = BufferView::new(Rc::new(RefCell::new(Buffer::new(0))), 0, None); editor.buffer.borrow_mut().edit(&[0..0], "abc.def---ghi"); editor.set_cursor_position(Point::new(0, 5), false); editor.select_word(); assert_eq!(render_selections(&editor), vec![selection((0, 4), (0, 7))]); editor.set_cursor_position(Point::new(0, 8), false); editor.select_word(); assert_eq!(render_selections(&editor), vec![selection((0, 7), (0, 10))]); } #[test] fn test_select_line() { let mut editor = BufferView::new(Rc::new(RefCell::new(Buffer::new(0))), 0, None); editor.buffer.borrow_mut().edit(&[0..0], "abc\ndef\nghi"); editor.set_cursor_position(Point::new(0, 2), false); editor.select_line(); assert_eq!(render_selections(&editor), vec![selection((0, 0), (1, 0))]); editor.set_cursor_position(Point::new(2, 1), false); editor.select_line(); assert_eq!(render_selections(&editor), vec![selection((2, 0), (2, 3))]); } #[test] fn test_move_to_top_or_bottom() { let mut editor = BufferView::new(Rc::new(RefCell::new(Buffer::new(0))), 0, None); editor.buffer.borrow_mut().edit(&[0..0], "abc\ndef\nghi"); assert_eq!(render_selections(&editor), vec![empty_selection(0, 0)]); editor.move_to_bottom(); assert_eq!(render_selections(&editor), vec![empty_selection(2, 3)]); editor.move_to_top(); assert_eq!(render_selections(&editor), vec![empty_selection(0, 0)]); } #[test] fn test_select_to_top_or_bottom() { let mut editor = BufferView::new(Rc::new(RefCell::new(Buffer::new(0))), 0, None); editor.buffer.borrow_mut().edit(&[0..0], "abc\ndef\nghi"); assert_eq!(render_selections(&editor), vec![empty_selection(0, 0)]); editor.select_to_bottom(); assert_eq!(render_selections(&editor), vec![selection((0, 0), (2, 3))]); editor.move_right(); editor.select_to_top(); assert_eq!( render_selections(&editor), vec![rev_selection((0, 0), (2, 3))] ); } #[test] fn test_backspace() { let mut editor = BufferView::new(Rc::new(RefCell::new(Buffer::new(0))), 0, None); editor.buffer.borrow_mut().edit(&[0..0], "abcdefghi"); editor.add_selection(Point::new(0, 3), Point::new(0, 4)); editor.add_selection(Point::new(0, 9), Point::new(0, 9)); editor.backspace(); assert_eq!(editor.buffer.borrow().to_string(), "abcefghi"); editor.backspace(); assert_eq!(editor.buffer.borrow().to_string(), "abefgh"); } #[test] fn test_delete() { let mut editor = BufferView::new(Rc::new(RefCell::new(Buffer::new(0))), 0, None); editor.buffer.borrow_mut().edit(&[0..0], "abcdefghi"); editor.add_selection(Point::new(0, 3), Point::new(0, 4)); editor.add_selection(Point::new(0, 9), Point::new(0, 9)); editor.delete(); assert_eq!(editor.buffer.borrow().to_string(), "abcefghi"); editor.delete(); assert_eq!(editor.buffer.borrow().to_string(), "bcfghi"); } #[test] fn test_add_selection() { let mut editor = BufferView::new(Rc::new(RefCell::new(Buffer::new(0))), 0, None); editor .buffer .borrow_mut() .edit(&[0..0], "abcd\nefgh\nijkl\nmnop"); assert_eq!(render_selections(&editor), vec![empty_selection(0, 0)]); // Adding non-overlapping selections editor.move_right(); editor.move_right(); editor.add_selection(Point::new(0, 0), Point::new(0, 1)); editor.add_selection(Point::new(2, 2), Point::new(2, 3)); editor.add_selection(Point::new(0, 3), Point::new(1, 2)); assert_eq!( render_selections(&editor), vec![ selection((0, 0), (0, 1)), selection((0, 2), (0, 2)), selection((0, 3), (1, 2)), selection((2, 2), (2, 3)), ] ); // Adding a selection that starts at the start of an existing selection editor.add_selection(Point::new(0, 3), Point::new(1, 0)); editor.add_selection(Point::new(0, 3), Point::new(1, 3)); editor.add_selection(Point::new(0, 3), Point::new(1, 2)); assert_eq!( render_selections(&editor), vec![ selection((0, 0), (0, 1)), selection((0, 2), (0, 2)), selection((0, 3), (1, 3)), selection((2, 2), (2, 3)), ] ); // Adding a selection that starts or ends inside an existing selection editor.add_selection(Point::new(0, 1), Point::new(0, 2)); editor.add_selection(Point::new(1, 2), Point::new(1, 4)); editor.add_selection(Point::new(2, 1), Point::new(2, 2)); assert_eq!( render_selections(&editor), vec![ selection((0, 0), (0, 2)), selection((0, 3), (1, 4)), selection((2, 1), (2, 3)), ] ); } #[test] fn test_add_selection_above() { let mut editor = BufferView::new(Rc::new(RefCell::new(Buffer::new(0))), 0, None); editor.buffer.borrow_mut().edit( &[0..0], "\ abcdefghijk\n\ lmnop\n\ \n\ \n\ qrstuvwxyz\n\ ", ); // Multi-line selections editor.move_down(); editor.move_right(); editor.move_right(); editor.select_down(); editor.select_down(); editor.select_down(); editor.select_right(); editor.select_right(); editor.add_selection_above(); assert_eq!(render_selections(&editor), vec![selection((1, 2), (4, 4))]); // Single-line selections editor.move_up(); editor.move_left(); editor.move_left(); editor.add_selection(Point::new(2, 0), Point::new(2, 0)); editor.add_selection(Point::new(4, 1), Point::new(4, 3)); editor.add_selection(Point::new(4, 6), Point::new(4, 6)); editor.add_selection(Point::new(4, 7), Point::new(4, 9)); editor.add_selection_above(); assert_eq!( render_selections(&editor), vec![ selection((0, 0), (0, 0)), selection((0, 7), (0, 9)), selection((1, 0), (1, 0)), selection((1, 1), (1, 3)), selection((1, 5), (1, 5)), selection((2, 0), (2, 0)), selection((4, 1), (4, 3)), selection((4, 6), (4, 6)), selection((4, 7), (4, 9)), ] ); editor.add_selection_above(); assert_eq!( render_selections(&editor), vec![ selection((0, 0), (0, 0)), selection((0, 1), (0, 3)), selection((0, 6), (0, 6)), selection((0, 7), (0, 9)), selection((1, 0), (1, 0)), selection((1, 1), (1, 3)), selection((1, 5), (1, 5)), selection((2, 0), (2, 0)), selection((4, 1), (4, 3)), selection((4, 6), (4, 6)), selection((4, 7), (4, 9)), ] ); } #[test] fn test_add_selection_below() { let mut editor = BufferView::new(Rc::new(RefCell::new(Buffer::new(0))), 0, None); editor.buffer.borrow_mut().edit( &[0..0], "\ abcdefgh\n\ ijklm\n\ \n\ \n\ nopqrstuvwx\n\ yz\ ", ); // Multi-line selections editor.select_down(); editor.select_down(); editor.select_down(); editor.select_down(); editor.select_right(); editor.add_selection_below(); assert_eq!(render_selections(&editor), vec![selection((0, 0), (4, 1))]); // Single-line selections editor.move_left(); editor.add_selection(Point::new(0, 1), Point::new(0, 1)); editor.add_selection(Point::new(0, 4), Point::new(0, 8)); editor.add_selection(Point::new(4, 5), Point::new(4, 6)); editor.add_selection_below(); assert_eq!( render_selections(&editor), vec![ selection((0, 0), (0, 0)), selection((0, 1), (0, 1)), selection((0, 4), (0, 8)), selection((1, 0), (1, 0)), selection((1, 1), (1, 1)), selection((1, 4), (1, 5)), selection((4, 5), (4, 6)), ] ); editor.add_selection_below(); assert_eq!( render_selections(&editor), vec![ selection((0, 0), (0, 0)), selection((0, 1), (0, 1)), selection((0, 4), (0, 8)), selection((1, 0), (1, 0)), selection((1, 1), (1, 1)), selection((1, 4), (1, 5)), selection((2, 0), (2, 0)), selection((4, 1), (4, 1)), selection((4, 4), (4, 8)), ] ); } #[test] fn test_set_cursor_position() { let mut editor = BufferView::new(Rc::new(RefCell::new(Buffer::new(0))), 0, None); editor.buffer.borrow_mut().edit(&[0..0], "abc\ndef\nghi"); editor.add_selection_below(); editor.add_selection_below(); assert_eq!( render_selections(&editor), vec![ empty_selection(0, 0), empty_selection(1, 0), empty_selection(2, 0), ] ); editor.set_cursor_position(Point::new(1, 2), false); assert_eq!(render_selections(&editor), vec![empty_selection(1, 2)]); } #[test] fn test_edit() { let mut editor = BufferView::new(Rc::new(RefCell::new(Buffer::new(0))), 0, None); editor .buffer .borrow_mut() .edit(&[0..0], "abcdefgh\nhijklmno"); // Three selections on the same line editor.select_right(); editor.select_right(); editor.add_selection(Point::new(0, 3), Point::new(0, 5)); editor.add_selection(Point::new(0, 7), Point::new(1, 1)); editor.edit("-"); assert_eq!(editor.buffer.borrow().to_string(), "-c-fg-ijklmno"); assert_eq!( render_selections(&editor), vec![ selection((0, 1), (0, 1)), selection((0, 3), (0, 3)), selection((0, 6), (0, 6)), ] ); let mut editor = BufferView::new(Rc::new(RefCell::new(Buffer::new(0))), 0, None); editor .buffer .borrow_mut() .edit(&[0..0], "123"); editor.edit("ä"); editor.edit("a"); assert_eq!(editor.buffer.borrow().to_string(), "äa123"); assert_eq!( render_selections(&editor), vec![ selection((0, 2), (0, 2)), ] ); } #[test] fn test_autoscroll() { let mut buffer = Buffer::new(0); buffer.edit(&[0..0], "abc\ndef\nghi\njkl\nmno\npqr\nstu\nvwx\nyz"); let start = buffer.anchor_before_offset(0).unwrap(); let end = buffer.anchor_before_offset(buffer.len()).unwrap(); let max_point = buffer.max_point(); let mut editor = BufferView::new(buffer.into_shared(), 0, None); let line_height = 5.0; let height = 3.0 * line_height; editor .set_height(height) .set_line_height(line_height) .set_scroll_top(2.5 * line_height); assert_eq!(editor.scroll_top(), 2.5 * line_height); editor .autoscroll_to_range(start.clone()..start.clone(), true) .unwrap(); assert_eq!(editor.scroll_top(), 0.0); editor .autoscroll_to_range(end.clone()..end.clone(), true) .unwrap(); assert_eq!( editor.scroll_top(), (max_point.row as f64 * line_height) - (height / 2.0) ); } #[test] fn test_render() { let buffer = Rc::new(RefCell::new(Buffer::new(0))); buffer .borrow_mut() .edit(&[0..0], "abc\ndef\nghi\njkl\nmno\npqr\nstu\nvwx\nyz"); let line_height = 6.0; { let mut editor = BufferView::new(buffer.clone(), 0, None); // Selections starting or ending outside viewport editor.add_selection(Point::new(1, 2), Point::new(3, 1)); editor.add_selection(Point::new(5, 2), Point::new(6, 0)); // Selection fully inside viewport editor.add_selection(Point::new(3, 2), Point::new(4, 1)); // Selection fully outside viewport editor.add_selection(Point::new(6, 3), Point::new(7, 2)); editor .set_height(3.0 * line_height) .set_line_height(line_height) .set_scroll_top(2.5 * line_height); let frame = editor.render(); assert_eq!(frame["first_visible_row"], 2); assert_eq!( stringify_lines(&frame["lines"]), vec!["ghi", "jkl", "mno", "pqr"] ); assert_eq!( frame["selections"], json!([ selection((1, 2), (3, 1)), selection((3, 2), (4, 1)), selection((5, 2), (6, 0)), ]) ); } // Selection starting at the end of buffer { let mut editor = BufferView::new(buffer.clone(), 0, None); editor.add_selection(Point::new(8, 2), Point::new(8, 2)); editor .set_height(8.0 * line_height) .set_line_height(line_height) .set_scroll_top(1.0 * line_height); let frame = editor.render(); assert_eq!(frame["first_visible_row"], 1); assert_eq!( stringify_lines(&frame["lines"]), vec!["def", "ghi", "jkl", "mno", "pqr", "stu", "vwx", "yz"] ); assert_eq!(frame["selections"], json!([selection((8, 2), (8, 2))])); } // Selection ending exactly at first visible row { let mut editor = BufferView::new(buffer.clone(), 0, None); editor.add_selection(Point::new(0, 2), Point::new(1, 0)); editor .set_height(3.0 * line_height) .set_line_height(line_height) .set_scroll_top(1.0 * line_height); let frame = editor.render(); assert_eq!(frame["first_visible_row"], 1); assert_eq!(stringify_lines(&frame["lines"]), vec!["def", "ghi", "jkl"]); assert_eq!(frame["selections"], json!([])); } } #[test] fn test_render_past_last_line() { let line_height = 4.0; let mut editor = BufferView::new(Rc::new(RefCell::new(Buffer::new(0))), 0, None); editor.buffer.borrow_mut().edit(&[0..0], "abc\ndef\nghi"); editor.add_selection(Point::new(2, 3), Point::new(2, 3)); editor .set_height(3.0 * line_height) .set_line_height(line_height) .set_scroll_top(2.0 * line_height); let frame = editor.render(); assert_eq!(frame["first_visible_row"], 2); assert_eq!(stringify_lines(&frame["lines"]), vec!["ghi"]); assert_eq!(frame["selections"], json!([selection((2, 3), (2, 3))])); editor.set_scroll_top(3.0 * line_height); let frame = editor.render(); assert_eq!(frame["first_visible_row"], 2); assert_eq!(stringify_lines(&frame["lines"]), vec!["ghi"]); assert_eq!(frame["selections"], json!([selection((2, 3), (2, 3))])); } #[test] fn test_dropping_view_removes_selection_set() { let buffer = Buffer::new(0).into_shared(); let editor = BufferView::new(buffer.clone(), 0, None); let selection_set_id = editor.selection_set_id; assert!(buffer.borrow_mut().selections(selection_set_id).is_ok()); drop(editor); assert!(buffer.borrow_mut().selections(selection_set_id).is_err()); } fn stringify_lines(lines: &serde_json::Value) -> Vec { lines .as_array() .unwrap() .iter() .map(|line| line.as_str().unwrap().into()) .collect() } fn render_selections(editor: &BufferView) -> Vec { let buffer = editor.buffer.borrow(); editor .selections() .iter() .map(|s| SelectionProps { user_id: 0, start: buffer.point_for_anchor(&s.start).unwrap(), end: buffer.point_for_anchor(&s.end).unwrap(), reversed: s.reversed, remote: false, }) .collect() } fn empty_selection(row: u32, column: u32) -> SelectionProps { SelectionProps { user_id: 0, start: Point::new(row, column), end: Point::new(row, column), reversed: false, remote: false, } } fn selection(start: (u32, u32), end: (u32, u32)) -> SelectionProps { SelectionProps { user_id: 0, start: Point::new(start.0, start.1), end: Point::new(end.0, end.1), reversed: false, remote: false, } } fn rev_selection(start: (u32, u32), end: (u32, u32)) -> SelectionProps { SelectionProps { user_id: 0, start: Point::new(start.0, start.1), end: Point::new(end.0, end.1), reversed: true, remote: false, } } } ================================================ FILE: xray_core/src/cross_platform.rs ================================================ use std::borrow::Cow; #[cfg(unix)] use std::ffi::{OsStr, OsString}; #[cfg(unix)] use std::path::PathBuf; pub const UNIX_MAIN_SEPARATOR: u8 = b'/'; #[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize)] pub enum PathComponent { Unix(Vec), } #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] pub struct Path(Option); #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] enum PathState { Unix(Vec), } impl PathComponent { pub fn to_string_lossy(&self) -> Cow { match self { &PathComponent::Unix(ref chars) => String::from_utf8_lossy(&chars), } } } impl Path { pub fn new() -> Self { Path(None) } pub fn push(&mut self, component: &PathComponent) { if let Some(ref mut path) = self.0 { match path { &mut PathState::Unix(ref mut path_chars) => { let &PathComponent::Unix(ref component_chars) = component; if path_chars.len() != 0 { path_chars.push(UNIX_MAIN_SEPARATOR); } path_chars.extend(component_chars); } } } else { match component { &PathComponent::Unix(ref chars) => self.0 = Some(PathState::Unix(chars.clone())), } } } pub fn push_path(&mut self, other: &Self) { if let Some(ref mut path) = self.0 { match path { &mut PathState::Unix(ref mut path_chars) => { if let Some(ref other) = other.0 { let &PathState::Unix(ref component_chars) = other; if path_chars.len() != 0 { path_chars.push(UNIX_MAIN_SEPARATOR); } path_chars.extend(component_chars); } } } } else { *self = other.clone(); } } #[cfg(unix)] pub fn to_path_buf(&self) -> PathBuf { use std::os::unix::ffi::OsStrExt; if let Some(ref path) = self.0 { match path { &PathState::Unix(ref chars) => OsStr::from_bytes(chars).into(), } } else { PathBuf::new() } } #[cfg(test)] pub fn to_string_lossy(&self) -> String { if let Some(ref path) = self.0 { match path { &PathState::Unix(ref chars) => String::from_utf8_lossy(chars).into_owned(), } } else { String::new() } } } #[cfg(unix)] impl From for Path { fn from(path: OsString) -> Self { use std::os::unix::ffi::OsStringExt; Path(Some(PathState::Unix(path.into_vec()))) } } #[cfg(unix)] impl From for PathComponent { fn from(string: OsString) -> Self { use std::os::unix::ffi::OsStringExt; PathComponent::Unix(string.into_vec()) } } #[cfg(unix)] impl<'a> From<&'a OsStr> for PathComponent { fn from(string: &'a OsStr) -> Self { use std::os::unix::ffi::OsStrExt; PathComponent::Unix(string.as_bytes().to_owned()) } } #[cfg(test)] impl<'a> From<&'a str> for PathComponent { #[cfg(unix)] fn from(string: &'a str) -> Self { PathComponent::Unix(string.as_bytes().to_owned()) } } #[cfg(test)] impl<'a> From<&'a str> for Path { #[cfg(unix)] fn from(string: &'a str) -> Self { Path(Some(PathState::Unix(string.as_bytes().to_owned()))) } } ================================================ FILE: xray_core/src/file_finder.rs ================================================ use cross_platform; use futures::{Async, Poll, Stream}; use notify_cell::{NotifyCell, NotifyCellObserver}; use project::{PathSearch, PathSearchResult, PathSearchStatus, TreeId}; use serde_json; use window::{View, WeakViewHandle, Window}; pub trait FileFinderViewDelegate { fn search_paths( &self, needle: &str, max_results: usize, include_ignored: bool, ) -> (PathSearch, NotifyCellObserver); fn did_close(&mut self); fn did_confirm( &mut self, tree_id: TreeId, relative_path: &cross_platform::Path, window: &mut Window, ); } pub struct FileFinderView { delegate: WeakViewHandle, query: String, include_ignored: bool, selected_index: usize, search_results: Vec, search_updates: Option>, updates: NotifyCell<()>, } #[derive(Deserialize)] #[serde(tag = "type")] enum FileFinderAction { UpdateQuery { query: String }, UpdateIncludeIgnored { include_ignored: bool }, SelectPrevious, SelectNext, Confirm, Close, } impl View for FileFinderView { fn component_name(&self) -> &'static str { "FileFinder" } fn render(&self) -> serde_json::Value { json!({ "selected_index": self.selected_index, "query": self.query.as_str(), "results": self.search_results, }) } fn dispatch_action(&mut self, action: serde_json::Value, window: &mut Window) { match serde_json::from_value(action) { Ok(FileFinderAction::UpdateQuery { query }) => self.update_query(query, window), Ok(FileFinderAction::UpdateIncludeIgnored { include_ignored }) => { self.update_include_ignored(include_ignored, window) } Ok(FileFinderAction::SelectPrevious) => self.select_previous(), Ok(FileFinderAction::SelectNext) => self.select_next(), Ok(FileFinderAction::Confirm) => self.confirm(window), Ok(FileFinderAction::Close) => self.close(), _ => eprintln!("Unrecognized action"), } } } impl Stream for FileFinderView { type Item = (); type Error = (); fn poll(&mut self) -> Poll, Self::Error> { let search_poll = self.search_updates .as_mut() .map(|s| s.poll()) .unwrap_or(Ok(Async::NotReady))?; let updates_poll = self.updates.poll()?; match (search_poll, updates_poll) { (Async::NotReady, Async::NotReady) => Ok(Async::NotReady), (Async::Ready(Some(search_status)), _) => { match search_status { PathSearchStatus::Pending => {} PathSearchStatus::Ready(results) => { self.search_results = results; } } Ok(Async::Ready(Some(()))) } _ => Ok(Async::Ready(Some(()))), } } } impl FileFinderView { pub fn new(delegate: WeakViewHandle) -> Self { Self { delegate, query: String::new(), include_ignored: false, selected_index: 0, search_results: Vec::new(), search_updates: None, updates: NotifyCell::new(()), } } fn update_query(&mut self, query: String, window: &mut Window) { if self.query != query { self.query = query; self.search(window); self.updates.set(()); } } fn update_include_ignored(&mut self, include_ignored: bool, window: &mut Window) { if self.include_ignored != include_ignored { self.include_ignored = include_ignored; self.search(window); self.updates.set(()); } } fn select_previous(&mut self) { if self.selected_index > 0 { self.selected_index -= 1; self.updates.set(()); } } fn select_next(&mut self) { if self.selected_index + 1 < self.search_results.len() { self.selected_index += 1; self.updates.set(()); } } fn confirm(&mut self, window: &mut Window) { if let Some(search_result) = self.search_results.get(self.selected_index) { self.delegate.map(|delegate| { delegate.did_confirm(search_result.tree_id, &search_result.relative_path, window) }); } } fn close(&mut self) { self.delegate.map(|delegate| delegate.did_close()); } fn search(&mut self, window: &mut Window) { let search = self.delegate .map(|delegate| delegate.search_paths(&self.query, 10, self.include_ignored)); if let Some((search, search_updates)) = search { self.search_updates = Some(search_updates); window.spawn(search); self.updates.set(()); } } } ================================================ FILE: xray_core/src/fs.rs ================================================ use buffer::BufferSnapshot; use cross_platform; use futures::{Async, Future, Stream}; use notify_cell::NotifyCell; use parking_lot::RwLock; use rpc::{client, server}; use serde::{Deserialize, Deserializer, Serialize, Serializer}; #[cfg(test)] use serde_json; use std::cell::RefCell; use std::io; use std::iter::Iterator; use std::rc::Rc; use std::sync::Arc; use ForegroundExecutor; pub type EntryId = usize; pub type FileId = u64; pub trait Tree { fn root(&self) -> Entry; fn updates(&self) -> Box>; } pub trait LocalTree: Tree { fn path(&self) -> &cross_platform::Path; fn populated(&self) -> Box>; fn as_tree(&self) -> &Tree; } pub trait FileProvider { fn open(&self, path: &cross_platform::Path) -> Box, Error = io::Error>>; } pub trait File { fn id(&self) -> FileId; fn read(&self) -> Box>; fn write_snapshot(&self, snapshot: BufferSnapshot) -> Box>; } #[derive(Clone, Debug, Serialize, Deserialize)] pub enum Entry { #[serde(serialize_with = "serialize_dir", deserialize_with = "deserialize_dir")] Dir(Arc), #[serde(serialize_with = "serialize_file", deserialize_with = "deserialize_file")] File(Arc), } #[derive(Debug, Serialize, Deserialize)] pub struct DirInner { name: cross_platform::PathComponent, #[serde(skip_serializing, skip_deserializing)] name_chars: Vec, #[serde(serialize_with = "serialize_dir_children")] #[serde(deserialize_with = "deserialize_dir_children")] children: RwLock>>, symlink: bool, ignored: bool, } #[derive(Clone, Debug, Serialize, Deserialize)] pub struct FileInner { name: cross_platform::PathComponent, #[serde(skip_serializing, skip_deserializing)] name_chars: Vec, symlink: bool, ignored: bool, } pub struct TreeService { tree: Rc, populated: Option>>, } pub struct RemoteTree(Rc>); struct RemoteTreeState { root: Entry, _service: client::Service, updates: NotifyCell<()>, } impl Entry { pub fn file(name: cross_platform::PathComponent, symlink: bool, ignored: bool) -> Self { Entry::File(Arc::new(FileInner { name_chars: name.to_string_lossy().chars().collect(), name, symlink, ignored, })) } pub fn dir(name: cross_platform::PathComponent, symlink: bool, ignored: bool) -> Self { let mut name_chars: Vec = name.to_string_lossy().chars().collect(); name_chars.push('/'); Entry::Dir(Arc::new(DirInner { name_chars, name, children: RwLock::new(Arc::new(Vec::new())), symlink, ignored, })) } pub fn is_dir(&self) -> bool { match self { &Entry::Dir(_) => true, &Entry::File(_) => false, } } pub fn id(&self) -> EntryId { match self { &Entry::Dir(ref inner) => inner.as_ref() as *const DirInner as EntryId, &Entry::File(ref inner) => inner.as_ref() as *const FileInner as EntryId, } } pub fn name(&self) -> &cross_platform::PathComponent { match self { &Entry::Dir(ref inner) => &inner.name, &Entry::File(ref inner) => &inner.name, } } pub fn name_chars(&self) -> &[char] { match self { &Entry::Dir(ref inner) => &inner.name_chars, &Entry::File(ref inner) => &inner.name_chars, } } pub fn is_symlink(&self) -> bool { match self { &Entry::Dir(ref inner) => inner.symlink, &Entry::File(ref inner) => inner.symlink, } } pub fn is_ignored(&self) -> bool { match self { &Entry::Dir(ref inner) => inner.ignored, &Entry::File(ref inner) => inner.ignored, } } pub fn children(&self) -> Option>> { match self { &Entry::Dir(ref inner) => Some(inner.children.read().clone()), &Entry::File(..) => None, } } pub fn insert(&self, new_entry: Entry) -> Result<(), ()> { match self { &Entry::Dir(ref inner) => { let mut children = inner.children.write(); let children = Arc::make_mut(&mut children); if children .last() .map(|child| child.name() < new_entry.name()) .unwrap_or(true) { children.push(new_entry); Ok(()) } else { let index = { let new_name = new_entry.name(); match children.binary_search_by(|child| child.name().cmp(new_name)) { Ok(_) => return Err(()), // An entry already exists with this name Err(index) => index, } }; children.insert(index, new_entry); Ok(()) } } &Entry::File(_) => Err(()), } } } fn serialize_dir(dir: &Arc, serializer: S) -> Result { dir.serialize(serializer) } fn deserialize_dir<'de, D: Deserializer<'de>>(deserializer: D) -> Result, D::Error> { let mut inner = DirInner::deserialize(deserializer)?; let mut name_chars: Vec = inner.name.to_string_lossy().chars().collect(); name_chars.push('/'); inner.name_chars = name_chars; Ok(Arc::new(inner)) } fn serialize_file(file: &Arc, serializer: S) -> Result { file.serialize(serializer) } fn deserialize_file<'de, D: Deserializer<'de>>( deserializer: D, ) -> Result, D::Error> { let mut inner = FileInner::deserialize(deserializer)?; inner.name_chars = inner.name.to_string_lossy().chars().collect(); Ok(Arc::new(inner)) } fn serialize_dir_children( children: &RwLock>>, serializer: S, ) -> Result { children.read().serialize(serializer) } fn deserialize_dir_children<'de, D: Deserializer<'de>>( deserializer: D, ) -> Result>>, D::Error> { Ok(RwLock::new(Arc::new(Vec::deserialize(deserializer)?))) } impl TreeService { pub fn new(tree: Rc) -> Self { let populated = Some(tree.populated()); Self { tree, populated } } } impl server::Service for TreeService { type State = Entry; type Update = Entry; type Request = (); type Response = (); fn init(&mut self, connection: &server::Connection) -> Self::State { if let Async::Ready(Some(tree)) = self.poll_update(connection) { tree } else { let root = self.tree.root(); Entry::dir(root.name().to_owned(), root.is_symlink(), root.is_ignored()) } } fn poll_update(&mut self, _: &server::Connection) -> Async> { if let Some(populated) = self.populated.as_mut().map(|p| p.poll().unwrap()) { if let Async::Ready(_) = populated { self.populated.take(); Async::Ready(Some(self.tree.root().clone())) } else { Async::NotReady } } else { Async::NotReady } } } impl RemoteTree { pub fn new(foreground: ForegroundExecutor, service: client::Service) -> Self { let updates = service.updates().unwrap(); let state = Rc::new(RefCell::new(RemoteTreeState { root: service.state().unwrap(), _service: service, updates: NotifyCell::new(()), })); let state_clone = state.clone(); foreground .execute(Box::new(updates.for_each(move |root| { let mut state = state_clone.borrow_mut(); state.root = root; state.updates.set(()); Ok(()) }))) .unwrap(); RemoteTree(state) } } impl Tree for RemoteTree { fn root(&self) -> Entry { self.0.borrow().root.clone() } fn updates(&self) -> Box> { Box::new(self.0.borrow().updates.observe()) } } #[cfg(test)] pub(crate) mod tests { use super::*; use bincode::{deserialize, serialize}; use cross_platform::PathComponent; use futures::{future, task, Async, IntoFuture, Poll}; use never::Never; use notify_cell::NotifyCell; use rpc; use std::collections::HashMap; use std::ffi::OsString; use std::path::PathBuf; use stream_ext::StreamExt; use tokio_core::reactor; #[test] fn test_insert() { let root = Entry::dir(PathComponent::from("root"), false, false); assert_eq!( root.insert(Entry::file(PathComponent::from("a"), false, false)), Ok(()) ); assert_eq!( root.insert(Entry::file(PathComponent::from("c"), false, false)), Ok(()) ); assert_eq!( root.insert(Entry::file(PathComponent::from("b"), false, false)), Ok(()) ); assert_eq!( root.insert(Entry::file(PathComponent::from("a"), false, false)), Err(()) ); assert_eq!(root.child_names(), vec!["a", "b", "c"]); } #[test] fn test_serialize_deserialize() { let root = Entry::from_json( "root", &json!({ "child-1": { "subchild-1-1": null }, "child-2": null, "child-3": { "subchild-3-1": { "subchild-3-1-1": null, "subchild-3-1-2": null, } } }), ); assert_eq!( deserialize::(&serialize(&root).unwrap()).unwrap(), root ); } #[test] fn test_tree_replication() { let mut reactor = reactor::Core::new().unwrap(); let handle = Rc::new(reactor.handle()); let local_tree = Rc::new(TestTree::new( "/foo/bar", Entry::from_json( "root", &json!({ "child-1": { "subchild": null }, "child-2": null, }), ), )); let remote_tree = RemoteTree::new( handle, rpc::tests::connect(&mut reactor, TreeService::new(local_tree.clone())), ); assert_eq!(remote_tree.root().name(), local_tree.root().name()); assert_eq!(remote_tree.root().children().unwrap().len(), 0); let mut remote_tree_updates = remote_tree.updates(); local_tree.populated.set(true); remote_tree_updates.wait_next(&mut reactor); assert_eq!(remote_tree.root(), local_tree.root()); } pub struct TestTree { path: cross_platform::Path, root: Entry, pub populated: NotifyCell, } pub struct TestFileProvider(Rc>); struct TestFileProviderState { next_file_id: FileId, files: HashMap, } #[derive(Clone)] struct TestFile(Rc>); struct TestFileState { id: FileId, content: String, } struct NextTick(bool); impl TestTree { pub fn new>(path: T, root: Entry) -> Self { Self { path: cross_platform::Path::from(path.into()), root, populated: NotifyCell::new(false), } } pub fn from_json>(path: T, json: serde_json::Value) -> Self { let path = path.into(); let root = Entry::from_json(PathComponent::from(path.file_name().unwrap()), &json); Self::new(path, root) } } impl Tree for TestTree { fn root(&self) -> Entry { self.root.clone() } fn updates(&self) -> Box> { unimplemented!() } } impl LocalTree for TestTree { fn path(&self) -> &cross_platform::Path { &self.path } fn populated(&self) -> Box> { if self.populated.get() { Box::new(future::ok(())) } else { Box::new( self.populated .observe() .skip_while(|p| Ok(!p)) .into_future() .then(|_| Ok(())), ) } } fn as_tree(&self) -> &Tree { self } } impl Entry { fn from_json>(name: T, json: &serde_json::Value) -> Self { if json.is_object() { let object = json.as_object().unwrap(); let dir = Entry::dir(name.into(), false, false); for (key, value) in object { let child_entry = Self::from_json(key.as_str(), value); assert_eq!(dir.insert(child_entry), Ok(())); } dir } else { Entry::file(name.into(), false, false) } } fn child_names(&self) -> Vec { match self { &Entry::Dir(ref inner) => inner .children .read() .iter() .map(|ref entry| entry.name().to_string_lossy().into_owned()) .collect(), _ => panic!(), } } } impl PartialEq for Entry { fn eq(&self, other: &Self) -> bool { self.name() == other.name() && self.name_chars() == other.name_chars() && self.is_dir() == other.is_dir() && self.is_ignored() == other.is_ignored() && self.children() == other.children() } } impl TestFileProvider { pub fn new() -> Self { TestFileProvider(Rc::new(RefCell::new(TestFileProviderState { next_file_id: 0, files: HashMap::new(), }))) } pub fn write_sync>(&self, path: cross_platform::Path, content: S) { let mut state = self.0.borrow_mut(); let file_id = state.next_file_id; state.next_file_id += 1; state.files.insert( path.to_path_buf(), TestFile(Rc::new(RefCell::new(TestFileState { id: file_id, content: content.into(), }))), ); } } impl FileProvider for TestFileProvider { fn open( &self, path: &cross_platform::Path, ) -> Box, Error = io::Error>> { let path = path.to_path_buf(); let state = self.0.clone(); Box::new(NextTick::new().then(move |_| { let state = state.borrow(); state .files .get(&path) .map(|file| Box::new(file.clone()) as Box) .ok_or(io::Error::new(io::ErrorKind::NotFound, "Path not found")) .into_future() })) } } impl File for TestFile { fn id(&self) -> FileId { self.0.borrow().id } fn read(&self) -> Box> { let file = self.0.clone(); Box::new(NextTick::new().then(move |_| { let file = file.borrow(); future::ok(file.content.clone()) })) } fn write_snapshot( &self, snapshot: BufferSnapshot, ) -> Box> { let file = self.0.clone(); Box::new(NextTick::new().then(move |_| { let mut file = file.borrow_mut(); file.content = snapshot.to_string(); future::ok(()) })) } } impl NextTick { fn new() -> Self { NextTick(false) } } impl Future for NextTick { type Item = (); type Error = Never; fn poll(&mut self) -> Poll { if self.0 { Ok(Async::Ready(())) } else { self.0 = true; task::current().notify(); Ok(Async::NotReady) } } } } ================================================ FILE: xray_core/src/fuzzy.rs ================================================ use std::f64; use std::fmt; use std::ops::{Index, IndexMut}; pub type Score = f64; pub const SCORE_MIN: Score = f64::NEG_INFINITY; const SCORE_GAP_LEADING: Score = -0.005; const SCORE_GAP_TRAILING: Score = -0.005; const SCORE_GAP_INNER: Score = -0.01; const SCORE_MATCH_CONSECUTIVE: Score = 1.0; const SCORE_MATCH_SLASH: Score = 0.9; const SCORE_MATCH_WORD: Score = 0.8; const SCORE_MATCH_CAPITAL: Score = 0.7; const SCORE_MATCH_DOT: Score = 0.6; pub struct Matcher<'a> { needle: &'a [char], stack: Vec, } pub struct Scorer<'a> { needle: &'a [char], d: Matrix, m: Matrix, bonus_cache: Vec, stack: Vec, } struct Matrix { rows: usize, cols: usize, buffer: Vec, } impl<'a> Matcher<'a> { pub fn new(needle: &'a [char]) -> Self { Self { needle, stack: Vec::new(), } } pub fn push(&mut self, component: &[char]) -> bool { if self.needle.is_empty() { true } else { let mut needle_index = self.stack.last().cloned().unwrap_or(0); for ch in component { if self.needle[needle_index].eq_ignore_ascii_case(ch) { needle_index += 1; if needle_index == self.needle.len() { self.stack.push(needle_index); return true; } } } self.stack.push(needle_index); false } } pub fn pop(&mut self) { self.stack.pop(); } } impl<'a> Scorer<'a> { pub fn new(needle: &'a [char]) -> Self { Self { d: Matrix::new(needle.len(), 0), m: Matrix::new(needle.len(), 0), needle, bonus_cache: Vec::new(), stack: Vec::new(), } } pub fn push(&mut self, component: &[char], positions: Option<&mut [usize]>) -> Score { let component_len = component.len(); let haystack_start = self.m.cols; let haystack_len = haystack_start + component_len; let needle_len = self.needle.len(); self.stack.push(component_len); self.precompute_bonus(component); self.d.add_columns(component_len); self.m.add_columns(component_len); for i in 0..needle_len { let mut prev_score = if haystack_start > 0 { self.m[(i, haystack_start - 1)] } else { SCORE_MIN }; let gap_score = if i == needle_len - 1 { SCORE_GAP_TRAILING } else { SCORE_GAP_INNER }; for j in haystack_start..haystack_len { let needle_ch = self.needle[i]; let haystack_ch = component[j - haystack_start]; if needle_ch.eq_ignore_ascii_case(&haystack_ch) { let score; if i == 0 { score = (j as Score * SCORE_GAP_LEADING) + self.bonus_cache[j - haystack_start]; } else if j > 0 { let score_1 = self.m[(i - 1, j - 1)] + self.bonus_cache[j - haystack_start]; let score_2 = self.d[(i - 1, j - 1)] + SCORE_MATCH_CONSECUTIVE; score = score_1.max(score_2); } else { score = SCORE_MIN; } self.d[(i, j)] = score; let best_score = score.max(prev_score + gap_score); self.m[(i, j)] = best_score; prev_score = best_score; } else { self.d[(i, j)] = SCORE_MIN; let best_score = prev_score + gap_score; self.m[(i, j)] = best_score; prev_score = best_score; } } } positions.map(|positions| { let mut match_required = false; let mut j = (haystack_len - 1) as isize; for i in (0..needle_len).rev() { while j >= 0 { if self.d[(i, j)] != SCORE_MIN && (match_required || self.d[(i, j)] == self.m[(i, j)]) { match_required = i > 0 && j > 0 && self.m[(i, j)] == self.d[(i - 1, j - 1)] + SCORE_MATCH_CONSECUTIVE; positions[i] = j as usize; j -= 1; break; } j -= 1; } } }); self.m[(needle_len - 1, haystack_len - 1)] } pub fn pop(&mut self) { let component_len = self.stack.pop().unwrap(); self.d.remove_columns(component_len); self.m.remove_columns(component_len); } fn precompute_bonus(&mut self, component: &[char]) { self.bonus_cache.truncate(0); let mut last_ch = '/'; for ch in component { self.bonus_cache.push(compute_bonus(last_ch, *ch)); last_ch = *ch; } } } impl Matrix { pub fn new(rows: usize, cols: usize) -> Self { Self { rows, cols, buffer: Vec::with_capacity(rows * cols), } } fn add_columns(&mut self, additional: usize) { let prev_len = self.buffer.len(); self.buffer .resize(prev_len + (self.rows * additional), T::default()); self.cols += additional; } fn remove_columns(&mut self, exceeding: usize) { let prev_len = self.buffer.len(); self.buffer.truncate(prev_len - (self.rows * exceeding)); self.cols -= exceeding; } } impl Index<(usize, usize)> for Matrix { type Output = T; fn index(&self, (row, col): (usize, usize)) -> &Self::Output { &self.buffer[(col * self.rows) + row] } } impl Index<(usize, isize)> for Matrix { type Output = T; fn index(&self, (row, col): (usize, isize)) -> &Self::Output { debug_assert!(col >= 0); &self.buffer[(col as usize * self.rows) + row] } } impl IndexMut<(usize, usize)> for Matrix { fn index_mut(&mut self, (row, col): (usize, usize)) -> &mut Self::Output { &mut self.buffer[(col * self.rows) + row] } } impl fmt::Debug for Matrix { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { for row in 0..self.rows { for col in 0..self.cols { write!(f, "{:.2} ", self[(row, col)])?; } writeln!(f)?; } Ok(()) } } #[inline(always)] fn compute_bonus(last_ch: char, ch: char) -> Score { if last_ch as usize > 255 || ch as usize > 255 { 0_f64 } else { BONUS_STATES[BONUS_INDEX[ch as usize] * 256 + last_ch as usize] } } lazy_static! { static ref BONUS_INDEX: [usize; 256] = { let mut table = [0; 256]; for ch in b'A'..b'Z' { table[ch as usize] = 2; } for ch in b'a'..b'z' { table[ch as usize] = 1; } for ch in b'0'..b'9' { table[ch as usize] = 1; } table }; static ref BONUS_STATES: [Score; 3 * 256] = { let mut table = [0_f64; 3 * 256]; table[1 * 256 + b'/' as usize] = SCORE_MATCH_SLASH; table[1 * 256 + b'-' as usize] = SCORE_MATCH_WORD; table[1 * 256 + b'_' as usize] = SCORE_MATCH_WORD; table[1 * 256 + b' ' as usize] = SCORE_MATCH_WORD; table[1 * 256 + b'.' as usize] = SCORE_MATCH_DOT; table[2 * 256 + b'/' as usize] = SCORE_MATCH_SLASH; table[2 * 256 + b'-' as usize] = SCORE_MATCH_WORD; table[2 * 256 + b'_' as usize] = SCORE_MATCH_WORD; table[2 * 256 + b' ' as usize] = SCORE_MATCH_WORD; table[2 * 256 + b'.' as usize] = SCORE_MATCH_DOT; for ch in b'a'..b'z' { table[2 * 256 + ch as usize] = SCORE_MATCH_CAPITAL; } table }; } #[cfg(test)] mod tests { use super::*; #[test] fn test_matcher() { let needle = to_chars("abc"); let mut matcher = Matcher::new(&needle); assert_eq!(matcher.push(&to_chars("abra/")), false); assert_eq!(matcher.push(&to_chars("cadabra")), true); matcher.pop(); assert_eq!(matcher.push(&to_chars("ham/")), false); assert_eq!(matcher.push(&to_chars("lincoln")), true); } #[test] fn test_scorer() { let mut positions = [0; 3].to_vec(); let needle = to_chars("bna"); let mut scorer = Scorer::new(&needle); scorer.push(&to_chars("abc/"), None); scorer.push(&to_chars("bandana/"), None); scorer.push(&to_chars("banana/"), None); scorer.push(&to_chars("foo"), Some(&mut positions)); assert_eq!(positions, &[12, 14, 15]); scorer.pop(); scorer.pop(); scorer.push(&to_chars("bar"), Some(&mut positions)); assert_eq!(positions, &[4, 9, 10]); scorer.pop(); scorer.pop(); scorer.push(&to_chars("ban/"), None); scorer.push(&to_chars("dana"), Some(&mut positions)); assert_eq!(positions, &[4, 10, 11]); } fn to_chars(s: &str) -> Vec { s.chars().collect() } } ================================================ FILE: xray_core/src/lib.rs ================================================ #![feature(unsize, coerce_unsized)] extern crate bincode; extern crate bytes; #[macro_use] extern crate lazy_static; extern crate futures; extern crate parking_lot; extern crate seahash; extern crate serde; #[macro_use] extern crate serde_derive; #[macro_use] extern crate serde_json; extern crate smallvec; #[cfg(test)] extern crate tokio_core; #[cfg(test)] extern crate tokio_timer; #[cfg(target_arch = "wasm32")] extern crate wasm_bindgen; #[cfg(target_arch = "wasm32")] #[macro_use] pub mod wasm_logging; pub mod app; pub mod buffer; pub mod buffer_view; pub mod cross_platform; pub mod fs; pub mod notify_cell; pub mod rpc; pub mod window; pub mod workspace; mod file_finder; mod fuzzy; mod movement; mod never; mod project; #[cfg(test)] mod stream_ext; mod tree; pub use app::{App, WindowId}; use futures::future::{Executor, Future}; pub use never::Never; use std::cell::RefCell; use std::rc::Rc; pub use window::{ViewId, WindowUpdate}; pub type ForegroundExecutor = Rc + 'static>>>; pub type BackgroundExecutor = Rc + Send + 'static>>>; pub type UserId = usize; pub(crate) trait IntoShared { fn into_shared(self) -> Rc>; } impl IntoShared for T { fn into_shared(self) -> Rc> { Rc::new(RefCell::new(self)) } } ================================================ FILE: xray_core/src/movement.rs ================================================ use buffer::{Buffer, Point}; use std::char::decode_utf16; use std::cmp; pub fn left(buffer: &Buffer, mut point: Point) -> Point { if point.column > 0 { point.column -= 1; } else if point.row > 0 { point.row -= 1; point.column = buffer.len_for_row(point.row).unwrap(); } point } pub fn right(buffer: &Buffer, mut point: Point) -> Point { let max_column = buffer.len_for_row(point.row).unwrap(); if point.column < max_column { point.column += 1; } else if point.row < buffer.max_point().row { point.row += 1; point.column = 0; } point } pub fn up(buffer: &Buffer, mut point: Point, goal_column: Option) -> (Point, Option) { let goal_column = goal_column.or(Some(point.column)); if point.row > 0 { point.row -= 1; point.column = cmp::min(goal_column.unwrap(), buffer.len_for_row(point.row).unwrap()); } else { point = Point::new(0, 0); } (point, goal_column) } pub fn down(buffer: &Buffer, mut point: Point, goal_column: Option) -> (Point, Option) { let goal_column = goal_column.or(Some(point.column)); let max_point = buffer.max_point(); if point.row < max_point.row { point.row += 1; point.column = cmp::min(goal_column.unwrap(), buffer.len_for_row(point.row).unwrap()) } else { point = max_point; } (point, goal_column) } pub fn beginning_of_word(buffer: &Buffer, mut point: Point) -> Point { // TODO: remove this once the iterator returns char instances. let mut iter = decode_utf16(buffer.backward_iter_starting_at_point(point)).map(|c| c.unwrap()); let skip_alphanumeric = iter.next().map_or(false, |c| c.is_alphanumeric()); point = left(buffer, point); for character in iter { if skip_alphanumeric == character.is_alphanumeric() { point = left(buffer, point); } else { break; } } point } pub fn end_of_word(buffer: &Buffer, mut point: Point) -> Point { // TODO: remove this once the iterator returns char instances. let mut iter = decode_utf16(buffer.iter_starting_at_point(point)).map(|c| c.unwrap()); let skip_alphanumeric = iter.next().map_or(false, |c| c.is_alphanumeric()); point = right(buffer, point); for character in iter { if skip_alphanumeric == character.is_alphanumeric() { point = right(buffer, point); } else { break; } } point } pub fn beginning_of_line(mut point: Point) -> Point { point.column = 0; point } pub fn end_of_line(buffer: &Buffer, mut point: Point) -> Point { point.column = buffer.len_for_row(point.row).unwrap(); point } ================================================ FILE: xray_core/src/never.rs ================================================ #[derive(Debug, Serialize, Deserialize)] pub enum Never {} ================================================ FILE: xray_core/src/notify_cell.rs ================================================ use std::sync::{Arc, Weak}; use futures::{Async, Poll, Stream}; use futures::task::{self, Task}; use parking_lot::RwLock; type Version = usize; #[derive(Debug, Eq, PartialEq)] pub enum TrySetError { ObserverDisconnected } #[derive(Clone, Debug)] pub struct NotifyCell { observer: Option>, inner: Arc>> } pub struct WeakNotifyCell(Weak>>); #[derive(Clone, Debug)] pub struct NotifyCellObserver { last_polled_at: Version, inner: Arc>>, } #[derive(Debug)] struct Inner { value: T, last_written_at: Version, subscribers: Vec, dropped: bool } impl NotifyCell { pub fn new(value: T) -> Self { NotifyCell { observer: None, inner: Arc::new(RwLock::new(Inner { value, last_written_at: 0, subscribers: Vec::new(), dropped: false })) } } pub fn weak(value: T) -> (WeakNotifyCell, NotifyCellObserver) { let observer = NotifyCellObserver { last_polled_at: 0, inner: Arc::new(RwLock::new(Inner { value, last_written_at: 0, subscribers: Vec::new(), dropped: false })) }; let weak_cell = WeakNotifyCell(Arc::downgrade(&observer.inner)); (weak_cell, observer) } pub fn set(&self, value: T) { let mut inner = self.inner.write(); inner.value = value; inner.last_written_at += 1; for subscriber in inner.subscribers.drain(..) { subscriber.notify(); } } pub fn get(&self) -> T { self.inner.read().value.clone() } pub fn observe(&self) -> NotifyCellObserver { let inner = self.inner.read(); NotifyCellObserver { last_polled_at: inner.last_written_at, inner: self.inner.clone(), } } } impl WeakNotifyCell { pub fn has_observers(&self) -> bool { self.0.upgrade().is_some() } pub fn try_set(&self, value: T) -> Result<(), TrySetError> { let inner = self.0.upgrade().ok_or(TrySetError::ObserverDisconnected)?; let mut inner = inner.write(); inner.value = value; inner.last_written_at += 1; for subscriber in inner.subscribers.drain(..) { subscriber.notify(); } Ok(()) } } impl NotifyCellObserver { pub fn get(&self) -> T { self.inner.read().value.clone() } } impl Stream for NotifyCellObserver { type Item = T; type Error = (); fn poll(&mut self) -> Poll, Self::Error> { let inner = self.inner.upgradable_read(); if inner.dropped { Ok(Async::Ready(None)) } else if self.last_polled_at < inner.last_written_at { self.last_polled_at = inner.last_written_at; Ok(Async::Ready(Some(inner.value.clone()))) } else { inner.upgrade().subscribers.push(task::current()); Ok(Async::NotReady) } } } impl Stream for NotifyCell { type Item = T; type Error = (); fn poll(&mut self) -> Poll, Self::Error> { if self.observer.is_none() { self.observer = Some(self.observe()); let inner = self.inner.read(); if inner.last_written_at == 0 { // Release read lock before polling to avoid deadlocks. drop(inner); self.observer.as_mut().unwrap().poll() } else { Ok(Async::Ready(Some(inner.value.clone()))) } } else { self.observer.as_mut().unwrap().poll() } } } impl Drop for NotifyCell { fn drop(&mut self) { let mut inner = self.inner.write(); inner.dropped = true; for subscriber in inner.subscribers.drain(..) { subscriber.notify(); } } } #[cfg(test)] mod tests { extern crate futures_cpupool; extern crate rand; use super::*; use std::collections::BTreeSet; use futures::Future; use self::rand::Rng; use self::futures_cpupool::CpuPool; #[test] fn test_notify() { let generated_values = rand::thread_rng() .gen_iter::() .take(1000) .collect::>(); let mut generated_values_iter = generated_values.clone().into_iter(); let cell = NotifyCell::new(generated_values_iter.next().unwrap()); let num_threads = 100; let pool = CpuPool::new(num_threads); let cpu_futures = (0..num_threads) .map(|_| pool.spawn(cell.observe().collect())) .collect::>(); for value in generated_values_iter { cell.set(value); } drop(cell); // Dropping the cell terminates the stream. for future in cpu_futures { let observed_values = future.wait().unwrap(); let mut iter = observed_values.iter().peekable(); while let Some(value) = iter.next() { assert!(generated_values.contains(value)); if let Some(next_value) = iter.peek() { assert!(value < next_value); } } } } #[test] fn test_notify_cell_poll() { CpuPool::new_num_cpus().spawn_fn(|| -> Result<(), ()> { let mut cell = NotifyCell::new(1); assert_eq!(cell.poll(), Ok(Async::NotReady)); cell.set(2); assert_eq!(cell.poll(), Ok(Async::Ready(Some(2)))); assert_eq!(cell.poll(), Ok(Async::NotReady)); let mut cell = NotifyCell::new(1); cell.set(2); assert_eq!(cell.poll(), Ok(Async::Ready(Some(2)))); assert_eq!(cell.poll(), Ok(Async::NotReady)); cell.set(3); assert_eq!(cell.poll(), Ok(Async::Ready(Some(3)))); assert_eq!(cell.poll(), Ok(Async::NotReady)); Ok(()) }).wait().unwrap(); } #[test] fn test_weak_notify_cell() { let (cell, observer) = NotifyCell::weak(1); assert_eq!(observer.get(), 1); assert_eq!(cell.try_set(2), Ok(())); assert_eq!(observer.get(), 2); assert_eq!(cell.try_set(3), Ok(())); assert_eq!(observer.get(), 3); drop(observer); assert_eq!(cell.try_set(4), Err(TrySetError::ObserverDisconnected)); } } ================================================ FILE: xray_core/src/project.rs ================================================ use buffer::{self, Buffer, BufferId}; use cross_platform; use fs; use futures::{future, Async, Future, Poll}; use fuzzy; use never::Never; use notify_cell::{NotifyCell, NotifyCellObserver, WeakNotifyCell}; use rpc; use std::cell::{Cell, RefCell}; use std::cmp; use std::collections::{BinaryHeap, HashMap}; use std::error; use std::io; use std::rc::{Rc, Weak}; use std::sync::Arc; use ForegroundExecutor; use IntoShared; pub type TreeId = usize; pub trait Project { fn open_path( &self, tree_id: TreeId, relative_path: &cross_platform::Path, ) -> Box>, Error = Error>>; fn open_buffer( &self, buffer_id: BufferId, ) -> Box>, Error = Error>>; fn search_paths( &self, needle: &str, max_results: usize, include_ignored: bool, ) -> (PathSearch, NotifyCellObserver); } struct BufferWeakSet { buffers: Vec>>, } pub struct LocalProject { file_provider: Rc, next_tree_id: TreeId, next_buffer_id: Rc>, trees: HashMap>, buffers: Rc>, } pub struct RemoteProject { foreground: ForegroundExecutor, service: Rc>>, trees: HashMap>, } pub struct ProjectService { project: Rc>, tree_services: HashMap, } #[derive(Deserialize, Serialize)] pub struct RpcState { trees: HashMap, } #[derive(Deserialize, Serialize)] pub enum RpcRequest { OpenPath { tree_id: TreeId, relative_path: cross_platform::Path, }, OpenBuffer { buffer_id: BufferId, }, } #[derive(Deserialize, Serialize)] pub enum RpcResponse { OpenedBuffer(Result), } pub struct PathSearch { tree_ids: Vec, roots: Arc>, needle: Vec, max_results: usize, include_ignored: bool, stack: Vec, updates: WeakNotifyCell, } #[derive(Clone, Debug, PartialEq)] pub enum PathSearchStatus { Pending, Ready(Vec), } #[derive(Clone, Debug, Serialize, PartialEq)] pub struct PathSearchResult { pub score: fuzzy::Score, pub positions: Vec, pub tree_id: TreeId, pub relative_path: cross_platform::Path, pub display_path: String, } struct StackEntry { children: Arc>, child_index: usize, found_match: bool, } #[derive(Debug)] enum MatchMarker { ContainsMatch, IsMatch, } #[derive(Debug, Serialize, Deserialize)] pub enum Error { BufferNotFound, TreeNotFound, IoError(String), RpcError(rpc::Error), UnexpectedResponse, } impl BufferWeakSet { fn new() -> Self { Self { buffers: Vec::new(), } } fn insert(&mut self, buffer: Buffer) -> Rc> { let buffer = Rc::new(RefCell::new(buffer)); self.buffers.push(Rc::downgrade(&buffer)); buffer } fn find_by_buffer_id(&mut self, buffer_id: BufferId) -> Option>> { let mut found_buffer = None; self.buffers.retain(|buffer| { if let Some(buffer) = buffer.upgrade() { if buffer_id == buffer.borrow().id() { found_buffer = Some(buffer); } true } else { false } }); found_buffer } fn find_by_file_id(&mut self, file_id: fs::FileId) -> Option>> { let mut found_buffer = None; self.buffers.retain(|buffer| { if let Some(buffer) = buffer.upgrade() { if buffer.borrow().file_id().map_or(false, |id| file_id == id) { found_buffer = Some(buffer); } true } else { false } }); found_buffer } } impl LocalProject { pub fn new(file_provider: Rc, trees: Vec) -> Self where T: 'static + fs::LocalTree, { let mut project = LocalProject { file_provider, next_tree_id: 0, next_buffer_id: Rc::new(Cell::new(0)), trees: HashMap::new(), buffers: Rc::new(RefCell::new(BufferWeakSet::new())), }; for tree in trees { project.add_tree(tree); } project } fn add_tree(&mut self, tree: T) { let id = self.next_tree_id; self.next_tree_id += 1; self.trees.insert(id, Rc::new(tree)); } fn resolve_path( &self, tree_id: TreeId, relative_path: &cross_platform::Path, ) -> Option { self.trees.get(&tree_id).map(|tree| { let mut absolute_path = tree.path().clone(); absolute_path.push_path(relative_path); absolute_path }) } } impl Project for LocalProject { fn open_path( &self, tree_id: TreeId, relative_path: &cross_platform::Path, ) -> Box>, Error = Error>> { if let Some(absolute_path) = self.resolve_path(tree_id, relative_path) { let next_buffer_id_cell = self.next_buffer_id.clone(); let buffers = self.buffers.clone(); Box::new( self.file_provider .open(&absolute_path) .and_then(move |file| { let buffer = buffers.borrow_mut().find_by_file_id(file.id()); if let Some(buffer) = buffer { Box::new(future::ok(buffer)) as Box>, Error = io::Error>> } else { Box::new(file.read().and_then(move |content| { let buffer = buffers.borrow_mut().find_by_file_id(file.id()); if let Some(buffer) = buffer { Ok(buffer) } else { let buffer_id = next_buffer_id_cell.get(); next_buffer_id_cell.set(next_buffer_id_cell.get() + 1); let mut buffer = Buffer::new(buffer_id); buffer.edit(&[0..0], content.as_str()); buffer.set_file(file); Ok(buffers.borrow_mut().insert(buffer)) } })) } }) .map_err(|error| error.into()), ) } else { Box::new(future::err(Error::TreeNotFound)) } } fn open_buffer( &self, buffer_id: BufferId, ) -> Box>, Error = Error>> { use futures::IntoFuture; Box::new( self.buffers .borrow_mut() .find_by_buffer_id(buffer_id) .ok_or(Error::BufferNotFound) .into_future(), ) } fn search_paths( &self, needle: &str, max_results: usize, include_ignored: bool, ) -> (PathSearch, NotifyCellObserver) { let (updates, updates_observer) = NotifyCell::weak(PathSearchStatus::Pending); let mut tree_ids = Vec::new(); let mut roots = Vec::new(); for (id, tree) in &self.trees { tree_ids.push(*id); roots.push(tree.root().clone()); } let search = PathSearch { tree_ids, roots: Arc::new(roots), needle: needle.chars().collect(), max_results, include_ignored, stack: Vec::new(), updates, }; (search, updates_observer) } } impl RemoteProject { pub fn new( foreground: ForegroundExecutor, service: rpc::client::Service, ) -> Result { let state = service.state()?; let mut trees = HashMap::new(); for (tree_id, service_id) in &state.trees { let tree_service = service .take_service(*service_id) .expect("The server should create services for each tree in our project state."); let remote_tree = fs::RemoteTree::new(foreground.clone(), tree_service); trees.insert(*tree_id, Box::new(remote_tree) as Box); } Ok(Self { foreground, service: service.into_shared(), trees, }) } } impl Project for RemoteProject { fn open_path( &self, tree_id: TreeId, relative_path: &cross_platform::Path, ) -> Box>, Error = Error>> { let foreground = self.foreground.clone(); let service = self.service.clone(); Box::new( self.service .borrow() .request(RpcRequest::OpenPath { tree_id, relative_path: relative_path.clone(), }) .then(move |response| { response .map_err(|error| error.into()) .and_then(|response| match response { RpcResponse::OpenedBuffer(result) => result.and_then(|service_id| { service .borrow() .take_service(service_id) .map_err(|error| error.into()) .and_then(|buffer_service| { Buffer::remote(foreground, buffer_service) .map_err(|error| error.into()) }) }), }) }), ) } fn open_buffer( &self, buffer_id: BufferId, ) -> Box>, Error = Error>> { let foreground = self.foreground.clone(); let service = self.service.clone(); Box::new( self.service .borrow() .request(RpcRequest::OpenBuffer { buffer_id }) .then(move |response| { response .map_err(|error| error.into()) .and_then(|response| match response { RpcResponse::OpenedBuffer(result) => result.and_then(|service_id| { service .borrow() .take_service(service_id) .map_err(|error| error.into()) .and_then(|buffer_service| { Buffer::remote(foreground, buffer_service) .map_err(|error| error.into()) }) }), }) }), ) } fn search_paths( &self, needle: &str, max_results: usize, include_ignored: bool, ) -> (PathSearch, NotifyCellObserver) { let (updates, updates_observer) = NotifyCell::weak(PathSearchStatus::Pending); let mut tree_ids = Vec::new(); let mut roots = Vec::new(); for (id, tree) in &self.trees { tree_ids.push(*id); roots.push(tree.root().clone()); } let search = PathSearch { tree_ids, roots: Arc::new(roots), needle: needle.chars().collect(), max_results, include_ignored, stack: Vec::new(), updates, }; (search, updates_observer) } } impl ProjectService { pub fn new(project: Rc>) -> Self { Self { project, tree_services: HashMap::new(), } } } impl rpc::server::Service for ProjectService { type State = RpcState; type Update = RpcState; type Request = RpcRequest; type Response = RpcResponse; fn init(&mut self, connection: &rpc::server::Connection) -> Self::State { let mut state = RpcState { trees: HashMap::new(), }; for (tree_id, tree) in &self.project.borrow().trees { let handle = connection.add_service(fs::TreeService::new(tree.clone())); state.trees.insert(*tree_id, handle.service_id()); self.tree_services.insert(*tree_id, handle); } state } fn poll_update( &mut self, _connection: &rpc::server::Connection, ) -> Async> { Async::NotReady } fn request( &mut self, request: Self::Request, connection: &rpc::server::Connection, ) -> Option>> { match request { RpcRequest::OpenPath { tree_id, relative_path, } => { let connection = connection.clone(); Some(Box::new( self.project .borrow() .open_path(tree_id, &relative_path) .then(move |result| { Ok(RpcResponse::OpenedBuffer(result.map(|buffer| { connection .add_service(buffer::rpc::Service::new(buffer)) .service_id() }))) }), )) } RpcRequest::OpenBuffer { buffer_id } => { let connection = connection.clone(); Some(Box::new(self.project.borrow().open_buffer(buffer_id).then( move |result| { Ok(RpcResponse::OpenedBuffer(result.map(|buffer| { connection .add_service(buffer::rpc::Service::new(buffer)) .service_id() }))) }, ))) } } } } impl PathSearch { fn find_matches(&mut self) -> Result, ()> { let mut results = HashMap::new(); let mut matcher = fuzzy::Matcher::new(&self.needle); let mut steps_since_last_check = 0; let mut children = if self.roots.len() == 1 { self.roots[0].children().unwrap() } else { self.roots.clone() }; let mut child_index = 0; let mut found_match = false; loop { self.check_cancellation(&mut steps_since_last_check, 10000)?; let stack = &mut self.stack; if child_index < children.len() { if children[child_index].is_ignored() { child_index += 1; continue; } if matcher.push(&children[child_index].name_chars()) { matcher.pop(); results.insert(children[child_index].id(), MatchMarker::IsMatch); found_match = true; child_index += 1; } else if children[child_index].is_dir() { let next_children = children[child_index].children().unwrap(); stack.push(StackEntry { children: children, child_index, found_match, }); children = next_children; child_index = 0; found_match = false; } else { matcher.pop(); child_index += 1; } } else if stack.len() > 0 { matcher.pop(); let entry = stack.pop().unwrap(); children = entry.children; child_index = entry.child_index; if found_match { results.insert(children[child_index].id(), MatchMarker::ContainsMatch); } else { found_match = entry.found_match; } child_index += 1; } else { break; } } Ok(results) } fn rank_matches( &mut self, matches: HashMap, ) -> Result, ()> { let mut results: BinaryHeap = BinaryHeap::new(); let mut positions = Vec::new(); positions.resize(self.needle.len(), 0); let mut scorer = fuzzy::Scorer::new(&self.needle); let mut steps_since_last_check = 0; let mut children = if self.roots.len() == 1 { self.roots[0].children().unwrap() } else { self.roots.clone() }; let mut child_index = 0; let mut found_match = false; loop { self.check_cancellation(&mut steps_since_last_check, 1000)?; let stack = &mut self.stack; if child_index < children.len() { if children[child_index].is_ignored() && !self.include_ignored { child_index += 1; } else if children[child_index].is_dir() { let descend; let child_is_match; if found_match { child_is_match = true; descend = true; } else { match matches.get(&children[child_index].id()) { Some(&MatchMarker::IsMatch) => { child_is_match = true; descend = true; } Some(&MatchMarker::ContainsMatch) => { child_is_match = false; descend = true; } None => { child_is_match = false; descend = false; } } }; if descend { scorer.push(children[child_index].name_chars(), None); let next_children = children[child_index].children().unwrap(); stack.push(StackEntry { child_index, children, found_match, }); found_match = child_is_match; children = next_children; child_index = 0; } else { child_index += 1; } } else { if found_match || matches.contains_key(&children[child_index].id()) { let score = scorer.push(children[child_index].name_chars(), Some(&mut positions)); scorer.pop(); if results.len() < self.max_results || score > results.peek().map(|r| r.score).unwrap() { let tree_id = if self.roots.len() == 1 { self.tree_ids[0] } else { self.tree_ids[stack[0].child_index] }; let mut relative_path = cross_platform::Path::new(); let mut display_path = String::new(); for (i, entry) in stack.iter().enumerate() { let child = &entry.children[entry.child_index]; if self.roots.len() == 1 || i != 0 { relative_path.push(child.name()); } display_path.extend(child.name_chars()); } let child = &children[child_index]; relative_path.push(child.name()); display_path.extend(child.name_chars()); if results.len() == self.max_results { results.pop(); } results.push(PathSearchResult { score, tree_id, relative_path, display_path, positions: positions.clone(), }); } } child_index += 1; } } else if stack.len() > 0 { scorer.pop(); let entry = stack.pop().unwrap(); children = entry.children; child_index = entry.child_index; found_match = entry.found_match; child_index += 1; } else { break; } } Ok(results.into_sorted_vec()) } #[inline(always)] fn check_cancellation( &self, steps_since_last_check: &mut usize, steps_between_checks: usize, ) -> Result<(), ()> { *steps_since_last_check += 1; if *steps_since_last_check == steps_between_checks { if self.updates.has_observers() { *steps_since_last_check = 0; } else { return Err(()); } } Ok(()) } } impl Future for PathSearch { type Item = (); type Error = (); fn poll(&mut self) -> Poll { if self.needle.is_empty() { let _ = self.updates.try_set(PathSearchStatus::Ready(Vec::new())); } else { let matches = self.find_matches()?; let results = self.rank_matches(matches)?; let _ = self.updates.try_set(PathSearchStatus::Ready(results)); } Ok(Async::Ready(())) } } impl Ord for PathSearchResult { fn cmp(&self, other: &Self) -> cmp::Ordering { self.partial_cmp(other).unwrap_or(cmp::Ordering::Equal) } } impl PartialOrd for PathSearchResult { fn partial_cmp(&self, other: &Self) -> Option { // Reverse the comparison so results with lower scores sort // closer to the top of the results heap. other.score.partial_cmp(&self.score) } } impl Eq for PathSearchResult {} impl From for Error { fn from(error: io::Error) -> Self { Error::IoError(error::Error::description(&error).to_owned()) } } impl From for Error { fn from(error: rpc::Error) -> Self { Error::RpcError(error) } } #[cfg(test)] mod tests { use super::*; use fs::tests::{TestFileProvider, TestTree}; use tokio_core::reactor; use IntoShared; #[test] fn test_open_same_path_concurrently() { let file_provider = Rc::new(TestFileProvider::new()); let project = build_project(file_provider.clone()); let tree_id = 0; let relative_path = cross_platform::Path::from("subdir-a/subdir-1/bar"); file_provider.write_sync( project.resolve_path(tree_id, &relative_path).unwrap(), "abc", ); let buffer_future_1 = project.open_path(tree_id, &relative_path); let buffer_future_2 = project.open_path(tree_id, &relative_path); let (buffer_1, buffer_2) = buffer_future_1.join(buffer_future_2).wait().unwrap(); assert!(Rc::ptr_eq(&buffer_1, &buffer_2)); } #[test] fn test_drop_buffer_rc() { let file_provider = Rc::new(TestFileProvider::new()); let project = build_project(file_provider.clone()); let tree_id = 0; let relative_path = cross_platform::Path::from("subdir-a/subdir-1/bar"); let absolute_path = project.resolve_path(tree_id, &relative_path).unwrap(); file_provider.write_sync(absolute_path, "disk"); let buffer_1 = project.open_path(tree_id, &relative_path).wait().unwrap(); buffer_1.borrow_mut().edit(&[0..4], "memory"); let buffer_2 = project.open_path(tree_id, &relative_path).wait().unwrap(); assert_eq!(buffer_2.borrow().to_string(), "memory"); // Dropping only one of the two strong references does not release the buffer. drop(buffer_2); let buffer_3 = project.open_path(tree_id, &relative_path).wait().unwrap(); assert_eq!(buffer_3.borrow().to_string(), "memory"); // Dropping all strong references causes the buffer to be released. drop(buffer_1); drop(buffer_3); let buffer_4 = project.open_path(tree_id, &relative_path).wait().unwrap(); assert_eq!(buffer_4.borrow().to_string(), "disk"); } #[test] fn test_search_one_tree() { let tree = TestTree::from_json( "/Users/someone/tree", json!({ "root-1": { "file-1": null, "subdir-1": { "file-1": null, "file-2": null, } }, "root-2": { "subdir-2": { "file-3": null, "file-4": null, } } }), ); let project = LocalProject::new(Rc::new(TestFileProvider::new()), vec![tree]); let (mut search, observer) = project.search_paths("sub2", 10, true); assert_eq!(search.poll(), Ok(Async::Ready(()))); assert_eq!( summarize_results(&observer.get()), Some(vec![ ( 0, "root-2/subdir-2/file-3".to_string(), "root-2/subdir-2/file-3".to_string(), vec![7, 8, 9, 14], ), ( 0, "root-2/subdir-2/file-4".to_string(), "root-2/subdir-2/file-4".to_string(), vec![7, 8, 9, 14], ), ( 0, "root-1/subdir-1/file-2".to_string(), "root-1/subdir-1/file-2".to_string(), vec![7, 8, 9, 21], ), ]) ); } #[test] fn test_search_many_trees() { let project = build_project(Rc::new(TestFileProvider::new())); let (mut search, observer) = project.search_paths("bar", 10, true); assert_eq!(search.poll(), Ok(Async::Ready(()))); assert_eq!( summarize_results(&observer.get()), Some(vec![ ( 1, "subdir-b/subdir-2/foo".to_string(), "bar/subdir-b/subdir-2/foo".to_string(), vec![0, 1, 2], ), ( 0, "subdir-a/subdir-1/bar".to_string(), "foo/subdir-a/subdir-1/bar".to_string(), vec![22, 23, 24], ), ( 1, "subdir-b/subdir-2/file-3".to_string(), "bar/subdir-b/subdir-2/file-3".to_string(), vec![0, 1, 2], ), ( 0, "subdir-a/subdir-1/file-1".to_string(), "foo/subdir-a/subdir-1/file-1".to_string(), vec![6, 11, 18], ), ]) ); } #[test] fn test_replication() { let mut reactor = reactor::Core::new().unwrap(); let handle = Rc::new(reactor.handle()); let file_provider = Rc::new(TestFileProvider::new()); let local_project = build_project(file_provider.clone()).into_shared(); let remote_project = RemoteProject::new( handle, rpc::tests::connect(&mut reactor, ProjectService::new(local_project.clone())), ).unwrap(); let (mut local_search, local_observer) = local_project.borrow().search_paths("bar", 10, true); let (mut remote_search, remote_observer) = remote_project.search_paths("bar", 10, true); assert_eq!(local_search.poll(), Ok(Async::Ready(()))); assert_eq!(remote_search.poll(), Ok(Async::Ready(()))); assert_eq!( summarize_results(&remote_observer.get()), summarize_results(&local_observer.get()) ); let PathSearchResult { tree_id, ref relative_path, .. } = remote_observer.get().unwrap()[0]; let absolute_path = local_project .borrow() .resolve_path(tree_id, relative_path) .unwrap(); file_provider.write_sync(absolute_path, "abc"); let remote_buffer = reactor .run(remote_project.open_path(tree_id, &relative_path)) .unwrap(); let local_buffer = reactor .run( local_project .borrow_mut() .open_path(tree_id, &relative_path), ) .unwrap(); assert_eq!( remote_buffer.borrow().to_string(), local_buffer.borrow().to_string() ); } fn build_project(file_provider: Rc) -> LocalProject { let tree_1 = TestTree::from_json( "/Users/someone/foo", json!({ "subdir-a": { "file-1": null, "subdir-1": { "file-1": null, "bar": null, } } }), ); tree_1.populated.set(true); let tree_2 = TestTree::from_json( "/Users/someone/bar", json!({ "subdir-b": { "subdir-2": { "file-3": null, "foo": null, } } }), ); tree_2.populated.set(true); LocalProject::new(file_provider, vec![tree_1, tree_2]) } fn summarize_results( results: &PathSearchStatus, ) -> Option)>> { match results { &PathSearchStatus::Pending => None, &PathSearchStatus::Ready(ref results) => { let summary = results .iter() .map(|result| { let tree_id = result.tree_id; let relative_path = result.relative_path.to_string_lossy(); let display_path = result.display_path.clone(); let positions = result.positions.clone(); (tree_id, relative_path, display_path, positions) }) .collect(); Some(summary) } } } impl PathSearchStatus { fn unwrap(self) -> Vec { match self { PathSearchStatus::Ready(results) => results, _ => panic!(), } } } } ================================================ FILE: xray_core/src/rpc/client.rs ================================================ use super::messages::{MessageToClient, MessageToServer, RequestId, Response, ServiceId}; use super::{server, Error}; use bincode::{deserialize, serialize}; use bytes::Bytes; use futures::{self, future, stream, unsync, Async, Future, Poll, Stream}; use serde::{Deserialize, Serialize}; use std::cell::{Ref, RefCell}; use std::collections::{HashMap, HashSet}; use std::error; use std::io; use std::marker::PhantomData; use std::rc::{Rc, Weak}; pub struct Service { registration: Rc, _marker: PhantomData, } pub struct ServiceRegistration { service_id: ServiceId, connection: Weak>, } pub struct FullUpdateService { latest_state: Rc>>, service: Service, } struct ServiceState { has_client: bool, state: Bytes, updates_rx: Option>, updates_tx: unsync::mpsc::UnboundedSender, pending_requests: HashMap>>, } pub struct Connection(Rc>); struct ConnectionState { next_request_id: RequestId, client_states: HashMap, incoming: Box>, outgoing_tx: unsync::mpsc::UnboundedSender, outgoing_rx: unsync::mpsc::UnboundedReceiver, } impl Service { pub fn state(&self) -> Result { let connection = self.registration.connection()?; let connection = connection.borrow(); let client_state = connection .client_states .get(&self.registration.service_id) .ok_or(Error::ServiceDropped)?; Ok(deserialize(&client_state.state).unwrap()) } pub fn updates(&self) -> Result>, Error> { let connection = self.registration.connection()?; let mut connection = connection.borrow_mut(); let client_state = connection .client_states .get_mut(&self.registration.service_id) .ok_or(Error::ServiceDropped)?; let updates = client_state.updates_rx.take().ok_or(Error::UpdatesTaken)?; let deserialized_updates = updates.map(|update| deserialize(&update).unwrap()); Ok(Box::new(deserialized_updates)) } pub fn request(&self, request: T::Request) -> Box> { fn perform_request( registration: &Rc, request: T::Request, ) -> Result>, Error> { let connection = registration.connection()?; let mut connection = connection.borrow_mut(); let request_id = connection.next_request_id; connection.next_request_id += 1; let (response_tx, response_rx) = unsync::oneshot::channel(); connection .client_states .get_mut(®istration.service_id) .ok_or(Error::ServiceDropped)? .pending_requests .insert(request_id, response_tx); let response_future = response_rx .map_err(|_: futures::Canceled| Error::ServiceDropped) .and_then(|response| response.map(|payload| deserialize(&payload).unwrap())); let request = MessageToServer::Request { request_id, service_id: registration.service_id, payload: serialize(&request).unwrap().into(), }; connection.outgoing_tx.unbounded_send(request).unwrap(); Ok(Box::new(response_future)) } match perform_request::(&self.registration, request) { Ok(future) => future, Err(error) => Box::new(future::err(error)), } } pub fn take_service(&self, id: ServiceId) -> Result, Error> { let connection = self.registration.connection()?; Ok(Connection::service(&connection, id)?) } } // Can't derive Clone because of https://github.com/rust-lang/rust/issues/26925 impl Clone for Service { fn clone(&self) -> Self { Self { registration: self.registration.clone(), _marker: PhantomData, } } } impl FullUpdateService where T: 'static + Serialize + for<'a> Deserialize<'a>, S: server::Service, { pub fn new(service: Service) -> Self { FullUpdateService { latest_state: Rc::new(RefCell::new(service.state())), service, } } pub fn latest_state(&self) -> Result, Error> { let state = self.latest_state.borrow(); if state.is_ok() { Ok(Ref::map(state, |state| state.as_ref().unwrap())) } else { Err(state.as_ref().err().unwrap().clone()) } } pub fn updates(&self) -> Result>, Error> { let latest_state_1 = self.latest_state.clone(); let latest_state_2 = self.latest_state.clone(); self.service.updates().map(|updates| { let update_latest_state = updates.map(move |update| { *latest_state_1.borrow_mut() = Ok(update); }); let clear_latest_state = stream::once(Ok(())).map(move |_| { *latest_state_2.borrow_mut() = Err(Error::ServiceDropped); }); Box::new(update_latest_state.chain(clear_latest_state)) as Box> }) } pub fn request(&self, request: S::Request) -> Box> { self.service.request(request) } pub fn take_service(&self, id: ServiceId) -> Result, Error> { self.service.take_service(id) } } // Can't derive Clone because of https://github.com/rust-lang/rust/issues/26925 impl Clone for FullUpdateService { fn clone(&self) -> Self { Self { latest_state: self.latest_state.clone(), service: self.service.clone(), } } } impl Connection { pub fn new(incoming: S) -> Box), Error = Error>> where S: 'static + Stream, B: 'static + server::Service, { Box::new(incoming.into_future().then(|result| match result { Ok((Some(payload), incoming)) => { let (outgoing_tx, outgoing_rx) = unsync::mpsc::unbounded(); let mut connection = Connection(Rc::new(RefCell::new(ConnectionState { next_request_id: 0, client_states: HashMap::new(), incoming: Box::new(incoming), outgoing_tx, outgoing_rx, }))); let message = deserialize::>(&payload).unwrap()?; connection.update(message); let root_service = Self::service(&connection.0, 0).unwrap(); Ok((connection, root_service)) } Ok((None, _)) => Err(Error::ConnectionDropped), Err((error, _)) => Err(Error::IoError(format!("{}", error))), })) } fn service( connection: &Rc>, id: ServiceId, ) -> Result, Error> { let mut connection_state = connection.borrow_mut(); let service_state = connection_state .client_states .get_mut(&id) .ok_or(Error::ServiceNotFound)?; if service_state.has_client { Err(Error::ServiceTaken) } else { service_state.has_client = true; Ok(Service { registration: Rc::new(ServiceRegistration { service_id: id, connection: Rc::downgrade(connection), }), _marker: PhantomData, }) } } fn update(&mut self, message: MessageToClient) { match message { MessageToClient::Update { insertions, updates, removals, responses, } => { self.process_insertions(insertions); self.process_updates(updates); self.process_removals(removals); self.process_responses(responses); } } } fn process_insertions(&self, insertions: HashMap) { let mut connection = self.0.borrow_mut(); for (id, state) in insertions { let (updates_tx, updates_rx) = unsync::mpsc::unbounded(); connection.client_states.insert( id, ServiceState { has_client: false, state, updates_tx, updates_rx: Some(updates_rx), pending_requests: HashMap::new(), }, ); } } fn process_updates(&self, updates: HashMap>) { let mut connection = self.0.borrow_mut(); for (service_id, updates) in updates { connection .client_states .get_mut(&service_id) .map(|service_state| { for update in updates { let _ = service_state.updates_tx.unbounded_send(update); } }); } } fn process_removals(&self, removals: HashSet) { let mut connection = self.0.borrow_mut(); for id in removals { connection.client_states.remove(&id); } } fn process_responses(&self, responses: HashMap>) { let mut connection = self.0.borrow_mut(); for (service_id, responses) in responses { if let Some(state) = connection.client_states.get_mut(&service_id) { for (request_id, response) in responses { let request_tx = state.pending_requests.remove(&request_id); if let Some(request_tx) = request_tx { match response { Ok(payload) => { request_tx.send(Ok(payload)).unwrap(); } Err(error) => { request_tx.send(Err(error)).unwrap(); } } } else { eprintln!("Received response for unknown request {}", request_id); } } } } } } impl Stream for Connection { type Item = Bytes; type Error = (); fn poll(&mut self) -> Poll, Self::Error> { loop { let incoming_message = self.0.borrow_mut().incoming.poll(); match incoming_message { Ok(Async::Ready(Some(payload))) => { let message: Result = deserialize(&payload).unwrap(); match message { Ok(message) => self.update(message), Err(error) => eprintln!( "Error occurred on server: {}", error::Error::description(&error) ), } } Ok(Async::Ready(None)) => return Ok(Async::Ready(None)), Ok(Async::NotReady) => break, Err(error) => { eprintln!("Error polling incoming connection: {}", error); return Err(()); } } } match self.0.borrow_mut().outgoing_rx.poll() { Ok(Async::Ready(Some(message))) => { return Ok(Async::Ready(Some(serialize(&message).unwrap().into()))) } Ok(Async::Ready(None)) => unreachable!(), Ok(Async::NotReady) => {} Err(_) => { eprintln!("Error polling outgoing messages"); return Err(()); } } Ok(Async::NotReady) } } impl ServiceRegistration { fn connection(&self) -> Result>, Error> { self.connection.upgrade().ok_or(Error::ConnectionDropped) } } impl Drop for ServiceRegistration { fn drop(&mut self) { if let Ok(connection) = self.connection() { let _ = connection .borrow_mut() .outgoing_tx .unbounded_send(MessageToServer::DroppedService(self.service_id)); } } } ================================================ FILE: xray_core/src/rpc/messages.rs ================================================ use super::Error; use bytes::Bytes; use std::collections::{HashMap, HashSet}; pub type RequestId = usize; pub type ServiceId = usize; #[derive(Serialize, Deserialize)] pub enum MessageToClient { Update { insertions: HashMap, updates: HashMap>, removals: HashSet, responses: HashMap>, }, } pub type Response = Result; #[derive(Debug, Serialize, Deserialize)] pub enum MessageToServer { Request { service_id: ServiceId, request_id: RequestId, payload: Bytes, }, DroppedService(ServiceId), } ================================================ FILE: xray_core/src/rpc/mod.rs ================================================ pub mod client; mod messages; pub mod server; pub use self::messages::{Response, ServiceId}; use std::error; use std::fmt; #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] pub enum Error { ConnectionDropped, IoError(String), ServiceDropped, ServiceNotFound, ServiceTaken, UpdatesTaken, } impl fmt::Display for Error { fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { match *self { Error::IoError(ref description) => { write!(fmt, "{}: {}", error::Error::description(self), description) } _ => write!(fmt, "{}", error::Error::description(self)), } } } impl error::Error for Error { fn description(&self) -> &str { match *self { Error::ConnectionDropped => "connection dropped", Error::IoError(_) => "io error", Error::ServiceDropped => "service dropped", Error::ServiceNotFound => "service not found", Error::ServiceTaken => "service taken", Error::UpdatesTaken => "updates taken", } } } #[cfg(test)] pub(crate) mod tests { use super::*; use futures::{future, unsync, Async, Future, Sink, Stream}; use never::Never; use notify_cell::{NotifyCell, NotifyCellObserver}; use std::rc::Rc; use stream_ext::StreamExt; use tokio_core::reactor; #[test] fn test_connection() { let mut reactor = reactor::Core::new().unwrap(); let model = Rc::new(TestModel::new(42)); let client_1 = connect(&mut reactor, TestService::new(model.clone())); assert_eq!(client_1.state(), Ok(42)); model.increment_by(2); let client_2 = connect(&mut reactor, TestService::new(model.clone())); assert_eq!(client_2.state(), Ok(44)); model.increment_by(4); let mut client_1_updates = client_1.updates().unwrap(); assert_eq!(client_1_updates.wait_next(&mut reactor), Some(44)); assert_eq!(client_1_updates.wait_next(&mut reactor), Some(48)); let mut client_2_updates = client_2.updates().unwrap(); assert_eq!(client_2_updates.wait_next(&mut reactor), Some(48)); let request_future = client_2.request(TestRequest::Increment(3)); let response = reactor.run(request_future).unwrap(); assert_eq!(response, TestServiceResponse::Ack); assert_eq!(client_1_updates.wait_next(&mut reactor), Some(51)); assert_eq!(client_2_updates.wait_next(&mut reactor), Some(51)); } #[test] fn test_create_and_drop_service() { let mut reactor = reactor::Core::new().unwrap(); let mut root_model = TestModel::new(42); let child_model = Rc::new(TestModel::new(12)); root_model.child_model = Some(child_model.clone()); let root_model = Rc::new(root_model); let client = connect(&mut reactor, TestService::new(root_model.clone())); assert_eq!(Rc::strong_count(&child_model), 2); let request_future = client.request(TestRequest::CreateService); let response = reactor.run(request_future).unwrap(); assert_eq!(response, TestServiceResponse::ServiceCreated(1)); let child_client = client.take_service::(1).unwrap(); let mut child_updates = child_client.updates().unwrap(); assert_eq!(child_client.state(), Ok(12)); assert!(client.take_service::(1).is_err()); assert_eq!(Rc::strong_count(&child_model), 3); let request_future = client.request(TestRequest::DropService); let response = reactor.run(request_future).unwrap(); assert_eq!(response, TestServiceResponse::Ack); assert!(child_client.state().is_ok()); assert!( reactor .run(child_client.request(TestRequest::Increment(5))) .is_ok() ); assert_eq!(Rc::strong_count(&child_model), 3); drop(child_client); assert_eq!(child_updates.poll(), Ok(Async::Ready(Some(17)))); assert_eq!(Rc::strong_count(&child_model), 3); drop(child_updates); reactor.turn(None); // Send drop message reactor.turn(None); // Receive drop message assert!(client.take_service::(1).is_err()); assert_eq!(Rc::strong_count(&child_model), 2); } #[test] fn test_creating_service_in_async_response() { let mut reactor = reactor::Core::new().unwrap(); let service = TestService::new(Rc::new(TestModel::new(42))); // Throttle connection to ensure both the response *and* the new service are sent together. let client = connect_throttled(&mut reactor, service, Some(100)); let request_future = client.request(TestRequest::CreateServiceAsync); let response = reactor.run(request_future).unwrap(); match response { TestServiceResponse::ServiceCreated(id) => { client .take_service::(id) .expect("Service to exist by the time we receive a response"); } _ => panic!(), } } #[test] fn test_add_service_on_init_or_update() { struct NoopService { init_called: bool, services_to_add_on_init: usize, services_to_add_on_update: usize, child_services: Vec, } impl NoopService { fn new(services_to_add_on_init: usize, services_to_add_on_update: usize) -> Self { Self { init_called: false, services_to_add_on_init, services_to_add_on_update, child_services: Vec::new(), } } } impl server::Service for NoopService { type State = (); type Update = (); type Request = (); type Response = (); fn init(&mut self, connection: &server::Connection) -> Self::State { self.init_called = true; while self.services_to_add_on_init != 0 { self.child_services .push(connection.add_service(NoopService::new(0, 0))); self.services_to_add_on_init -= 1; } () } fn poll_update( &mut self, connection: &server::Connection, ) -> Async> { assert!(self.init_called); while self.services_to_add_on_update != 0 { self.child_services .push(connection.add_service(NoopService::new(0, 0))); self.services_to_add_on_update -= 1; } Async::NotReady } } let mut reactor = reactor::Core::new().unwrap(); let client = connect(&mut reactor, NoopService::new(1, 2)); assert!(client.take_service::(1).is_ok()); assert!(client.take_service::(2).is_ok()); assert!(client.take_service::(3).is_ok()); assert!(client.take_service::(4).is_err()); } #[test] fn test_drop_client_updates() { let mut reactor = reactor::Core::new().unwrap(); let model = Rc::new(TestModel::new(42)); let root_client = connect(&mut reactor, TestService::new(model.clone())); let updates = root_client.updates(); drop(updates); model.increment_by(3); reactor.turn(None); } #[test] fn test_interrupting_connection_to_client() { let (client_to_server_tx, client_to_server_rx) = unsync::mpsc::unbounded(); let client_to_server_rx = client_to_server_rx.map_err(|_| unreachable!()); let model = Rc::new(TestModel::new(42)); let mut server = server::Connection::new(client_to_server_rx, TestService::new(model)); drop(client_to_server_tx); assert_eq!(server.poll(), Ok(Async::Ready(None))); } #[test] fn test_interrupting_connection_to_server_during_handshake() { let mut reactor = reactor::Core::new().unwrap(); let (server_to_client_tx, server_to_client_rx) = unsync::mpsc::unbounded(); let server_to_client_rx = server_to_client_rx.map_err(|_| unreachable!()); drop(server_to_client_tx); let client_future = client::Connection::new::<_, TestService>(server_to_client_rx); assert!(reactor.run(client_future).is_err()); } #[test] fn test_interrupting_connection_to_server_after_handshake() { let mut reactor = reactor::Core::new().unwrap(); let (server_to_client_tx, server_to_client_rx) = unsync::mpsc::unbounded(); let server_to_client_rx = server_to_client_rx.map_err(|_| unreachable!()); let (_client_to_server_tx, client_to_server_rx) = unsync::mpsc::unbounded(); let client_to_server_rx = client_to_server_rx.map_err(|_| unreachable!()); let model = Rc::new(TestModel::new(42)); let server = server::Connection::new(client_to_server_rx, TestService::new(model)); reactor.handle().spawn( server_to_client_tx .send_all(server.map_err(|_| unreachable!())) .then(|_| Ok(())), ); let client_future = client::Connection::new::<_, TestService>(server_to_client_rx); let (mut client, _) = reactor.run(client_future).unwrap(); drop(reactor); assert_eq!(client.poll(), Ok(Async::Ready(None))); } pub fn connect( reactor: &mut reactor::Core, service: S, ) -> client::Service { connect_throttled(reactor, service, None) } fn connect_throttled( reactor: &mut reactor::Core, service: S, delay: Option, ) -> client::Service { let (server_to_client_tx, server_to_client_rx) = unsync::mpsc::unbounded(); let server_to_client_rx = server_to_client_rx.map_err(|_| unreachable!()); let (client_to_server_tx, client_to_server_rx) = unsync::mpsc::unbounded(); let client_to_server_rx = client_to_server_rx.map_err(|_| unreachable!()); let server = if let Some(delay) = delay { server::Connection::new(client_to_server_rx.throttle(delay), service) } else { server::Connection::new(client_to_server_rx, service) }; reactor.handle().spawn( server_to_client_tx .send_all(server.map_err(|_| unreachable!())) .then(|_| Ok(())), ); let client_future = if let Some(delay) = delay { client::Connection::new(server_to_client_rx.throttle(delay)) } else { client::Connection::new(server_to_client_rx) }; let (client, service_client) = reactor.run(client_future).unwrap(); reactor.handle().spawn( client_to_server_tx .send_all(client.map_err(|_| unreachable!())) .then(|_| Ok(())), ); service_client } #[derive(Clone)] struct TestModel { cell: NotifyCell, child_model: Option>, } struct TestService { model: Rc, observer: NotifyCellObserver, child_service: Option, } #[derive(Serialize, Deserialize)] enum TestRequest { Increment(usize), CreateService, DropService, CreateServiceAsync, } #[derive(Debug, PartialEq, Serialize, Deserialize)] enum TestServiceResponse { Ack, ServiceCreated(ServiceId), } impl TestService { fn new(model: Rc) -> Self { let observer = model.cell.observe(); TestService { model, observer, child_service: None, } } } impl server::Service for TestService { type State = usize; type Update = usize; type Request = TestRequest; type Response = TestServiceResponse; fn init(&mut self, _: &server::Connection) -> Self::State { self.model.cell.get() } fn poll_update(&mut self, _: &server::Connection) -> Async> { self.observer.poll().unwrap() } fn request( &mut self, request: Self::Request, connection: &server::Connection, ) -> Option>> { match request { TestRequest::Increment(count) => { self.model.increment_by(count); Some(Box::new(future::ok(TestServiceResponse::Ack))) } TestRequest::CreateService => { let handle = connection.add_service(TestService::new( self.model.child_model.as_ref().unwrap().clone(), )); let service_id = handle.service_id(); self.child_service = Some(handle); Some(Box::new(future::ok(TestServiceResponse::ServiceCreated( service_id, )))) } TestRequest::DropService => { self.child_service.take(); Some(Box::new(future::ok(TestServiceResponse::Ack))) } TestRequest::CreateServiceAsync => { use futures; use std::thread; let (tx, rx) = futures::sync::oneshot::channel(); thread::spawn(|| { tx.send(()).unwrap(); }); let connection = connection.clone(); Some(Box::new(rx.map_err(|_| unreachable!()).and_then( move |_| { let service_handle = connection .add_service(TestService::new(Rc::new(TestModel::new(0)))); Ok(TestServiceResponse::ServiceCreated( service_handle.service_id(), )) }, ))) } } } } impl TestModel { fn new(count: usize) -> Self { TestModel { cell: NotifyCell::new(count), child_model: None, } } fn increment_by(&self, delta: usize) { self.cell.set(self.cell.get() + delta); } } } ================================================ FILE: xray_core/src/rpc/server.rs ================================================ use super::messages::{MessageToClient, MessageToServer, RequestId, Response, ServiceId}; use super::Error; use bincode::{deserialize, serialize}; use bytes::Bytes; use futures::stream::FuturesUnordered; use futures::task::{self, Task}; use futures::{future, Async, Future, Poll, Stream}; use never::Never; use serde::{Deserialize, Serialize}; use std::cell::RefCell; use std::collections::{HashMap, HashSet}; use std::io; use std::mem; use std::rc::{Rc, Weak}; pub trait Service { type State: 'static + Serialize + for<'a> Deserialize<'a>; type Update: 'static + Serialize + for<'a> Deserialize<'a>; type Request: 'static + Serialize + for<'a> Deserialize<'a>; type Response: 'static + Serialize + for<'a> Deserialize<'a>; fn init(&mut self, connection: &Connection) -> Self::State; fn poll_update(&mut self, _connection: &Connection) -> Async> { Async::NotReady } fn request( &mut self, _request: Self::Request, _connection: &Connection, ) -> Option>> { None } } trait RawBytesService { fn init(&mut self, connection: &mut Connection) -> Bytes; fn poll_update(&mut self, connection: &mut Connection) -> Async>; fn request( &mut self, request: Bytes, connection: &mut Connection, ) -> Option>>; } #[derive(Clone)] pub struct Connection(Rc>); struct ConnectionState { next_service_id: ServiceId, services: HashMap>>, client_service_handles: HashMap, inserted: HashSet, removed: HashSet, incoming: Box>, pending_responses: Rc>>>>, pending_task: Option, } struct ServiceRegistration { pub service_id: ServiceId, connection: Weak>, } #[derive(Clone)] pub struct ServiceHandle(Rc); struct ResponseEnvelope { service_id: ServiceId, request_id: RequestId, response: Response, } impl Connection { pub fn new(incoming: S, root_service: T) -> Self where S: 'static + Stream, T: 'static + Service, { let connection = Connection(Rc::new(RefCell::new(ConnectionState { next_service_id: 0, services: HashMap::new(), client_service_handles: HashMap::new(), inserted: HashSet::new(), removed: HashSet::new(), incoming: Box::new(incoming), pending_responses: Rc::new(RefCell::new(FuturesUnordered::new())), pending_task: None, }))); connection.add_service(root_service); connection } pub fn add_service(&self, service: T) -> ServiceHandle { let mut state = self.0.borrow_mut(); let service_id = state.next_service_id; state.next_service_id += 1; let service = Rc::new(RefCell::new(service)); state.services.insert(service_id, service); state.inserted.insert(service_id); let handle = ServiceHandle(Rc::new(ServiceRegistration { connection: Rc::downgrade(&self.0), service_id, })); state .client_service_handles .insert(service_id, handle.clone()); handle } fn poll_incoming(&mut self) -> Result { loop { let poll = self.0.borrow_mut().incoming.poll(); match poll { Ok(Async::Ready(Some(request))) => match deserialize(&request).unwrap() { MessageToServer::Request { request_id, service_id, payload, } => { let pending_responses = self.0.borrow().pending_responses.clone(); if let Some(service) = self.take_service(service_id) { if let Some(response) = service.borrow_mut().request(payload, self) { pending_responses.borrow_mut().push(Box::new(response.map( move |response| ResponseEnvelope { request_id, service_id, response: Ok(response), }, ))); } } else { pending_responses.borrow_mut().push(Box::new(future::ok( ResponseEnvelope { request_id, service_id, response: Err(Error::ServiceNotFound), }, ))); } } MessageToServer::DroppedService(service_id) => { let service_handle = { let mut state = self.0.borrow_mut(); state.client_service_handles.remove(&service_id) }; if service_handle.is_none() { eprintln!("Dropping unknown service with id {}", service_id); } } }, Ok(Async::Ready(None)) => return Ok(false), Ok(Async::NotReady) => return Ok(true), Err(error) => { eprintln!("Error polling incoming connection: {}", error); return Err(error); } } } } fn poll_outgoing(&mut self) -> Poll, ()> { let pending_responses = self.0.borrow_mut().pending_responses.clone(); let mut responses = HashMap::new(); loop { match pending_responses.borrow_mut().poll() { Ok(Async::Ready(Some(envelope))) => { responses .entry(envelope.service_id) .or_insert(Vec::new()) .push((envelope.request_id, envelope.response)); } Ok(Async::Ready(None)) | Ok(Async::NotReady) => break, Err(_) => unreachable!(), } } let existing_service_ids = { let state = self.0.borrow(); state .services .keys() .cloned() .filter(|id| !state.inserted.contains(id)) .collect::>() }; let mut updates: HashMap> = HashMap::new(); for id in existing_service_ids { if let Some(service) = self.take_service(id) { while let Async::Ready(Some(update)) = service.borrow_mut().poll_update(self) { updates.entry(id).or_insert(Vec::new()).push(update); } } } let mut insertions = HashMap::new(); while self.0.borrow().inserted.len() > 0 { let inserted = mem::replace(&mut self.0.borrow_mut().inserted, HashSet::new()); for id in inserted { if let Some(service) = self.take_service(id) { let mut service = service.borrow_mut(); insertions.insert(id, service.init(self)); while let Async::Ready(Some(update)) = service.poll_update(self) { updates.entry(id).or_insert(Vec::new()).push(update); } } } } let mut removals = HashSet::new(); mem::swap(&mut removals, &mut self.0.borrow_mut().removed); if insertions.len() > 0 || updates.len() > 0 || removals.len() > 0 || responses.len() > 0 { let message = serialize::>(&Ok(MessageToClient::Update { insertions, updates, removals, responses, })).unwrap() .into(); Ok(Async::Ready(Some(message))) } else { self.0.borrow_mut().pending_task = Some(task::current()); Ok(Async::NotReady) } } fn take_service(&self, id: ServiceId) -> Option>> { self.0.borrow_mut().services.get(&id).cloned() } } impl Stream for Connection { type Item = Bytes; type Error = (); fn poll(&mut self) -> Poll, Self::Error> { match self.poll_incoming() { Ok(true) => {} Ok(false) => return Ok(Async::Ready(None)), Err(error) => { let message = serialize::>(&Err(Error::IoError( format!("{}", error), ))).unwrap(); return Ok(Async::Ready(Some(message.into()))); } } self.poll_outgoing() } } impl ServiceHandle { pub fn service_id(&self) -> ServiceId { self.0.service_id } } impl Drop for ServiceRegistration { fn drop(&mut self) { if let Some(connection) = self.connection.upgrade() { let mut connection = connection.borrow_mut(); connection.services.remove(&self.service_id); connection.removed.insert(self.service_id); connection.pending_task.as_ref().map(|task| task.notify()); } } } impl RawBytesService for T where T: Service, { fn init(&mut self, connection: &mut Connection) -> Bytes { serialize(&T::init(self, connection)).unwrap().into() } fn poll_update(&mut self, connection: &mut Connection) -> Async> { T::poll_update(self, connection) .map(|option| option.map(|update| serialize(&update).unwrap().into())) } fn request( &mut self, request: Bytes, connection: &mut Connection, ) -> Option>> { T::request(self, deserialize(&request).unwrap(), connection).map(|future| { Box::new(future.map(|item| serialize(&item).unwrap().into())) as Box> }) } } ================================================ FILE: xray_core/src/stream_ext.rs ================================================ use futures::{Future, Poll, Stream}; use std::fmt::Debug; use std::time; use tokio_core::reactor; use tokio_timer::Interval; pub trait StreamExt where Self: Stream + Sized, { fn wait_next(&mut self, reactor: &mut reactor::Core) -> Option where Self::Item: Debug, Self::Error: Debug, { struct TakeOne<'a, S: 'a>(&'a mut S); impl<'a, S: 'a + Stream> Future for TakeOne<'a, S> { type Item = Option; type Error = S::Error; fn poll(&mut self) -> Poll { self.0.poll() } } reactor.run(TakeOne(self)).unwrap() } fn throttle<'a>(self, millis: u64) -> Box<'a + Stream> where Self: 'a, { let delay = time::Duration::from_millis(millis); Box::new(self.zip( Interval::new(time::Instant::now() + delay, delay).map_err(|_| unreachable!()), ).and_then(|(item, _)| Ok(item))) } } impl StreamExt for T {} ================================================ FILE: xray_core/src/tree.rs ================================================ use std::clone::Clone; use std::fmt; use std::ops::{Add, AddAssign, Range}; use std::sync::Arc; const MIN_CHILDREN: usize = 2; const MAX_CHILDREN: usize = 4; pub trait Item: Clone + Eq + fmt::Debug { type Summary: for<'a> AddAssign<&'a Self::Summary> + Default + Eq + Clone + fmt::Debug; fn summarize(&self) -> Self::Summary; } pub trait Dimension: for<'a> Add<&'a Self, Output = Self> + Ord + Clone + fmt::Debug { type Summary: Default + Eq + Clone + fmt::Debug; fn from_summary(summary: &Self::Summary) -> Self; fn default() -> Self { Self::from_summary(&Self::Summary::default()) } } #[derive(Clone, Eq, PartialEq, Debug)] pub struct Tree(Arc>); #[derive(Clone, Eq, PartialEq, Debug)] pub enum Node { Internal { rightmost_leaf: Option>, summary: T::Summary, children: Vec>, height: u16, }, Leaf { summary: T::Summary, value: T, }, } pub struct Iter<'a, T: 'a + Item> { tree: &'a Tree, did_start: bool, stack: Vec<(&'a Tree, usize)>, } #[derive(Debug)] pub struct Cursor<'a, T: 'a + Item> { tree: &'a Tree, did_seek: bool, stack: Vec<(&'a Tree, usize, T::Summary)>, prev_leaf: Option<&'a Tree>, summary: T::Summary, } #[derive(Clone, Copy, Eq, PartialEq, Debug)] pub enum SeekBias { Left, Right, } impl Extend for Tree { fn extend>(&mut self, items: I) { for item in items.into_iter() { self.push(item); } } } impl<'a, T: Item> Tree { pub fn new() -> Self { Self::from_children(vec![]) } pub fn from_item(item: T) -> Self { let mut tree = Self::new(); tree.push(item); tree } fn from_children(children: Vec) -> Self { let summary = Self::summarize_children(&children); let rightmost_leaf = children .last() .and_then(|last_child| last_child.rightmost_leaf().cloned()); let height = children.get(0).map(|c| c.height()).unwrap_or(0) + 1; Tree(Arc::new(Node::Internal { rightmost_leaf, summary, children, height, })) } fn summarize_children(children: &[Tree]) -> T::Summary { let mut summary = T::Summary::default(); for ref child in children { summary += child.summary(); } summary } pub fn iter(&self) -> Iter { Iter::new(self) } pub fn cursor(&self) -> Cursor { Cursor::new(self) } pub fn len>(&self) -> D { D::from_summary(self.summary()) } pub fn last(&self) -> Option<&T> { self.rightmost_leaf().map(|leaf| leaf.value()) } pub fn push(&mut self, item: T) { self.push_tree(Tree(Arc::new(Node::Leaf { summary: item.summarize(), value: item, }))) } pub fn push_tree(&mut self, other: Self) { if other.is_empty() { return; } let self_height = self.height(); let other_height = other.height(); // Other is a taller tree, push its children one at a time if self_height < other_height { for other_child in other.children().iter().cloned() { self.push_tree(other_child); } return; } // Self is an internal node. Pushing other could cause the root to split. if let Some(split) = self.push_recursive(other) { *self = Self::from_children(vec![self.clone(), split]) } } fn push_recursive(&mut self, other: Tree) -> Option> { *self.summary_mut() += other.summary(); *self.rightmost_leaf_mut() = other.rightmost_leaf().cloned(); let self_height = self.height(); let other_height = other.height(); if other_height == self_height { self.append_children(other.children()) } else if other_height == self_height - 1 && !other.underflowing() { self.append_children(&[other]) } else { if let Some(split) = self.last_child_mut().push_recursive(other) { self.append_children(&[split]) } else { None } } } fn append_children(&mut self, new_children: &[Tree]) -> Option> { match Arc::make_mut(&mut self.0) { &mut Node::Internal { ref mut children, ref mut summary, ref mut rightmost_leaf, .. } => { let child_count = children.len() + new_children.len(); if child_count > MAX_CHILDREN { let midpoint = (child_count + child_count % 2) / 2; let (left_children, right_children): ( Vec>, Vec>, ) = { let mut all_children = children.iter().chain(new_children.iter()).cloned(); ( all_children.by_ref().take(midpoint).collect(), all_children.collect(), ) }; *children = left_children; *summary = Self::summarize_children(children); *rightmost_leaf = children.last().unwrap().rightmost_leaf().cloned(); Some(Tree::from_children(right_children)) } else { children.extend(new_children.iter().cloned()); None } } &mut Node::Leaf { .. } => panic!("Tried to append children to a leaf node"), } } #[allow(dead_code)] pub fn splice, I: IntoIterator>( &mut self, old_range: Range<&D>, new_items: I, ) { let mut result = Self::new(); self.append_subsequence(&mut result, &D::default(), old_range.start); result.extend(new_items); self.append_subsequence(&mut result, old_range.end, &D::from_summary(self.summary())); *self = result; } fn append_subsequence>( &self, result: &mut Self, start: &D, end: &D, ) { self.append_subsequence_recursive(result, D::default(), start, end); } fn append_subsequence_recursive>( &self, result: &mut Self, node_start: D, start: &D, end: &D, ) { match self.0.as_ref() { &Node::Internal { ref summary, ref children, .. } => { let node_end = node_start.clone() + &D::from_summary(summary); if *start <= node_start && node_end <= *end { result.push_tree(self.clone()); } else if node_start < *end || *start < node_end { let mut child_start = node_start.clone(); for ref child in children { child.append_subsequence_recursive(result, child_start.clone(), start, end); child_start = child_start + &D::from_summary(child.summary()); } } } &Node::Leaf { .. } => { if *start <= node_start && node_start < *end { result.push_tree(self.clone()); } } } } fn rightmost_leaf(&self) -> Option<&Tree> { match self.0.as_ref() { &Node::Internal { ref rightmost_leaf, .. } => rightmost_leaf.as_ref(), &Node::Leaf { .. } => Some(self), } } fn rightmost_leaf_mut(&mut self) -> &mut Option> { match Arc::make_mut(&mut self.0) { &mut Node::Internal { ref mut rightmost_leaf, .. } => rightmost_leaf, _ => { panic!("Requested a mutable reference to the rightmost leaf of a non-internal node") } } } pub fn summary(&self) -> &T::Summary { match self.0.as_ref() { &Node::Internal { ref summary, .. } => summary, &Node::Leaf { ref summary, .. } => summary, } } fn summary_mut(&mut self) -> &mut T::Summary { match Arc::make_mut(&mut self.0) { &mut Node::Internal { ref mut summary, .. } => summary, &mut Node::Leaf { ref mut summary, .. } => summary, } } fn children(&self) -> &[Tree] { match self.0.as_ref() { &Node::Internal { ref children, .. } => children.as_slice(), &Node::Leaf { .. } => panic!("Requested children of a leaf node"), } } fn last_child_mut(&mut self) -> &mut Tree { match Arc::make_mut(&mut self.0) { &mut Node::Internal { ref mut children, .. } => children.last_mut().unwrap(), &mut Node::Leaf { .. } => panic!("Requested last child of a leaf node"), } } fn value(&self) -> &T { match self.0.as_ref() { &Node::Internal { .. } => panic!("Requested value of an internal node"), &Node::Leaf { ref value, .. } => value, } } fn underflowing(&self) -> bool { match self.0.as_ref() { &Node::Internal { ref children, .. } => children.len() < MIN_CHILDREN, &Node::Leaf { .. } => false, } } fn is_empty(&self) -> bool { match self.0.as_ref() { &Node::Internal { ref children, .. } => children.len() == 0, &Node::Leaf { .. } => false, } } fn height(&self) -> u16 { match self.0.as_ref() { &Node::Internal { height, .. } => height, &Node::Leaf { .. } => 0, } } } impl<'a, T: 'a + Item> Iter<'a, T> { fn new(tree: &'a Tree) -> Self { Iter { tree, did_start: false, stack: Vec::with_capacity(tree.height() as usize), } } fn seek_to_first_item(&mut self, mut tree: &'a Tree) -> Option<&'a T> { if tree.is_empty() { None } else { loop { match tree.0.as_ref() { &Node::Internal { ref children, .. } => { self.stack.push((tree, 0)); tree = &children[0]; } &Node::Leaf { ref value, .. } => return Some(value), } } } } } impl<'a, T: 'a + Item> Iterator for Iter<'a, T> where Self: 'a, { type Item = &'a T; fn next(&mut self) -> Option { if self.did_start { while self.stack.len() > 0 { let (tree, index) = { let &mut (tree, ref mut index) = self.stack.last_mut().unwrap(); *index += 1; (tree, *index) }; if let Some(child) = tree.children().get(index) { return self.seek_to_first_item(child); } else { self.stack.pop(); } } None } else { self.did_start = true; self.seek_to_first_item(self.tree) } } } impl<'tree, T: 'tree + Item> Cursor<'tree, T> { fn new(tree: &'tree Tree) -> Self { Self { tree, did_seek: false, stack: Vec::with_capacity(tree.height() as usize), prev_leaf: None, summary: T::Summary::default(), } } fn reset(&mut self) { self.did_seek = false; self.stack.truncate(0); self.prev_leaf = None; self.summary = T::Summary::default(); } pub fn start>(&self) -> D { D::from_summary(&self.summary) } pub fn item<'a>(&'a self) -> Option<&'tree T> { self.cur_leaf().map(|leaf| leaf.value()) } pub fn prev_item<'a>(&'a self) -> Option<&'tree T> { self.prev_leaf.map(|leaf| leaf.value()) } fn cur_leaf<'a>(&'a self) -> Option<&'tree Tree> { assert!(self.did_seek, "Must seek before reading cursor position"); self.stack .last() .map(|&(subtree, index, _)| &subtree.children()[index]) } pub fn next(&mut self) { assert!(self.did_seek, "Must seek before calling next"); while self.stack.len() > 0 { let (prev_subtree, index) = { let &mut (prev_subtree, ref mut index, _) = self.stack.last_mut().unwrap(); if prev_subtree.height() == 1 { let prev_leaf = &prev_subtree.children()[*index]; self.prev_leaf = Some(prev_leaf); self.summary += prev_leaf.summary(); } *index += 1; (prev_subtree, *index) }; if let Some(child) = prev_subtree.children().get(index) { self.seek_to_first_item(child); break; } else { self.stack.pop(); } } } pub fn prev(&mut self) { assert!(self.did_seek, "Must seek before calling prev"); if self.stack.is_empty() && self.prev_leaf.is_some() { self.summary = T::Summary::default(); self.seek_to_last_item(self.tree); } else { while self.stack.len() > 0 { let subtree = { let (parent, index, summary) = self.stack.last_mut().unwrap(); if *index == 0 { None } else { *index -= 1; self.summary = summary.clone(); for child in &parent.children()[0..*index] { self.summary += child.summary(); } parent.children().get(*index) } }; if let Some(subtree) = subtree { self.seek_to_last_item(subtree); break; } else { self.stack.pop(); } } } self.prev_leaf = if self.stack.is_empty() { None } else { let mut stack_index = self.stack.len() - 1; loop { let (ancestor, index, _) = &self.stack[stack_index]; if *index == 0 { if stack_index == 0 { break None; } else { stack_index -= 1; } } else { break ancestor.children()[index - 1].rightmost_leaf(); } } }; } fn seek_to_first_item<'a>(&'a mut self, mut tree: &'tree Tree) { self.did_seek = true; loop { match tree.0.as_ref() { &Node::Internal { ref children, .. } => { self.stack.push((tree, 0, self.summary.clone())); tree = &children[0]; } &Node::Leaf { .. } => { break; } } } } fn seek_to_last_item<'a>(&'a mut self, mut tree: &'tree Tree) { self.did_seek = true; loop { match tree.0.as_ref() { &Node::Internal { ref children, .. } => { self.stack .push((tree, children.len() - 1, self.summary.clone())); for child in &tree.children()[0..children.len() - 1] { self.summary += child.summary(); } tree = children.last().unwrap(); } &Node::Leaf { .. } => { break; } } } } pub fn seek>(&mut self, pos: &D, bias: SeekBias) { self.reset(); self.seek_and_slice(pos, bias, None); } pub fn slice>( &mut self, end: &D, bias: SeekBias, ) -> Tree { let mut prefix = Tree::new(); self.seek_and_slice(end, bias, Some(&mut prefix)); prefix } fn seek_and_slice>( &mut self, pos: &D, bias: SeekBias, mut slice: Option<&mut Tree>, ) { let mut cur_subtree = None; if self.did_seek { debug_assert!(*pos >= D::from_summary(&self.summary)); while self.stack.len() > 0 { { let &mut (prev_subtree, ref mut index, _) = self.stack.last_mut().unwrap(); if prev_subtree.height() > 1 { *index += 1; } let children_len = prev_subtree.children().len(); while *index < children_len { let subtree = &prev_subtree.children()[*index]; let summary = subtree.summary(); let subtree_end = D::from_summary(&self.summary) + &D::from_summary(summary); if *pos > subtree_end || (*pos == subtree_end && bias == SeekBias::Right) { self.summary += summary; self.prev_leaf = subtree.rightmost_leaf(); slice.as_mut().map(|slice| slice.push_tree(subtree.clone())); *index += 1; } else { cur_subtree = Some(subtree); break; } } } if cur_subtree.is_some() { break; } else { self.stack.pop(); } } } else { self.reset(); self.did_seek = true; cur_subtree = Some(self.tree); } while let Some(subtree) = cur_subtree.take() { match subtree.0.as_ref() { &Node::Internal { ref rightmost_leaf, ref summary, ref children, .. } => { let subtree_end = D::from_summary(&self.summary) + &D::from_summary(summary); if *pos > subtree_end || (*pos == subtree_end && bias == SeekBias::Right) { self.summary += summary; self.prev_leaf = rightmost_leaf.as_ref(); slice.as_mut().map(|slice| slice.push_tree(subtree.clone())); } else { for (index, child) in children.iter().enumerate() { let child_end = D::from_summary(&self.summary) + &D::from_summary(child.summary()); if *pos > child_end || (*pos == child_end && bias == SeekBias::Right) { self.summary += child.summary(); self.prev_leaf = child.rightmost_leaf(); slice.as_mut().map(|slice| slice.push_tree(child.clone())); } else { self.stack.push((subtree, index, self.summary.clone())); cur_subtree = Some(child); break; } } } } &Node::Leaf { ref summary, .. } => { // TODO? Can we push the child unconditionally? let subtree_end = D::from_summary(&self.summary) + &D::from_summary(summary); if *pos > subtree_end || (*pos == subtree_end && bias == SeekBias::Right) { self.prev_leaf = Some(subtree); self.summary += summary; slice.as_mut().map(|slice| slice.push_tree(subtree.clone())); } } } } } } #[cfg(test)] mod tests { extern crate rand; use super::*; #[derive(Default, Eq, PartialEq, Clone, Debug)] pub struct IntegersSummary { count: usize, sum: usize, } #[derive(Ord, PartialOrd, Default, Eq, PartialEq, Clone, Debug)] struct Count(usize); #[derive(Ord, PartialOrd, Default, Eq, PartialEq, Clone, Debug)] struct Sum(usize); impl Item for u16 { type Summary = IntegersSummary; fn summarize(&self) -> Self::Summary { IntegersSummary { count: 1, sum: *self as usize, } } } impl<'a> AddAssign<&'a Self> for IntegersSummary { fn add_assign(&mut self, other: &Self) { self.count += other.count; self.sum += other.sum; } } impl Dimension for Count { type Summary = IntegersSummary; fn from_summary(summary: &Self::Summary) -> Self { Count(summary.count) } } impl<'a> Add<&'a Self> for Count { type Output = Self; fn add(mut self, other: &Self) -> Self { self.0 += other.0; self } } impl Dimension for Sum { type Summary = IntegersSummary; fn from_summary(summary: &Self::Summary) -> Self { Sum(summary.sum) } } impl<'a> Add<&'a Self> for Sum { type Output = Self; fn add(mut self, other: &Self) -> Self { self.0 += other.0; self } } impl Tree { fn items(&self) -> Vec { self.iter().cloned().collect() } } #[test] fn test_extend_and_push() { let mut tree1 = Tree::new(); tree1.extend(1..20); let mut tree2 = Tree::new(); tree2.extend(1..50); tree1.push_tree(tree2); assert_eq!(tree1.items(), (1..20).chain(1..50).collect::>()); } #[test] fn splice() { let mut tree = Tree::new(); tree.extend(0..10); tree.splice(&Count(2)..&Count(8), 20..23); assert_eq!(tree.items(), vec![0, 1, 20, 21, 22, 8, 9]); } #[test] fn random() { for seed in 0..100 { use self::rand::{Rng, SeedableRng, StdRng}; let mut rng = StdRng::from_seed(&[seed]); let mut tree = Tree::::new(); let count = rng.gen_range(0, 10); tree.extend(rng.gen_iter().take(count)); for _i in 0..100 { let end = rng.gen_range(0, tree.len::().0 + 1); let start = rng.gen_range(0, end + 1); let count = rng.gen_range(0, 3); let new_items = rng.gen_iter().take(count).collect::>(); let mut reference_items = tree.items(); tree.splice(&Count(start)..&Count(end), new_items.clone()); reference_items.splice(start..end, new_items); assert_eq!(tree.items(), reference_items); let mut cursor = tree.cursor(); let suffix_start = rng.gen_range(0, tree.len::().0 + 1); let prefix_end = rng.gen_range(0, suffix_start + 1); let prefix_items = cursor.slice(&Count(prefix_end), SeekBias::Right).items(); assert_eq!(prefix_items, reference_items[0..prefix_end].to_vec()); // Scan to the start of the suffix if we aren't already there if suffix_start > prefix_end { for i in prefix_end..suffix_start { assert_eq!(cursor.item(), reference_items.get(i)); assert_eq!( cursor.prev_item(), if i > 0 { reference_items.get(i - 1) } else { None } ); assert_eq!(cursor.start::(), Count(i)); cursor.next(); } } let suffix_items = cursor.slice(&tree.len::(), SeekBias::Right).items(); assert_eq!(suffix_items, reference_items[suffix_start..].to_vec()); } } } #[test] fn cursor() { // Empty tree let tree = Tree::::new(); let mut cursor = tree.cursor(); assert_eq!(cursor.slice(&Sum(0), SeekBias::Right), Tree::new()); assert_eq!(cursor.item(), None); assert_eq!(cursor.prev_item(), None); assert_eq!(cursor.start::(), Count(0)); assert_eq!(cursor.start::(), Sum(0)); // Single-element tree let mut tree = Tree::::new(); tree.extend(vec![1]); let mut cursor = tree.cursor(); assert_eq!(cursor.slice(&Sum(0), SeekBias::Right), Tree::new()); assert_eq!(cursor.item(), Some(&1)); assert_eq!(cursor.prev_item(), None); assert_eq!(cursor.start::(), Count(0)); assert_eq!(cursor.start::(), Sum(0)); cursor.next(); assert_eq!(cursor.item(), None); assert_eq!(cursor.prev_item(), Some(&1)); assert_eq!(cursor.start::(), Count(1)); assert_eq!(cursor.start::(), Sum(1)); cursor.prev(); assert_eq!(cursor.item(), Some(&1)); assert_eq!(cursor.prev_item(), None); assert_eq!(cursor.start::(), Count(0)); assert_eq!(cursor.start::(), Sum(0)); cursor.reset(); assert_eq!(cursor.slice(&Sum(1), SeekBias::Right).items(), [1]); assert_eq!(cursor.item(), None); assert_eq!(cursor.prev_item(), Some(&1)); assert_eq!(cursor.start::(), Count(1)); assert_eq!(cursor.start::(), Sum(1)); cursor.seek(&Sum(0), SeekBias::Right); assert_eq!( cursor.slice(&tree.len::(), SeekBias::Right).items(), [1] ); assert_eq!(cursor.item(), None); assert_eq!(cursor.prev_item(), Some(&1)); assert_eq!(cursor.start::(), Count(1)); assert_eq!(cursor.start::(), Sum(1)); // Multiple-element tree let mut tree = Tree::new(); tree.extend(vec![1, 2, 3, 4, 5, 6]); let mut cursor = tree.cursor(); assert_eq!(cursor.slice(&Sum(4), SeekBias::Right).items(), [1, 2]); assert_eq!(cursor.item(), Some(&3)); assert_eq!(cursor.prev_item(), Some(&2)); assert_eq!(cursor.start::(), Count(2)); assert_eq!(cursor.start::(), Sum(3)); cursor.next(); assert_eq!(cursor.item(), Some(&4)); assert_eq!(cursor.prev_item(), Some(&3)); assert_eq!(cursor.start::(), Count(3)); assert_eq!(cursor.start::(), Sum(6)); cursor.next(); assert_eq!(cursor.item(), Some(&5)); assert_eq!(cursor.prev_item(), Some(&4)); assert_eq!(cursor.start::(), Count(4)); assert_eq!(cursor.start::(), Sum(10)); cursor.next(); assert_eq!(cursor.item(), Some(&6)); assert_eq!(cursor.prev_item(), Some(&5)); assert_eq!(cursor.start::(), Count(5)); assert_eq!(cursor.start::(), Sum(15)); cursor.next(); cursor.next(); assert_eq!(cursor.item(), None); assert_eq!(cursor.prev_item(), Some(&6)); assert_eq!(cursor.start::(), Count(6)); assert_eq!(cursor.start::(), Sum(21)); cursor.prev(); assert_eq!(cursor.item(), Some(&6)); assert_eq!(cursor.prev_item(), Some(&5)); assert_eq!(cursor.start::(), Count(5)); assert_eq!(cursor.start::(), Sum(15)); cursor.prev(); assert_eq!(cursor.item(), Some(&5)); assert_eq!(cursor.prev_item(), Some(&4)); assert_eq!(cursor.start::(), Count(4)); assert_eq!(cursor.start::(), Sum(10)); cursor.prev(); assert_eq!(cursor.item(), Some(&4)); assert_eq!(cursor.prev_item(), Some(&3)); assert_eq!(cursor.start::(), Count(3)); assert_eq!(cursor.start::(), Sum(6)); cursor.prev(); assert_eq!(cursor.item(), Some(&3)); assert_eq!(cursor.prev_item(), Some(&2)); assert_eq!(cursor.start::(), Count(2)); assert_eq!(cursor.start::(), Sum(3)); cursor.prev(); assert_eq!(cursor.item(), Some(&2)); assert_eq!(cursor.prev_item(), Some(&1)); assert_eq!(cursor.start::(), Count(1)); assert_eq!(cursor.start::(), Sum(1)); cursor.prev(); assert_eq!(cursor.item(), Some(&1)); assert_eq!(cursor.prev_item(), None); assert_eq!(cursor.start::(), Count(0)); assert_eq!(cursor.start::(), Sum(0)); cursor.prev(); assert_eq!(cursor.item(), None); assert_eq!(cursor.prev_item(), None); assert_eq!(cursor.start::(), Count(0)); assert_eq!(cursor.start::(), Sum(0)); cursor.reset(); assert_eq!( cursor.slice(&tree.len::(), SeekBias::Right).items(), tree.items() ); assert_eq!(cursor.item(), None); assert_eq!(cursor.prev_item(), Some(&6)); assert_eq!(cursor.start::(), Count(6)); assert_eq!(cursor.start::(), Sum(21)); cursor.seek(&Count(3), SeekBias::Right); assert_eq!( cursor.slice(&tree.len::(), SeekBias::Right).items(), [4, 5, 6] ); assert_eq!(cursor.item(), None); assert_eq!(cursor.prev_item(), Some(&6)); assert_eq!(cursor.start::(), Count(6)); assert_eq!(cursor.start::(), Sum(21)); // Seeking can bias left or right cursor.seek(&Sum(1), SeekBias::Left); assert_eq!(cursor.item(), Some(&1)); cursor.seek(&Sum(1), SeekBias::Right); assert_eq!(cursor.item(), Some(&2)); // Slicing without resetting starts from where the cursor is parked at. cursor.seek(&Sum(1), SeekBias::Right); assert_eq!(cursor.slice(&Sum(6), SeekBias::Right).items(), vec![2, 3]); assert_eq!(cursor.slice(&Sum(21), SeekBias::Left).items(), vec![4, 5]); assert_eq!(cursor.slice(&Sum(21), SeekBias::Right).items(), vec![6]); } } ================================================ FILE: xray_core/src/wasm_logging.rs ================================================ use wasm_bindgen::prelude::*; #[wasm_bindgen(js_namespace = console)] extern "C" { pub fn log(s: &str); pub fn error(s: &str); } #[macro_export] macro_rules! println { ($($arg:tt)*) => ($crate::wasm_logging::log(&::std::fmt::format(format_args!($($arg)*)))); } #[macro_export] macro_rules! eprintln { ($($arg:tt)*) => ($crate::wasm_logging::error(&::std::fmt::format(format_args!($($arg)*)))); } ================================================ FILE: xray_core/src/window.rs ================================================ use futures::task::{self, Task}; use futures::{Async, Future, Poll, Stream}; use serde_json; use std::boxed::Box; use std::cell::RefCell; use std::collections::{HashMap, HashSet}; use std::marker::Unsize; use std::ops::CoerceUnsized; use std::rc::{Rc, Weak}; use BackgroundExecutor; pub type ViewId = usize; pub trait View: Stream { fn component_name(&self) -> &'static str; fn will_mount(&mut self, &mut Window, WeakViewHandle) where Self: Sized, { } fn render(&self) -> serde_json::Value; fn dispatch_action(&mut self, serde_json::Value, &mut Window) {} } pub struct Window(Rc>); #[derive(Clone)] pub struct WeakWindowHandle(Weak>); pub struct WindowUpdateStream { counter: usize, polled_once: bool, inner: Weak>, } pub struct Inner { background: Option, root_view: Option, next_view_id: ViewId, views: HashMap>>>, inserted: HashSet, removed: HashSet, focused: Option, height: f64, update_stream_counter: usize, update_stream_task: Option, } pub struct ViewHandle { pub view_id: ViewId, inner: Weak>, } pub struct WeakViewHandle(Weak>); #[derive(Serialize, Debug)] pub struct WindowUpdate { updated: Vec, removed: Vec, focused: Option, } #[derive(Serialize, Debug)] pub struct ViewUpdate { component_name: &'static str, view_id: ViewId, props: serde_json::Value, } impl Window { pub fn new(background: Option, height: f64) -> Self { Window(Rc::new(RefCell::new(Inner { background, root_view: None, next_view_id: 0, views: HashMap::new(), inserted: HashSet::new(), removed: HashSet::new(), focused: None, height: height, update_stream_counter: 0, update_stream_task: None, }))) } pub fn dispatch_action(&mut self, view_id: ViewId, action: serde_json::Value) { let view = self.0.borrow().get_view(view_id); view.map(|view| view.borrow_mut().dispatch_action(action, self)); } pub fn updates(&mut self) -> WindowUpdateStream { let mut inner = self.0.borrow_mut(); inner.update_stream_counter += 1; WindowUpdateStream { counter: inner.update_stream_counter, polled_once: false, inner: Rc::downgrade(&self.0), } } pub fn set_height(&mut self, height: f64) { self.0.borrow_mut().height = height; } pub fn set_root_view(&mut self, root_view: ViewHandle) { self.0.borrow_mut().root_view = Some(root_view); } pub fn height(&self) -> f64 { self.0.borrow().height } pub fn add_view(&mut self, view: T) -> ViewHandle { let view_id = { let mut inner = self.0.borrow_mut(); inner.next_view_id += 1; inner.next_view_id - 1 }; let view_rc = Rc::new(RefCell::new(view)); let weak_view = Rc::downgrade(&view_rc); view_rc .borrow_mut() .will_mount(self, WeakViewHandle(weak_view)); let mut inner = self.0.borrow_mut(); inner.views.insert(view_id, view_rc); inner.inserted.insert(view_id); inner.notify(); ViewHandle { view_id, inner: Rc::downgrade(&self.0), } } pub fn spawn + Send + 'static>(&self, future: F) { self.0 .borrow() .background .as_ref() .map(|background| background.execute(Box::new(future))); } pub fn handle(&self) -> WeakWindowHandle { WeakWindowHandle(Rc::downgrade(&self.0)) } } impl WeakWindowHandle { pub fn map(&self, f: F) -> Option where F: FnOnce(&mut Window) -> T, { self.0.upgrade().map(|inner| { let mut window = Window(inner); f(&mut window) }) } } impl Stream for WindowUpdateStream { type Item = WindowUpdate; type Error = (); fn poll(&mut self) -> Poll, Self::Error> { let inner_ref = match self.inner.upgrade() { None => return Ok(Async::Ready(None)), Some(inner) => inner, }; let mut window_update; { let mut inner = inner_ref.borrow_mut(); if self.counter < inner.update_stream_counter { return Ok(Async::Ready(None)); } if self.polled_once { window_update = WindowUpdate { updated: Vec::new(), removed: inner.removed.iter().cloned().collect(), focused: inner.focused.take(), }; for id in inner.inserted.iter() { if !inner.removed.contains(&id) { let view = inner.get_view(*id).unwrap(); let view = view.borrow(); window_update.updated.push(ViewUpdate { view_id: *id, component_name: view.component_name(), props: view.render(), }); } } for (id, ref view) in inner.views.iter() { let result = view.borrow_mut().poll(); if !inner.inserted.contains(&id) { if let Ok(Async::Ready(Some(()))) = result { let view = view.borrow(); window_update.updated.push(ViewUpdate { view_id: *id, component_name: view.component_name(), props: view.render(), }); } } } } else { window_update = WindowUpdate { updated: Vec::new(), removed: Vec::new(), focused: inner.focused.take(), }; for (id, ref view) in inner.views.iter() { let mut view = view.borrow_mut(); let _ = view.poll(); window_update.updated.push(ViewUpdate { view_id: *id, component_name: view.component_name(), props: view.render(), }); } self.polled_once = true; } } let mut inner = inner_ref.borrow_mut(); inner.inserted.clear(); inner.removed.clear(); if window_update.removed.is_empty() && window_update.updated.is_empty() { inner.update_stream_task = Some(task::current()); Ok(Async::NotReady) } else { Ok(Async::Ready(Some(window_update))) } } } impl Inner { fn notify(&mut self) { self.update_stream_task.take().map(|task| task.notify()); } fn get_view(&self, id: ViewId) -> Option>>> { self.views.get(&id).map(|view| view.clone()) } } impl ViewHandle { pub fn focus(&self) -> Result<(), ()> { let inner = self.inner.upgrade().ok_or(())?; let mut inner = inner.borrow_mut(); inner.focused = Some(self.view_id); inner.notify(); Ok(()) } } impl Drop for ViewHandle { fn drop(&mut self) { // Store the removed view here to prevent it from being dropped until after the borrow of // inner is dropped, since the removed view may itself hold other view handles which will // call drop reentrantly. let mut _removed_view = None; let inner = self.inner.upgrade(); if let Some(inner) = inner { let mut inner = inner.borrow_mut(); _removed_view = inner.views.remove(&self.view_id); inner.removed.insert(self.view_id); inner.notify(); } } } impl WeakViewHandle { pub fn map(&self, f: F) -> Option where F: FnOnce(&mut T) -> R, { self.0.upgrade().map(|view| f(&mut *view.borrow_mut())) } } impl Clone for WeakViewHandle { fn clone(&self) -> Self { WeakViewHandle(self.0.clone()) } } impl CoerceUnsized> for WeakViewHandle where T: Unsize + ?Sized, U: ?Sized, { } #[cfg(test)] mod tests { use super::*; #[test] fn test_view_handle_drop() { // Dropping the window should not cause a panic let mut window = Window::new(None, 100.0); window.add_view(TestView::new(true)); } struct TestView { add_child: bool, handle: Option, updates: NotifyCell<()>, } use notify_cell::NotifyCell; impl TestView { fn new(add_child: bool) -> Self { TestView { add_child, handle: None, updates: NotifyCell::new(()), } } } impl View for TestView { fn component_name(&self) -> &'static str { "TestView" } fn render(&self) -> serde_json::Value { json!({}) } fn will_mount(&mut self, window: &mut Window, _view_handle: WeakViewHandle) { if self.add_child { self.handle = Some(window.add_view(TestView::new(false))); } } } impl Stream for TestView { type Item = (); type Error = (); fn poll(&mut self) -> Poll, Self::Error> { self.updates.poll() } } } ================================================ FILE: xray_core/src/workspace.rs ================================================ use buffer::{self, Buffer, BufferId}; use buffer_view::{BufferView, BufferViewDelegate}; use cross_platform; use file_finder::{FileFinderView, FileFinderViewDelegate}; use futures::{Future, Poll, Stream}; use never::Never; use notify_cell::NotifyCell; use notify_cell::NotifyCellObserver; use project::{ self, LocalProject, PathSearch, PathSearchStatus, Project, ProjectService, RemoteProject, TreeId, }; use rpc::{self, client, server}; use serde_json; use std::cell::{Ref, RefCell, RefMut}; use std::ops::Range; use std::rc::Rc; use window::{View, ViewHandle, WeakViewHandle, WeakWindowHandle, Window}; use ForegroundExecutor; use IntoShared; use UserId; pub trait Workspace { fn user_id(&self) -> UserId; fn project(&self) -> Ref; fn project_mut(&self) -> RefMut; } pub struct LocalWorkspace { next_user_id: UserId, user_id: UserId, project: Rc>, } pub struct RemoteWorkspace { user_id: UserId, project: Rc>, } pub struct WorkspaceService { workspace: Rc>, } #[derive(Serialize, Deserialize)] pub struct ServiceState { user_id: UserId, project: rpc::ServiceId, } pub struct WorkspaceView { foreground: ForegroundExecutor, workspace: Rc>, active_buffer_view: Option>, center_pane: Option, modal: Option, left_panel: Option, updates: NotifyCell<()>, self_handle: Option>, window_handle: Option, } #[derive(Clone, Serialize, Deserialize)] pub struct Anchor { buffer_id: BufferId, range: Range, } #[derive(Deserialize)] #[serde(tag = "type")] enum WorkspaceViewAction { ToggleFileFinder, SaveActiveBuffer, } impl LocalWorkspace { pub fn new(project: LocalProject) -> Self { Self { user_id: 0, next_user_id: 1, project: project.into_shared(), } } } impl Workspace for LocalWorkspace { fn user_id(&self) -> UserId { self.user_id } fn project(&self) -> Ref { self.project.borrow() } fn project_mut(&self) -> RefMut { self.project.borrow_mut() } } impl RemoteWorkspace { pub fn new( foreground: ForegroundExecutor, service: client::Service, ) -> Result { let state = service.state()?; let project = RemoteProject::new(foreground.clone(), service.take_service(state.project)?)?; Ok(Self { user_id: state.user_id, project: project.into_shared(), }) } } impl Workspace for RemoteWorkspace { fn user_id(&self) -> UserId { self.user_id } fn project(&self) -> Ref { self.project.borrow() } fn project_mut(&self) -> RefMut { self.project.borrow_mut() } } impl WorkspaceService { pub fn new(workspace: Rc>) -> Self { Self { workspace } } } impl server::Service for WorkspaceService { type State = ServiceState; type Update = Never; type Request = Never; type Response = Never; fn init(&mut self, connection: &server::Connection) -> ServiceState { let mut workspace = self.workspace.borrow_mut(); let user_id = workspace.next_user_id; workspace.next_user_id += 1; ServiceState { user_id, project: connection .add_service(ProjectService::new(workspace.project.clone())) .service_id(), } } } impl WorkspaceView { pub fn new(foreground: ForegroundExecutor, workspace: Rc>) -> Self { WorkspaceView { workspace, foreground, active_buffer_view: None, center_pane: None, modal: None, left_panel: None, updates: NotifyCell::new(()), self_handle: None, window_handle: None, } } fn toggle_file_finder(&mut self, window: &mut Window) { if self.modal.is_some() { self.modal = None; } else { let delegate = self.self_handle.as_ref().cloned().unwrap(); let view = window.add_view(FileFinderView::new(delegate)); view.focus().unwrap(); self.modal = Some(view); } self.updates.set(()); } fn open_buffer(&self, buffer: T) where T: 'static + Future>, Error = project::Error>, { if let Some(window_handle) = self.window_handle.clone() { let user_id = self.workspace.borrow().user_id(); let view_handle = self.self_handle.clone(); self.foreground .execute(Box::new(buffer.then(move |result| { window_handle.map(|window| match result { Ok(buffer) => { if let Some(view_handle) = view_handle { let mut buffer_view = BufferView::new(buffer, user_id, Some(view_handle.clone())); buffer_view.set_line_height(20.0); let buffer_view = window.add_view(buffer_view); buffer_view.focus().unwrap(); view_handle.map(|view| { view.center_pane = Some(buffer_view); view.modal = None; view.updates.set(()); }); } } Err(error) => { eprintln!("Error opening buffer {:?}", error); unimplemented!("Error handling for open_buffer: {:?}", error); } }); Ok(()) }))) .unwrap(); } } fn save_active_buffer(&self) { if let Some(ref active_buffer_view) = self.active_buffer_view { active_buffer_view.map(|buffer_view| { self.foreground .execute(Box::new(buffer_view.save().then(|result| { if let Err(error) = result { eprintln!("Error saving buffer {:?}", error); unimplemented!("Error handling for save_buffer: {:?}", error); } else { Ok(()) } }))) .unwrap(); }); } } } impl View for WorkspaceView { fn component_name(&self) -> &'static str { "Workspace" } fn render(&self) -> serde_json::Value { json!({ "center_pane": self.center_pane.as_ref().map(|view_handle| view_handle.view_id), "modal": self.modal.as_ref().map(|view_handle| view_handle.view_id), "left_panel": self.left_panel.as_ref().map(|view_handle| view_handle.view_id) }) } fn will_mount(&mut self, window: &mut Window, view_handle: WeakViewHandle) { self.self_handle = Some(view_handle.clone()); self.window_handle = Some(window.handle()); } fn dispatch_action(&mut self, action: serde_json::Value, window: &mut Window) { match serde_json::from_value(action) { Ok(WorkspaceViewAction::ToggleFileFinder) => self.toggle_file_finder(window), Ok(WorkspaceViewAction::SaveActiveBuffer) => self.save_active_buffer(), Err(error) => eprintln!("Unrecognized action {}", error), } } } impl BufferViewDelegate for WorkspaceView { fn set_active_buffer_view(&mut self, handle: WeakViewHandle) { self.active_buffer_view = Some(handle); } } impl FileFinderViewDelegate for WorkspaceView { fn search_paths( &self, needle: &str, max_results: usize, include_ignored: bool, ) -> (PathSearch, NotifyCellObserver) { let workspace = self.workspace.borrow(); let project = workspace.project(); project.search_paths(needle, max_results, include_ignored) } fn did_close(&mut self) { self.modal = None; self.updates.set(()); } fn did_confirm(&mut self, tree_id: TreeId, path: &cross_platform::Path, _: &mut Window) { let workspace = self.workspace.borrow(); self.open_buffer(workspace.project().open_path(tree_id, path)); } } impl Stream for WorkspaceView { type Item = (); type Error = (); fn poll(&mut self) -> Poll, Self::Error> { self.updates.poll() } } ================================================ FILE: xray_electron/.gitignore ================================================ node_modules out ================================================ FILE: xray_electron/README.md ================================================ # Xray Electron Shell This is the front-end of the desktop application. It spawns an instance of `xray_server`, where the majority of application logic resides, and communicates with it over a domain socket. ## Building and running This assumes `xray_electron` is cloned as part of the Xray repository and that all of its sibling packages are next to it. Also, make sure you have installed the required [system dependencies](../CONTRIBUTING.md#install-system-dependencies) before proceeding. ```sh # Move to this subdirectory of the repository: cd xray_electron # Install and build dependencies: yarn install # Launch Electron: yarn start ``` ================================================ FILE: xray_electron/index.html ================================================
================================================ FILE: xray_electron/lib/main_process/main.js ================================================ const {app, BrowserWindow} = require('electron'); const {spawn} = require('child_process'); const path = require('path'); const url = require('url'); const XrayClient = require('../shared/xray_client'); const SERVER_PATH = process.env.XRAY_SERVER_PATH; if (!SERVER_PATH) { console.error('Missing XRAY_SERVER_PATH environment variable'); process.exit(1); } const SOCKET_PATH = process.env.XRAY_SOCKET_PATH; if (!SOCKET_PATH) { console.error('Missing XRAY_SOCKET_PATH environment variable'); process.exit(1); } class XrayApplication { constructor (serverPath, socketPath) { this.serverPath = serverPath; this.socketPath = socketPath; this.windowsById = new Map(); this.readyPromise = new Promise(resolve => app.on('ready', resolve)); this.xrayClient = new XrayClient(); } async start () { const serverProcess = spawn(this.serverPath, [], {stdio: ['ignore', 'pipe', 'inherit']}); app.on('before-quit', () => serverProcess.kill()); serverProcess.on('error', console.error); serverProcess.on('exit', () => app.quit()); await new Promise(resolve => { let serverStdout = ''; serverProcess.stdout.on('data', data => { serverStdout += data.toString('utf8'); if (serverStdout.includes('Listening\n')) resolve() }); }); await this.xrayClient.start(this.socketPath); this.xrayClient.addMessageListener(this._handleMessage.bind(this)); this.xrayClient.sendMessage({type: 'StartApp'}); } async _handleMessage (message) { await this.readyPromise; switch (message.type) { case 'OpenWindow': { this._createWindow(message.window_id); break; } } } _createWindow (windowId) { const window = new BrowserWindow({width: 800, height: 600, webSecurity: false}); window.loadURL(url.format({ pathname: path.join(__dirname, '../../index.html'), search: `windowId=${windowId}&socketPath=${encodeURIComponent(this.socketPath)}`, protocol: 'file:', slashes: true })); this.windowsById.set(windowId, window); window.on('closed', () => { this.windowsById.delete(windowId); this.xrayClient.sendMessage({type: 'CloseWindow', window_id: windowId}); }); } } app.commandLine.appendSwitch("enable-experimental-web-platform-features"); app.on('window-all-closed', function () { if (process.platform !== 'darwin') { app.quit(); } }); const application = new XrayApplication(SERVER_PATH, SOCKET_PATH); application.start().then(() => { console.log('Listening'); }); ================================================ FILE: xray_electron/lib/render_process/main.js ================================================ process.env.NODE_ENV = "production"; const { React, ReactDOM, App, buildViewRegistry } = require("xray_ui"); const XrayClient = require("../shared/xray_client"); const QueryString = require("querystring"); const $ = React.createElement; async function start() { const url = window.location.search.replace("?", ""); const { socketPath, windowId } = QueryString.parse(url); const xrayClient = new XrayClient(); await xrayClient.start(socketPath); const viewRegistry = buildViewRegistry(xrayClient); let initialRender = true; xrayClient.addMessageListener(message => { switch (message.type) { case "UpdateWindow": viewRegistry.update(message); if (initialRender) { ReactDOM.render( $(App, { inBrowser: false, viewRegistry }), document.getElementById("app") ); initialRender = false; } break; default: console.warn("Received unexpected message", message); } }); xrayClient.sendMessage({ type: "StartWindow", window_id: Number(windowId), height: window.innerHeight }); } start(); ================================================ FILE: xray_electron/lib/shared/xray_client.js ================================================ const net = require("net"); const EventEmitter = require('events'); module.exports = class XrayClient { constructor () { this.socket = null; this.emitter = new EventEmitter(); this.currentMessageFragments = []; } start (socketPath) { return new Promise((resolve, reject) => { this.socket = net.connect(socketPath, (error) => { if (error) { reject(error) } else { resolve() } }); this.socket.on('data', this._handleInput.bind(this)); this.socket.on('error', reject) }) } sendMessage (message) { this.socket.write(JSON.stringify(message)); this.socket.write('\n'); } addMessageListener (callback) { this.emitter.on('message', callback); } removeMessageListener (callback) { this.emitter.removeListener('message', callback); } _handleInput (input) { let searchStartIndex = 0; while (searchStartIndex < input.length) { const newlineIndex = input.indexOf('\n', searchStartIndex); if (newlineIndex !== -1) { this.currentMessageFragments.push(input.slice(searchStartIndex, newlineIndex)); this.emitter.emit('message', JSON.parse(Buffer.concat(this.currentMessageFragments))); this.currentMessageFragments.length = 0; searchStartIndex = newlineIndex + 1; } else { this.currentMessageFragments.push(input.slice(searchStartIndex)); break; } } } } ================================================ FILE: xray_electron/package.json ================================================ { "name": "Xray", "version": "0.0.0", "main": "./lib/main_process/main.js", "license": "MIT", "scripts": { "start": "electron .", "test": "electron-mocha --ui=tdd --renderer test/**/*.test.js", "itest": "electron-mocha --ui=tdd --renderer --interactive test/**/*.test.js" }, "dependencies": { "xray_ui": "../xray_ui" }, "devDependencies": { "electron": "2.0.0-beta.7" } } ================================================ FILE: xray_server/Cargo.toml ================================================ [package] name = "xray_server" version = "0.1.0" authors = ["Nathan Sobo "] [dependencies] bytes = "0.4" futures = "0.1" futures-cpupool = "0.1" ignore = { git = "https://github.com/atom/ripgrep", branch = "include_ignored" } parking_lot = "0.5" rand = "0.4" serde = "1.0" serde_derive = "1.0" serde_json = "1.0" tokio-io = "0.1" tokio-core = "0.1" tokio-process = "0.1" tokio-uds = "0.1" xray_core = {path = "../xray_core"} ================================================ FILE: xray_server/README.md ================================================ # Xray Server This crate is an executable that runs as a server process. It can be run in a headless mode in order to host workspaces for remote clients, and it is also spawned by `xray_electron`, which provides the application with a user interface and communicates with the server over a domain socket. ================================================ FILE: xray_server/src/fs.rs ================================================ use futures::{self, Future, Stream}; use ignore::WalkBuilder; use parking_lot::Mutex; use std::char::decode_utf16; use std::ffi::OsString; use std::fs; use std::io::{self, Read, Seek, SeekFrom, Write}; use std::os::unix::fs::MetadataExt; use std::path::PathBuf; use std::sync::Arc; use std::thread; use xray_core::buffer::BufferSnapshot; use xray_core::cross_platform; use xray_core::fs as xray_fs; use xray_core::notify_cell::NotifyCell; pub struct Tree { path: cross_platform::Path, root: xray_fs::Entry, updates: NotifyCell<()>, populated: NotifyCell, } pub struct FileProvider; pub struct File { id: xray_fs::FileId, file: Arc>, } impl Tree { pub fn new>(path: T) -> Result { let path = path.into(); let file_name = OsString::from(path.file_name().ok_or("Path must have a filename")?); let root = xray_fs::Entry::dir(file_name.into(), false, false); let updates = NotifyCell::new(()); let populated = NotifyCell::new(false); Self::populate( path.clone(), root.clone(), updates.clone(), populated.clone(), ); Ok(Self { path: cross_platform::Path::from(path.into_os_string()), root, updates, populated, }) } fn populate( path: PathBuf, root: xray_fs::Entry, updates: NotifyCell<()>, populated: NotifyCell, ) { thread::spawn(move || { let mut stack = vec![root]; let entries = WalkBuilder::new(path.clone()) .follow_links(true) .include_ignored(true) .build() .skip(1) .filter_map(|e| e.ok()); for entry in entries { stack.truncate(entry.depth()); let file_type = entry.file_type().unwrap(); let file_name = entry.file_name(); if file_type.is_dir() { let dir = xray_fs::Entry::dir( file_name.into(), file_type.is_symlink(), entry.ignored(), ); stack.last_mut().unwrap().insert(dir.clone()).unwrap(); stack.push(dir); } else if file_type.is_file() { let file = xray_fs::Entry::file( file_name.into(), file_type.is_symlink(), entry.ignored(), ); stack.last_mut().unwrap().insert(file).unwrap(); } updates.set(()); } populated.set(true); }); } } impl xray_fs::Tree for Tree { fn root(&self) -> xray_fs::Entry { self.root.clone() } fn updates(&self) -> Box> { Box::new(self.updates.observe()) } } impl xray_fs::LocalTree for Tree { fn path(&self) -> &cross_platform::Path { &self.path } fn populated(&self) -> Box> { Box::new( self.populated .observe() .skip_while(|p| Ok(!p)) .into_future() .then(|_| Ok(())), ) } fn as_tree(&self) -> &xray_fs::Tree { self } } impl FileProvider { pub fn new() -> Self { FileProvider } } impl xray_fs::FileProvider for FileProvider { fn open( &self, path: &cross_platform::Path, ) -> Box, Error = io::Error>> { let path = path.to_path_buf(); let (tx, rx) = futures::sync::oneshot::channel(); thread::spawn(|| { fn open(path: PathBuf) -> Result { Ok(File::new(fs::OpenOptions::new() .read(true) .write(true) .open(path)?)?) } let _ = tx.send(open(path)); }); Box::new( rx.then(|result| result.expect("Sender should not be dropped")) .map(|file| Box::new(file) as Box), ) } } impl File { fn new(file: fs::File) -> Result { Ok(File { id: file.metadata()?.ino(), file: Arc::new(Mutex::new(file)), }) } } impl xray_fs::File for File { fn id(&self) -> xray_fs::FileId { self.id } fn read(&self) -> Box> { let (tx, rx) = futures::sync::oneshot::channel(); let file = self.file.clone(); thread::spawn(move || { fn read(file: &fs::File) -> Result { let mut buf_reader = io::BufReader::new(file); let mut contents = String::new(); buf_reader.read_to_string(&mut contents)?; Ok(contents) } let _ = tx.send(read(&file.lock())); }); Box::new(rx.then(|result| result.expect("Sender should not be dropped"))) } fn write_snapshot( &self, snapshot: BufferSnapshot, ) -> Box> { let (tx, rx) = futures::sync::oneshot::channel(); let file = self.file.clone(); thread::spawn(move || { fn write(file: &mut fs::File, snapshot: BufferSnapshot) -> Result<(), io::Error> { let mut size = 0_u64; { let mut buf_writer = io::BufWriter::new(&mut *file); buf_writer.seek(SeekFrom::Start(0))?; for character in snapshot .iter() .flat_map(|c| decode_utf16(c.iter().cloned())) { let character = character.map_err(|_| { io::Error::new( io::ErrorKind::InvalidData, "buffer did not contain valid UTF-8", ) })?; let mut encode_buf = [0_u8; 4]; let encoded_char = character.encode_utf8(&mut encode_buf); buf_writer.write(encoded_char.as_bytes())?; size += encoded_char.len() as u64; } } file.set_len(size)?; Ok(()) } let _ = tx.send(write(&mut file.lock(), snapshot)); }); Box::new(rx.then(|result| result.expect("Sender should not be dropped"))) } } ================================================ FILE: xray_server/src/json_lines_codec.rs ================================================ use std::io; use bytes::BytesMut; use serde::{Deserialize, Serialize}; use serde_json; use tokio_io::codec::{Decoder, Encoder}; use std::marker::PhantomData; pub struct JsonLinesCodec { phantom1: PhantomData, phantom2: PhantomData, } impl JsonLinesCodec where In: for<'a> Deserialize<'a>, Out: Serialize, { pub fn new() -> Self { JsonLinesCodec { phantom1: PhantomData, phantom2: PhantomData, } } } impl Decoder for JsonLinesCodec where In: for<'a> Deserialize<'a>, Out: Serialize, { type Item = In; type Error = io::Error; fn decode(&mut self, buf: &mut BytesMut) -> Result, Self::Error> { if let Some(index) = buf.iter().position(|byte| *byte == b'\n') { let line = buf.split_to(index + 1); let item = serde_json::from_slice(&line[0..line.len() - 1])?; Ok(Some(item)) } else { Ok(None) } } } impl Encoder for JsonLinesCodec where In: for<'a> Deserialize<'a>, Out: Serialize, { type Item = Out; type Error = io::Error; fn encode(&mut self, msg: Self::Item, buf: &mut BytesMut) -> io::Result<()> { let mut vec = serde_json::to_vec(&msg)?; vec.push(b'\n'); buf.extend_from_slice(&vec); Ok(()) } } ================================================ FILE: xray_server/src/main.rs ================================================ mod messages; mod server; mod fs; mod json_lines_codec; extern crate bytes; extern crate futures; extern crate futures_cpupool; extern crate ignore; extern crate parking_lot; extern crate serde; #[macro_use] extern crate serde_derive; extern crate serde_json; extern crate tokio_core; extern crate tokio_io; extern crate tokio_process; extern crate tokio_uds; extern crate xray_core; use std::env; use futures::Stream; use tokio_core::reactor::Core; use tokio_io::AsyncRead; use tokio_uds::UnixListener; use json_lines_codec::JsonLinesCodec; use messages::{IncomingMessage, OutgoingMessage}; use server::Server; fn main() { let headless = env::var("XRAY_HEADLESS").expect("Missing XRAY_HEADLESS environment variable") != "0"; let socket_path = env::var("XRAY_SOCKET_PATH").expect("Missing XRAY_SOCKET_PATH environment variable"); let mut core = Core::new().unwrap(); let handle = core.handle(); let mut server = Server::new(headless, handle.clone()); let _ = std::fs::remove_file(&socket_path); let listener = UnixListener::bind(socket_path, &handle).unwrap(); let handle_connections = listener.incoming().for_each(move |(socket, _)| { let framed_socket = socket.framed(JsonLinesCodec::::new()); server.accept_connection(framed_socket); Ok(()) }); println!("Listening"); core.run(handle_connections).unwrap(); } ================================================ FILE: xray_server/src/messages.rs ================================================ use serde_json; use std::net::SocketAddr; use std::path::PathBuf; use xray_core::{ViewId, WindowId, WindowUpdate}; #[derive(Deserialize, Debug)] #[serde(tag = "type")] pub enum IncomingMessage { StartApp, StartCli { headless: bool, }, TcpListen { port: u16, }, StartWindow { window_id: WindowId, height: f64, }, CloseWindow { window_id: WindowId, }, OpenWorkspace { paths: Vec, }, ConnectToPeer { address: SocketAddr, }, Action { view_id: ViewId, action: serde_json::Value, }, } #[derive(Serialize, Debug)] #[serde(tag = "type")] pub enum OutgoingMessage { OpenWindow { window_id: WindowId }, UpdateWindow(WindowUpdate), Error { description: String }, Ok, } ================================================ FILE: xray_server/src/server.rs ================================================ use bytes::Bytes; use fs; use futures::{future, stream, Future, IntoFuture, Sink, Stream}; use futures_cpupool::CpuPool; use messages::{IncomingMessage, OutgoingMessage}; use std::cell::RefCell; use std::error::Error; use std::io; use std::net::SocketAddr; use std::path::PathBuf; use std::rc::Rc; use tokio_core::net::{TcpListener, TcpStream}; use tokio_core::reactor; use tokio_io::codec; use xray_core::app::Command; use xray_core::{self, App, Never, WindowId}; #[derive(Clone)] pub struct Server { app: Rc>, reactor: reactor::Handle, } impl Server { pub fn new(headless: bool, reactor: reactor::Handle) -> Self { let foreground = Rc::new(reactor.clone()); let background = Rc::new(CpuPool::new_num_cpus()); let file_provider = fs::FileProvider::new(); Server { app: App::new(headless, foreground, background, file_provider), reactor, } } pub fn accept_connection<'a, S>(&mut self, socket: S) where S: 'static + Stream + Sink, { let (outgoing, incoming) = socket.split(); let server = self.clone(); self.reactor.spawn( incoming .into_future() .map(move |(first_message, incoming)| { first_message.map(|first_message| match first_message { IncomingMessage::StartApp => { server.start_app(outgoing, incoming); } IncomingMessage::StartCli { headless } => { server.start_cli(outgoing, incoming, headless); } IncomingMessage::StartWindow { window_id, height } => { server.start_window(outgoing, incoming, window_id, height); } _ => eprintln!("Unexpected message {:?}", first_message), }); }) .then(|_| Ok(())), ); } fn start_app(&self, outgoing: O, incoming: I) where O: 'static + Sink, I: 'static + Stream, { if self.app.borrow().headless() { self.send_outgoing( outgoing, stream::once(Ok(OutgoingMessage::Error { description: "This is a headless application instance".into(), })), ); } else { if let Some(commands) = self.app.borrow_mut().commands() { let server = self.clone(); let outgoing_commands = commands.map(|update| match update { Command::OpenWindow(window_id) => OutgoingMessage::OpenWindow { window_id }, }); let outgoing_responses = report_input_errors(incoming.and_then(move |message| { server .handle_app_message(message) .map_err(|_| unreachable!()) })); self.send_outgoing(outgoing, outgoing_commands.select(outgoing_responses)); } else { self.send_outgoing( outgoing, stream::once(Ok(OutgoingMessage::Error { description: "An application client is already registered".into(), })), ); } } } fn start_cli(&self, outgoing: O, incoming: I, headless: bool) where O: 'static + Sink, I: 'static + Stream, { match (self.app.borrow().headless(), headless) { (true, false) => { return self.send_outgoing(outgoing, stream::once(Ok(OutgoingMessage::Error { description: "Since Xray was initially started with --headless, all subsequent commands must be --headless".into() }))); } (false, true) => { return self.send_outgoing(outgoing, stream::once(Ok(OutgoingMessage::Error { description: "Since Xray was initially started without --headless, no subsequent commands may be --headless".into() }))); } _ => {} } let server = self.clone(); let outgoing_ack = stream::once(Ok(OutgoingMessage::Ok)); let outgoing_responses = report_input_errors(incoming.and_then(move |message| { server .handle_app_message(message) .map_err(|_| unreachable!()) })); self.send_outgoing(outgoing, outgoing_ack.chain(outgoing_responses)); } pub fn start_window(&self, outgoing: O, incoming: I, window_id: WindowId, height: f64) where O: 'static + Sink, I: 'static + Stream, { let server = self.clone(); let receive_incoming = incoming .for_each(move |message| { server.handle_window_message(window_id, message); Ok(()) }) .then(|_| Ok(())); self.reactor.spawn(receive_incoming); match self.app.borrow_mut().start_window(&window_id, height) { Ok(updates) => { self.send_outgoing( outgoing, updates.map(|update| OutgoingMessage::UpdateWindow(update)), ); } Err(_) => { self.send_outgoing( outgoing, stream::once(Ok(OutgoingMessage::Error { description: format!("No window exists for id {}", window_id), })), ); } } } fn handle_app_message( &self, message: IncomingMessage, ) -> Box> { let result = match message { IncomingMessage::OpenWorkspace { paths } => { Box::new(self.open_workspace(paths).into_future()) } IncomingMessage::TcpListen { port } => Box::new(self.tcp_listen(port).into_future()), IncomingMessage::ConnectToPeer { address } => self.connect_to_peer(address), IncomingMessage::CloseWindow { window_id } => { Box::new(self.close_window(window_id).into_future()) } _ => Box::new(future::err(format!("Unexpected message {:?}", message))), }; Box::new(result.then(|result| match result { Ok(_) => Ok(OutgoingMessage::Ok), Err(description) => Ok(OutgoingMessage::Error { description }), })) } fn handle_window_message(&self, window_id: WindowId, message: IncomingMessage) { match message { IncomingMessage::Action { view_id, action } => { self.app .borrow_mut() .dispatch_action(window_id, view_id, action); } _ => { eprintln!("Unexpected message {:?}", message); } } } fn close_window(&self, window_id: WindowId) -> Result<(), String> { self.app .borrow_mut() .close_window(window_id) .map_err(|_| "Window not found".to_owned()) } fn open_workspace(&self, paths: Vec) -> Result<(), String> { if !paths.iter().all(|path| path.is_absolute()) { return Err("All paths must be absolute".to_owned()); } let roots = paths .iter() .map(|path| fs::Tree::new(path).unwrap()) .collect(); self.app.borrow_mut().open_local_workspace(roots); Ok(()) } fn tcp_listen(&self, port: u16) -> Result<(), String> { let local_addr = SocketAddr::new("0.0.0.0".parse().unwrap(), port); let listener = TcpListener::bind(&local_addr, &self.reactor) .map_err(|_| "Error binding address".to_owned())?; let app = self.app.clone(); let reactor = self.reactor.clone(); let handle_incoming = listener .incoming() .map_err(|_| eprintln!("Error accepting incoming connection")) .for_each(move |(socket, _)| { socket.set_nodelay(true).unwrap(); let transport = codec::length_delimited::Framed::<_, Bytes>::new(socket); let (tx, rx) = transport.split(); let connection = App::connect_to_client(app.clone(), rx.map(|frame| frame.into())); reactor.spawn(tx.send_all( connection.map_err(|_| -> io::Error { unreachable!() }), ).then(|result| { if let Err(error) = result { eprintln!("Error sending message to client on TCP socket: {}", error); } Ok(()) })); Ok(()) }); self.reactor.spawn(handle_incoming); Ok(()) } fn connect_to_peer(&self, address: SocketAddr) -> Box> { let reactor = self.reactor.clone(); let app = self.app.clone(); Box::new( TcpStream::connect(&address, &self.reactor) .map_err(move |error| { format!( "Could not connect to address {}, {}", address, error.description(), ) }) .and_then(move |socket| { socket.set_nodelay(true).unwrap(); let transport = codec::length_delimited::Framed::<_, Bytes>::new(socket); let (tx, rx) = transport.split(); let app = app.borrow(); app.connect_to_server(rx.map(|frame| frame.into())) .map_err(|error| format!("RPC error: {}", error)) .and_then(move |connection| { reactor.spawn( tx.send_all( connection .map(|bytes| bytes.into()) .map_err(|_| -> io::Error { unreachable!() }), ).then(|result| { if let Err(error) = result { eprintln!( "Error sending message to server on TCP socket: {}", error ); } Ok(()) }), ); Ok(()) }) }), ) } fn send_outgoing(&self, outgoing: O, responses: I) where O: 'static + Sink, I: 'static + Stream, { self.reactor.spawn( outgoing .send_all(responses.map_err(|_| unreachable!())) .then(|_| Ok(())), ); } } fn report_input_errors(incoming: S) -> Box> where S: 'static + Stream, { Box::new( incoming .then(|value| match value { Err(error) => Ok(OutgoingMessage::Error { description: format!("Error reading message on server: {}", error), }), _ => value, }) .map_err(|_| ()), ) } ================================================ FILE: xray_ui/README.md ================================================ # Xray UI This folder houses Xray's user interface, which is implemented in JavaScript and designed to run in a web environment. It is depended upon by [`xray_electron`](../xray_electron), which presents Xray as a desktop application, and [`xray_browser`](../xray_browser), which presents Xray in the browser. ================================================ FILE: xray_ui/lib/action_dispatcher.js ================================================ const React = require("react"); const ReactDOM = require("react-dom"); const propTypes = require("prop-types"); const { styled } = require("styletron-react"); const $ = React.createElement; const Root = styled("div", { width: "100%", height: "100%" }); class ActionSet { constructor() { this.context = null; this.actions = new Map(); } } class ActionDispatcher extends React.Component { constructor() { super(); this.handleKeyDown = this.handleKeyDown.bind(this); this.actionSets = new WeakMap(); this.defaultActionSet = new ActionSet(); } render() { return $(Root, { onKeyDown: this.handleKeyDown }, this.props.children); } handleKeyDown(event) { const { keyBindings } = this.props; const keystrokeString = keystrokeStringForEvent(event); let element = event.target; while (element) { let actionSet = this.actionSets.get(element); if (actionSet) { for (let i = keyBindings.length - 1; i >= 0; i--) { const keyBinding = keyBindings[i]; const action = actionSet.actions.get(keyBinding.action); if ( keyBinding.key === keystrokeString && action && contextMatches(actionSet.context, keyBinding.context) ) { if (action.onWillDispatch) action.onWillDispatch(); action.dispatch(); return; } } } element = element.parentElement; } } getChildContext() { return { actionSets: this.actionSets, currentActionSet: this.defaultActionSet }; } } ActionDispatcher.childContextTypes = { actionSets: propTypes.instanceOf(WeakMap), currentActionSet: propTypes.instanceOf(ActionSet) }; class ActionContext extends React.Component { constructor() { super(); this.actionSet = new ActionSet(); } componentWillMount() { this.actionSet.context = this.context.currentActionSet ? new Set(this.context.currentActionSet.context) : new Set(); if (this.props.add) { if (Array.isArray(this.props.add)) { for (let i = 0; i < this.props.add.length; i++) { this.actionSet.context.add(this.props.add[i]); } } else { this.actionSet.context.add(this.props.add); } } if (this.props.remove) { if (Array.isArray(this.props.remove)) { for (let i = 0; i < this.props.remove.length; i++) { this.actionSet.context.delete(this.props.remove[i]); } } else { this.actionSet.context.delete(this.props.remove); } } } componentDidMount() { if (this.context.actionSets) { this.context.actionSets.set( ReactDOM.findDOMNode(this).parentElement, this.actionSet ); } } render() { return this.props.children; } getChildContext() { return { currentActionSet: this.actionSet }; } } ActionContext.contextTypes = { actionSets: propTypes.instanceOf(WeakMap), currentActionSet: propTypes.instanceOf(ActionSet) }; ActionContext.childContextTypes = { currentActionSet: propTypes.instanceOf(ActionSet) }; class Action extends React.Component { constructor() { super(); this.dispatch = this.dispatch.bind(this); } render() { return null; } componentDidMount() { this.context.currentActionSet.actions.set(this.props.type, { onWillDispatch: this.props.onWillDispatch, dispatch: this.dispatch }); } dispatch() { this.context.dispatchAction({ type: this.props.type }); } } Action.contextTypes = { currentActionSet: propTypes.instanceOf(ActionSet), dispatchAction: propTypes.func }; function keystrokeStringForEvent(event) { let keystroke = ""; if (event.ctrlKey) keystroke = "ctrl"; if (event.altKey) keystroke = appendKeystrokeElement(keystroke, "alt"); if (event.shiftKey) keystroke = appendKeystrokeElement(keystroke, "shift"); if (event.metaKey) keystroke = appendKeystrokeElement(keystroke, "cmd"); switch (event.key) { case "ArrowDown": return appendKeystrokeElement(keystroke, "down"); case "ArrowUp": return appendKeystrokeElement(keystroke, "up"); case "ArrowLeft": return appendKeystrokeElement(keystroke, "left"); case "ArrowRight": return appendKeystrokeElement(keystroke, "right"); default: return appendKeystrokeElement(keystroke, event.key.toLowerCase()); } } function appendKeystrokeElement(keyString, element) { if (keyString.length > 0) keyString += "-"; keyString += element; return keyString; } function contextMatches(context, expression) { // TODO: Support arbitrary boolean expressions let expressionStartIndex = 0; for (let i = 0; i < expression.length; i++) { if (expression[i] == " ") { const component = expression.slice(expressionStartIndex, i); if (!context.has(component)) { return false; } } } return true; } module.exports = { ActionDispatcher, ActionContext, Action, keystrokeStringForEvent, contextMatches }; ================================================ FILE: xray_ui/lib/app.js ================================================ const propTypes = require("prop-types"); const React = require("react"); const { Client: StyletronClient } = require("styletron-engine-atomic"); const { Provider: StyletronProvider } = require("styletron-react"); const { ActionDispatcher } = require("./action_dispatcher"); const TextEditor = require("./text_editor/text_editor"); const ThemeProvider = require("./theme_provider"); const View = require("./view"); const ViewRegistry = require("./view_registry"); const $ = React.createElement; // TODO: Eventually, the theme should be provided to the view by the server const theme = { editor: { fontFamily: "Menlo", backgroundColor: "white", baseTextColor: "black", fontSize: 14, lineHeight: 1.5 }, userColors: [ { r: 31, g: 150, b: 255, a: 1 }, { r: 64, g: 181, b: 87, a: 1 }, { r: 206, g: 157, b: 59, a: 1 }, { r: 216, g: 49, b: 176, a: 1 }, { r: 235, g: 221, b: 91, a: 1 } ] }; // TODO: Eventually, the keyBindings should be provided to the view by the server const keyBindings = [ { key: "cmd-t", context: "Workspace", action: "ToggleFileFinder" }, { key: "ctrl-t", context: "Workspace", action: "ToggleFileFinder" }, { key: "cmd-s", context: "Workspace", action: "SaveActiveBuffer" }, { key: "up", context: "FileFinder", action: "SelectPrevious" }, { key: "down", context: "FileFinder", action: "SelectNext" }, { key: "enter", context: "FileFinder", action: "Confirm" }, { key: "escape", context: "FileFinder", action: "Close" }, { key: "alt-shift-up", context: "TextEditor", action: "AddSelectionAbove" }, { key: "alt-shift-down", context: "TextEditor", action: "AddSelectionBelow" }, { key: "shift-up", context: "TextEditor", action: "SelectUp" }, { key: "shift-down", context: "TextEditor", action: "SelectDown" }, { key: "shift-left", context: "TextEditor", action: "SelectLeft" }, { key: "shift-right", context: "TextEditor", action: "SelectRight" }, { key: "alt-shift-left", context: "TextEditor", action: "SelectToBeginningOfWord" }, { key: "alt-shift-right", context: "TextEditor", action: "SelectToEndOfWord" }, { key: "shift-cmd-left", context: "TextEditor", action: "SelectToBeginningOfLine" }, { key: "shift-cmd-right", context: "TextEditor", action: "SelectToEndOfLine" }, { key: "shift-cmd-up", context: "TextEditor", action: "SelectToTop" }, { key: "shift-cmd-down", context: "TextEditor", action: "SelectToBottom" }, { key: "up", context: "TextEditor", action: "MoveUp" }, { key: "down", context: "TextEditor", action: "MoveDown" }, { key: "left", context: "TextEditor", action: "MoveLeft" }, { key: "right", context: "TextEditor", action: "MoveRight" }, { key: "alt-left", context: "TextEditor", action: "MoveToBeginningOfWord" }, { key: "alt-right", context: "TextEditor", action: "MoveToEndOfWord" }, { key: "cmd-left", context: "TextEditor", action: "MoveToBeginningOfLine" }, { key: "cmd-right", context: "TextEditor", action: "MoveToEndOfLine" }, { key: "cmd-up", context: "TextEditor", action: "MoveToTop" }, { key: "cmd-down", context: "TextEditor", action: "MoveToBottom" }, { key: "backspace", context: "TextEditor", action: "Backspace" }, { key: "delete", context: "TextEditor", action: "Delete" } ]; const styletronInstance = new StyletronClient(); class App extends React.Component { constructor(props) { super(props); } getChildContext() { return { inBrowser: this.props.inBrowser, viewRegistry: this.props.viewRegistry }; } render() { return $( StyletronProvider, { value: styletronInstance }, $( ThemeProvider, { theme }, $(ActionDispatcher, { keyBindings }, $(View, { id: 0 })) ) ); } } App.childContextTypes = { inBrowser: propTypes.bool, viewRegistry: propTypes.instanceOf(ViewRegistry) }; module.exports = App; ================================================ FILE: xray_ui/lib/debounce.js ================================================ module.exports = function debounce (fn, wait) { let timestamp, timeout function later () { const last = Date.now() - timestamp if (last < wait && last >= 0) { timeout = setTimeout(later, wait - last) } else { timeout = null fn() } } return function () { timestamp = Date.now() if (!timeout) timeout = setTimeout(later, wait) } } ================================================ FILE: xray_ui/lib/file_finder.js ================================================ const React = require("react"); const ReactDOM = require("react-dom"); const { styled } = require("styletron-react"); const $ = React.createElement; const { ActionContext, Action } = require("./action_dispatcher"); const Root = styled("div", { boxShadow: "0 6px 12px -2px rgba(0, 0, 0, 0.4)", backgroundColor: "#f2f2f2", borderRadius: "6px", width: 500 + "px", padding: "10px", marginTop: "20px" }); const QueryInput = styled("input", { width: "100%", boxSizing: "border-box", padding: "5px", fontSize: "10pt", outline: "none", border: "1px solid #556de8", boxShadow: "0 0 0 1px #556de8", backgroundColor: "#ebeeff", borderRadius: "3px", color: "#232324" }); const SearchResultList = styled("ol", { listStyleType: "none", height: "200px", overflow: "auto", padding: 0 }); const SearchResultListItem = styled("li", { listStyleType: "none", padding: "0.75em 1em", lineHeight: "2em", fontSize: "10pt", fontFamily: "sans-serif", borderBottom: "1px solid #dbdbdc" }); const SearchResultMatchedQuery = styled("b", { color: "#304ee2", fontWeight: "bold" }); class SelectedSearchResultListItem extends React.Component { render() { return $( styled(SearchResultListItem, { backgroundColor: "#dbdbdc" }), {}, ...this.props.children ); } componentDidMount() { this.scrollIntoViewIfNeeded(); } componentDidUpdate() { this.scrollIntoViewIfNeeded(); } scrollIntoViewIfNeeded() { const domNode = ReactDOM.findDOMNode(this); if (domNode) domNode.scrollIntoViewIfNeeded(); } } module.exports = class FileFinder extends React.Component { constructor() { super(); this.didChangeQuery = this.didChangeQuery.bind(this); this.didChangeIncludeIgnored = this.didChangeIncludeIgnored.bind(this); } render() { return $( ActionContext, { add: "FileFinder" }, $( Root, null, $(QueryInput, { $ref: inputNode => (this.queryInput = inputNode), value: this.props.query, onChange: this.didChangeQuery }), $( SearchResultList, {}, ...this.props.results.map((result, i) => this.renderSearchResult(result, i === this.props.selected_index) ) ) ), $(Action, { type: "SelectPrevious" }), $(Action, { type: "SelectNext" }), $(Action, { type: "Confirm" }), $(Action, { type: "Close" }) ); } renderSearchResult({ positions, display_path }, isSelected) { let pathIndex = 0; let queryIndex = 0; const children = []; while (true) { if (pathIndex === positions[queryIndex]) { children.push( $(SearchResultMatchedQuery, null, display_path[pathIndex]) ); pathIndex++; queryIndex++; } else if (queryIndex < positions.length) { const nextPathIndex = positions[queryIndex]; children.push(display_path.slice(pathIndex, nextPathIndex)); pathIndex = nextPathIndex; } else { children.push(display_path.slice(pathIndex)); break; } } const item = isSelected ? SelectedSearchResultListItem : SearchResultListItem; return $(item, null, ...children); } focus() { this.queryInput.focus(); } didChangeQuery(event) { this.props.dispatch({ type: "UpdateQuery", query: event.target.value }); } didChangeIncludeIgnored(event) { this.props.dispatch({ type: "UpdateIncludeIgnored", include_ignored: event.target.checked }); } }; ================================================ FILE: xray_ui/lib/index.js ================================================ const FileFinder = require("./file_finder"); const ViewRegistry = require("./view_registry"); const Workspace = require("./workspace"); const TextEditorView = require("./text_editor/text_editor"); exports.buildViewRegistry = function buildViewRegistry(client) { const viewRegistry = new ViewRegistry({ onAction: action => { action.type = "Action"; client.sendMessage(action); } }); viewRegistry.addComponent("Workspace", Workspace); viewRegistry.addComponent("FileFinder", FileFinder); viewRegistry.addComponent("BufferView", TextEditorView); return viewRegistry; }; exports.App = require("./app"); exports.React = require("react"); exports.ReactDOM = require("react-dom"); ================================================ FILE: xray_ui/lib/modal.js ================================================ const React = require("react"); const ReactDOM = require("react-dom"); const { styled } = require("styletron-react"); const $ = React.createElement; const Root = styled("div", { position: "absolute", top: 0, left: 0, right: 0, width: "min-content", margin: "auto", outline: "none" }); module.exports = class Modal extends React.Component { render() { return $(Root, { tabIndex: -1 }, this.props.children); } componentDidMount() { this.previouslyFocusedElement = document.activeElement; } componentWillUnmount() { const element = ReactDOM.findDOMNode(this); if (element.contains(document.activeElement)) { this.previouslyFocusedElement.focus(); this.previouslyFocusedElement = null } } }; ================================================ FILE: xray_ui/lib/text_editor/shaders.js ================================================ exports.textBlendAttributes = { unitQuadVertex: 0, targetOrigin: 1, targetSize: 2, textColorRGBA: 3, atlasOrigin: 4, atlasSize: 5 }; exports.textBlendVertex = ` #version 300 es layout (location = 0) in vec2 unitQuadVertex; layout (location = 1) in vec2 targetOrigin; layout (location = 2) in vec2 targetSize; layout (location = 3) in vec4 textColorRGBA; layout (location = 4) in vec2 atlasOrigin; layout (location = 5) in vec2 atlasSize; uniform vec2 viewportScale; uniform float scrollLeft; flat out vec4 textColor; out vec2 atlasPosition; void main() { vec2 targetPixelPosition = (targetOrigin + unitQuadVertex * targetSize) - vec2(scrollLeft, 0.0); vec2 targetPosition = targetPixelPosition * viewportScale + vec2(-1.0, 1.0); gl_Position = vec4(targetPosition, 0.0, 1.0); textColor = textColorRGBA * vec4(1.0 / 255.0, 1.0 / 255.0, 1.0 / 255.0, 1.0); // Conversion to sRGB. textColor = textColor * textColor; textColor = textColorRGBA; atlasPosition = atlasOrigin + unitQuadVertex * atlasSize; } `.trim(); exports.textBlendPass1Fragment = ` #version 300 es precision mediump float; layout(location = 0) out vec4 outColor; flat in vec4 textColor; in vec2 atlasPosition; uniform sampler2D atlasTexture; void main() { vec3 atlasColor = texture(atlasTexture, atlasPosition).rgb; vec3 textColorRGB = textColor.rgb; vec3 correctedAtlasColor = mix(vec3(1.0) - atlasColor, sqrt(vec3(1.0) - atlasColor * atlasColor), textColorRGB); outColor = vec4(correctedAtlasColor, 1.0); } `.trim(); exports.textBlendPass2Fragment = ` #version 300 es precision mediump float; layout(location = 0) out vec4 outColor; flat in vec4 textColor; in vec2 atlasPosition; uniform sampler2D atlasTexture; void main() { vec3 atlasColor = texture(atlasTexture, atlasPosition).rgb; vec3 textColorRGB = textColor.rgb; vec3 correctedAtlasColor = mix(vec3(1.0) - atlasColor, sqrt(vec3(1.0) - atlasColor * atlasColor), textColorRGB); vec3 adjustedForegroundColor = textColorRGB * correctedAtlasColor; outColor = vec4(adjustedForegroundColor, 1.0); } `.trim(); exports.solidAttributes = { unitQuadVertex: 0, targetOrigin: 1, targetSize: 2, colorRGBA: 3 }; exports.solidVertex = ` #version 300 es layout (location = 0) in vec2 unitQuadVertex; layout (location = 1) in vec2 targetOrigin; layout (location = 2) in vec2 targetSize; layout (location = 3) in vec4 colorRGBA; flat out vec4 color; uniform vec2 viewportScale; uniform float scrollLeft; void main() { vec2 targetPixelPosition = (targetOrigin + unitQuadVertex * targetSize) - vec2(scrollLeft, 0.0); vec2 targetPosition = targetPixelPosition * viewportScale + vec2(-1.0, 1.0); gl_Position = vec4(targetPosition, 0.0, 1.0); color = colorRGBA * vec4(1.0 / 255.0, 1.0 / 255.0, 1.0 / 255.0, 1.0); } `.trim(); exports.solidFragment = ` #version 300 es precision mediump float; flat in vec4 color; layout (location = 0) out vec4 outColor; void main() { outColor = color; } `.trim(); ================================================ FILE: xray_ui/lib/text_editor/text_editor.js ================================================ const React = require("react"); const ReactDOM = require("react-dom"); const PropTypes = require("prop-types"); const { styled } = require("styletron-react"); const TextPlane = require("./text_plane"); const debounce = require("../debounce"); const $ = React.createElement; const { ActionContext, Action } = require("../action_dispatcher"); const CURSOR_BLINK_RESUME_DELAY = 300; const CURSOR_BLINK_PERIOD = 800; const Root = styled("div", { width: "100%", height: "100%", overflow: "hidden", cursor: "text" }); class TextEditor extends React.Component { static getDerivedStateFromProps(nextProps, prevState) { let derivedState = null; if (nextProps.width != null && nextProps.width !== prevState.width) { derivedState = { width: nextProps.width }; } if (nextProps.height != null && nextProps.height !== prevState.height) { if (derivedState) { derivedState.height = nextProps.height; } else { derivedState = { height: nextProps.height }; } } return derivedState; } constructor(props) { super(props); this.handleMouseDown = this.handleMouseDown.bind(this); this.handleMouseWheel = this.handleMouseWheel.bind(this); this.handleKeyDown = this.handleKeyDown.bind(this); this.pauseCursorBlinking = this.pauseCursorBlinking.bind(this); this.debouncedStartCursorBlinking = debounce( this.startCursorBlinking.bind(this), CURSOR_BLINK_RESUME_DELAY ); this.paddingLeft = 5; this.state = { scrollLeft: 0, showLocalCursors: true }; } componentDidMount() { const element = ReactDOM.findDOMNode(this); this.resizeObserver = new ResizeObserver(([{ contentRect }]) => { this.componentDidResize({ width: contentRect.width, height: contentRect.height }); }); this.resizeObserver.observe(element); if (this.props.width == null || this.props.height == null) { const dimensions = { width: element.offsetWidth, height: element.offsetHeight }; this.componentDidResize(dimensions); this.setState(dimensions); } element.addEventListener("wheel", this.handleMouseWheel, { passive: true }); element.addEventListener("mousedown", this.handleMouseDown, { passive: true }); this.startCursorBlinking(); } componentWillUnmount() { this.stopCursorBlinking(); const element = ReactDOM.findDOMNode(this); element.removeEventListener("wheel", this.handleMouseWheel, { passive: true }); this.resizeObserver.disconnect(); } componentDidResize(measurements) { this.props.dispatch({ type: "SetDimensions", width: measurements.width, height: measurements.height }); } render() { this.flushHorizontalAutoscroll(); return $( ActionContext, { add: "TextEditor" }, $( Root, { tabIndex: -1, onKeyDown: this.handleKeyDown, $ref: element => { this.element = element; } }, $(TextPlane, { showLocalCursors: this.state.showLocalCursors, lineHeight: this.props.line_height, scrollTop: this.props.scroll_top, paddingLeft: this.paddingLeft, scrollLeft: this.getScrollLeft(), height: this.props.height, width: this.getScrollWidth(), selections: this.props.selections, firstVisibleRow: this.props.first_visible_row, totalRowCount: this.props.total_row_count, lines: this.props.lines, ref: textPlane => { this.textPlane = textPlane; } }) ), $(Action, { type: "AddSelectionAbove", onWillDispatch: this.pauseCursorBlinking }), $(Action, { type: "AddSelectionBelow", onWillDispatch: this.pauseCursorBlinking }), $(Action, { type: "SelectUp", onWillDispatch: this.pauseCursorBlinking }), $(Action, { type: "SelectDown", onWillDispatch: this.pauseCursorBlinking }), $(Action, { type: "SelectLeft", onWillDispatch: this.pauseCursorBlinking }), $(Action, { type: "SelectRight", onWillDispatch: this.pauseCursorBlinking }), $(Action, { type: "SelectToBeginningOfWord", onWillDispatch: this.pauseCursorBlinking }), $(Action, { type: "SelectToEndOfWord", onWillDispatch: this.pauseCursorBlinking }), $(Action, { type: "SelectToBeginningOfLine", onWillDispatch: this.pauseCursorBlinking }), $(Action, { type: "SelectToEndOfLine", onWillDispatch: this.pauseCursorBlinking }), $(Action, { type: "SelectToTop", onWillDispatch: this.pauseCursorBlinking }), $(Action, { type: "SelectToBottom", onWillDispatch: this.pauseCursorBlinking }), $(Action, { type: "MoveUp", onWillDispatch: this.pauseCursorBlinking }), $(Action, { type: "MoveDown", onWillDispatch: this.pauseCursorBlinking }), $(Action, { type: "MoveLeft", onWillDispatch: this.pauseCursorBlinking }), $(Action, { type: "MoveRight", onWillDispatch: this.pauseCursorBlinking }), $(Action, { type: "MoveToBeginningOfWord", onWillDispatch: this.pauseCursorBlinking }), $(Action, { type: "MoveToEndOfWord", onWillDispatch: this.pauseCursorBlinking }), $(Action, { type: "MoveToBeginningOfLine", onWillDispatch: this.pauseCursorBlinking }), $(Action, { type: "MoveToEndOfLine", onWillDispatch: this.pauseCursorBlinking }), $(Action, { type: "MoveToTop", onWillDispatch: this.pauseCursorBlinking }), $(Action, { type: "MoveToBottom", onWillDispatch: this.pauseCursorBlinking }), $(Action, { type: "Backspace", onWillDispatch: this.pauseCursorBlinking }), $(Action, { type: "Delete", onWillDispatch: this.pauseCursorBlinking }) ); } handleMouseDown(event) { if (this.canUseTextPlane()) { this.handleClick(event); switch (event.detail) { case 2: this.handleDoubleClick(); break; case 3: this.handleTripleClick(); break; } } } handleClick({ clientX, clientY }) { const { scroll_top, line_height, first_visible_row, lines } = this.props; const { scrollLeft } = this.state; const targetX = clientX - this.element.offsetLeft + scrollLeft - this.getGutterWidth(); const targetY = clientY - this.element.offsetTop + scroll_top; const row = Math.max(0, Math.floor(targetY / line_height)); const line = lines[row - first_visible_row]; if (line != null) { const glyphWidths = this.textPlane.layoutLine(line); let column = 0; let x = 0; while (x < targetX && column < line.length) { const glyphWidth = glyphWidths[column]; if (targetX > x + glyphWidth / 2) { column++; x += glyphWidth; } else { break; } } this.pauseCursorBlinking(); this.props.dispatch({ type: "SetCursorPosition", row, column, autoscroll: false }); } } handleDoubleClick() { this.pauseCursorBlinking(); this.props.dispatch({ type: "SelectWord" }); } handleTripleClick() { this.pauseCursorBlinking(); this.props.dispatch({ type: "SelectLine" }); } handleMouseWheel(event) { if (Math.abs(event.deltaX) > Math.abs(event.deltaY)) { this.setScrollLeft(this.state.scrollLeft + event.deltaX); } else { this.props.dispatch({ type: "UpdateScrollTop", delta: event.deltaY }); } } handleKeyDown(event) { const hasNoModifierKeys = !event.metaKey && !event.ctrlKey && !event.altKey; if (event.key.length === 1 && hasNoModifierKeys) { this.props.dispatch({ type: "Edit", text: event.key }); } else if (event.key === "Enter") { this.props.dispatch({ type: "Edit", text: "\n" }); } } pauseCursorBlinking() { this.stopCursorBlinking(); this.debouncedStartCursorBlinking(); } stopCursorBlinking() { if (this.state.cursorsBlinking) { window.clearInterval(this.cursorBlinkIntervalHandle); this.cursorBlinkIntervalHandle = null; this.setState({ showLocalCursors: true, cursorsBlinking: false }); } } startCursorBlinking() { if (!this.state.cursorsBlinking) { this.cursorBlinkIntervalHandle = window.setInterval(() => { this.setState({ showLocalCursors: !this.state.showLocalCursors }); }, CURSOR_BLINK_PERIOD / 2); this.setState({ cursorsBlinking: true, showLocalCursors: false }); } } focus() { this.element.focus(); } flushHorizontalAutoscroll() { const { horizontal_autoscroll, horizontal_margin, width } = this.props; const gutterWidth = this.getGutterWidth(); const baseCharWidth = this.getBaseCharacterWidth(); if ( horizontal_autoscroll && width && gutterWidth && baseCharWidth && this.canUseTextPlane() ) { const horizontalMarginInPixels = baseCharWidth * horizontal_margin; const desiredScrollLeft = this.textPlane.measureLine( horizontal_autoscroll.start_line, horizontal_autoscroll.start.column ) - horizontalMarginInPixels; const desiredScrollRight = this.textPlane.measureLine( horizontal_autoscroll.end_line, horizontal_autoscroll.end.column ) + gutterWidth + horizontalMarginInPixels; // This function will be called during render, so we avoid calling // setState and we manually manipulate this.state instead. if (desiredScrollLeft < this.getScrollLeft()) { this.state.scrollLeft = this.constrainScrollLeft(desiredScrollLeft); } if (desiredScrollRight > this.getScrollRight()) { this.state.scrollLeft = this.constrainScrollLeft( desiredScrollRight - width ); } this.props.horizontal_autoscroll = null; } } getScrollLeft() { return this.constrainScrollLeft(this.state.scrollLeft); } getScrollRight() { if (this.props.width) { return this.getScrollLeft() + this.props.width; } else { return this.getScrollLeft(); } } setScrollLeft(scrollLeft) { this.setState({ scrollLeft: this.constrainScrollLeft(scrollLeft) }); } constrainScrollLeft(scrollLeft) { return Math.max(0, Math.min(scrollLeft, this.getMaxScrollLeft())); } getMaxScrollLeft() { const contentWidth = this.getContentWidth(); if (contentWidth != null && this.props.width != null) { return Math.max(0, contentWidth - this.props.width); } else { return 0; } } getScrollWidth() { const contentWidth = this.getContentWidth(); if (contentWidth != null && this.props.width != null) { return Math.max(contentWidth, this.props.width); } else { return 0; } } getContentWidth() { const longestLineWidth = this.getLongestLineWidth(); const baseCharWidth = this.getBaseCharacterWidth(); const gutterWidth = this.getGutterWidth(); if ( longestLineWidth != null && baseCharWidth != null && gutterWidth != null ) { return Math.ceil(gutterWidth + longestLineWidth + baseCharWidth); } else { return null; } } getBaseCharacterWidth() { if (this.baseCharWidth == null && this.canUseTextPlane()) { this.baseCharWidth = this.textPlane.measureLine("X"); } return this.baseCharWidth; } getLongestLineWidth() { const { longest_line: longestLine } = this.props; if (this.longestLine != longestLine && this.canUseTextPlane()) { this.longestLine = longestLine; this.longestLineWidth = this.textPlane.measureLine(longestLine); } return this.longestLineWidth; } getGutterWidth() { if (this.canUseTextPlane()) { return ( this.textPlane.getGutterWidth(this.props.total_row_count) + this.paddingLeft ); } else { return null; } } canUseTextPlane() { return this.textPlane && this.textPlane.isReady(); } } TextEditor.contextTypes = { theme: PropTypes.object }; module.exports = TextEditor; ================================================ FILE: xray_ui/lib/text_editor/text_plane.js ================================================ const React = require("react"); const PropTypes = require("prop-types"); const { styled } = require("styletron-react"); const $ = React.createElement; class TextPlane extends React.Component { constructor(props) { super(props); this.handleCanvas = this.handleCanvas.bind(this); } render() { return $("canvas", { ref: this.handleCanvas, className: this.props.className, width: this.props.width * window.devicePixelRatio, height: this.props.height * window.devicePixelRatio, style: { width: this.props.width + "px", height: this.props.height + "px" } }); } handleCanvas(canvas) { this.canvas = canvas; } async componentDidUpdate() { if (this.canvas == null) return; const { userColors } = this.context.theme; const { fontFamily, fontSize, backgroundColor, baseTextColor } = this.context.theme.editor; const cursorColors = userColors; const selectionColors = cursorColors.map(color => { color = Object.assign({}, color); color.a = 0.5; return color; }); const computedLineHeight = this.props.lineHeight; if (!this.gl) { this.gl = this.canvas.getContext("webgl2"); this.renderer = new Renderer(this.gl, { fontFamily, fontSize, backgroundColor, baseTextColor, computedLineHeight, dpiScale: window.devicePixelRatio }); } this.renderer.draw({ canvasWidth: this.props.width * window.devicePixelRatio, canvasHeight: this.props.height * window.devicePixelRatio, scrollTop: this.props.scrollTop, scrollLeft: this.props.scrollLeft, paddingLeft: this.props.paddingLeft * window.devicePixelRatio || 0, firstVisibleRow: this.props.firstVisibleRow, totalRowCount: this.props.totalRowCount, lines: this.props.lines, selections: this.props.selections, showLocalCursors: this.props.showLocalCursors, selectionColors, cursorColors, computedLineHeight }); } measureLine(line, column = line.length) { column = Math.min(line.length, column); const glyphWidths = this.layoutLine(line); let x = 0; for (let i = 0; i < column; i++) { x += glyphWidths[i]; } return x; } layoutLine(line) { return this.renderer .layoutLine(line) .map(width => width / window.devicePixelRatio); } getGutterWidth(totalRowCount) { return ( this.renderer.getGutterWidth(totalRowCount) / window.devicePixelRatio ); } isReady() { return this.renderer != null; } } TextPlane.contextTypes = { theme: PropTypes.object }; module.exports = TextPlane; const shaders = require("./shaders"); const UNIT_QUAD_VERTICES = new Float32Array([1, 1, 1, 0, 0, 0, 0, 1]); const UNIT_QUAD_ELEMENT_INDICES = new Uint8Array([0, 1, 3, 1, 2, 3]); const MAX_INSTANCES = 1 << 16; const GLYPH_INSTANCE_SIZE = 12; const GLYPH_INSTANCE_SIZE_IN_BYTES = GLYPH_INSTANCE_SIZE * Float32Array.BYTES_PER_ELEMENT; const SOLID_INSTANCE_SIZE = 8; const SOLID_INSTANCE_SIZE_IN_BYTES = SOLID_INSTANCE_SIZE * Float32Array.BYTES_PER_ELEMENT; const SUBPIXEL_DIVISOR = 4; class Renderer { constructor(gl, style) { this.gl = gl; this.gl.enable(this.gl.BLEND); this.atlas = new Atlas(gl, style); this.style = style; const textBlendVertexShader = this.createShader( shaders.textBlendVertex, this.gl.VERTEX_SHADER ); const textBlendPass1FragmentShader = this.createShader( shaders.textBlendPass1Fragment, this.gl.FRAGMENT_SHADER ); const textBlendPass2FragmentShader = this.createShader( shaders.textBlendPass2Fragment, this.gl.FRAGMENT_SHADER ); const solidVertexShader = this.createShader( shaders.solidVertex, this.gl.VERTEX_SHADER ); const solidFragmentShader = this.createShader( shaders.solidFragment, this.gl.FRAGMENT_SHADER ); this.textBlendPass1Program = this.createProgram( textBlendVertexShader, textBlendPass1FragmentShader ); this.textBlendPass2Program = this.createProgram( textBlendVertexShader, textBlendPass2FragmentShader ); this.solidProgram = this.createProgram( solidVertexShader, solidFragmentShader ); this.textBlendPass1ViewportScaleLocation = this.gl.getUniformLocation( this.textBlendPass1Program, "viewportScale" ); this.textBlendPass1ScrollLeftLocation = this.gl.getUniformLocation( this.textBlendPass1Program, "scrollLeft" ); this.textBlendPass2ViewportScaleLocation = this.gl.getUniformLocation( this.textBlendPass2Program, "viewportScale" ); this.textBlendPass2ScrollLeftLocation = this.gl.getUniformLocation( this.textBlendPass2Program, "scrollLeft" ); this.solidViewportScaleLocation = this.gl.getUniformLocation( this.solidProgram, "viewportScale" ); this.solidScrollLeftLocation = this.gl.getUniformLocation( this.solidProgram, "scrollLeft" ); this.createBuffers(); this.textBlendVAO = this.createTextBlendVAO(); this.solidVAO = this.createSolidVAO(); } createBuffers() { this.unitQuadVerticesBuffer = this.gl.createBuffer(); this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.unitQuadVerticesBuffer); this.gl.bufferData( this.gl.ARRAY_BUFFER, UNIT_QUAD_VERTICES, this.gl.STATIC_DRAW ); this.unitQuadElementIndicesBuffer = this.gl.createBuffer(); this.gl.bindBuffer( this.gl.ELEMENT_ARRAY_BUFFER, this.unitQuadElementIndicesBuffer ); this.gl.bufferData( this.gl.ELEMENT_ARRAY_BUFFER, UNIT_QUAD_ELEMENT_INDICES, this.gl.STATIC_DRAW ); this.lineGlyphInstances = new Float32Array( MAX_INSTANCES * GLYPH_INSTANCE_SIZE ); this.gutterGlyphInstances = new Float32Array( MAX_INSTANCES * GLYPH_INSTANCE_SIZE ); this.glyphInstancesBuffer = this.gl.createBuffer(); this.selectionSolidInstances = new Float32Array( MAX_INSTANCES * SOLID_INSTANCE_SIZE ); this.cursorSolidInstances = new Float32Array( MAX_INSTANCES * SOLID_INSTANCE_SIZE ); this.opaqueRectangleInstances = new Float32Array(1 * SOLID_INSTANCE_SIZE); this.solidInstancesBuffer = this.gl.createBuffer(); } createTextBlendVAO() { const vao = this.gl.createVertexArray(); this.gl.bindVertexArray(vao); this.gl.bindBuffer( this.gl.ELEMENT_ARRAY_BUFFER, this.unitQuadElementIndicesBuffer ); this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.unitQuadVerticesBuffer); this.gl.enableVertexAttribArray(shaders.textBlendAttributes.unitQuadVertex); this.gl.vertexAttribPointer( shaders.textBlendAttributes.unitQuadVertex, 2, this.gl.FLOAT, false, 0, 0 ); this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.glyphInstancesBuffer); this.gl.enableVertexAttribArray(shaders.textBlendAttributes.targetOrigin); this.gl.vertexAttribPointer( shaders.textBlendAttributes.targetOrigin, 2, this.gl.FLOAT, false, GLYPH_INSTANCE_SIZE_IN_BYTES, 0 ); this.gl.vertexAttribDivisor(shaders.textBlendAttributes.targetOrigin, 1); this.gl.enableVertexAttribArray(shaders.textBlendAttributes.targetSize); this.gl.vertexAttribPointer( shaders.textBlendAttributes.targetSize, 2, this.gl.FLOAT, false, GLYPH_INSTANCE_SIZE_IN_BYTES, 2 * Float32Array.BYTES_PER_ELEMENT ); this.gl.vertexAttribDivisor(shaders.textBlendAttributes.targetSize, 1); this.gl.enableVertexAttribArray(shaders.textBlendAttributes.textColorRGBA); this.gl.vertexAttribPointer( shaders.textBlendAttributes.textColorRGBA, 4, this.gl.FLOAT, false, GLYPH_INSTANCE_SIZE_IN_BYTES, 4 * Float32Array.BYTES_PER_ELEMENT ); this.gl.vertexAttribDivisor(shaders.textBlendAttributes.textColorRGBA, 1); this.gl.enableVertexAttribArray(shaders.textBlendAttributes.atlasOrigin); this.gl.vertexAttribPointer( shaders.textBlendAttributes.atlasOrigin, 2, this.gl.FLOAT, false, GLYPH_INSTANCE_SIZE_IN_BYTES, 8 * Float32Array.BYTES_PER_ELEMENT ); this.gl.vertexAttribDivisor(shaders.textBlendAttributes.atlasOrigin, 1); this.gl.enableVertexAttribArray(shaders.textBlendAttributes.atlasSize); this.gl.vertexAttribPointer( shaders.textBlendAttributes.atlasSize, 2, this.gl.FLOAT, false, GLYPH_INSTANCE_SIZE_IN_BYTES, 10 * Float32Array.BYTES_PER_ELEMENT ); this.gl.vertexAttribDivisor(shaders.textBlendAttributes.atlasSize, 1); return vao; } createSolidVAO() { const vao = this.gl.createVertexArray(); this.gl.bindVertexArray(vao); this.gl.bindBuffer( this.gl.ELEMENT_ARRAY_BUFFER, this.unitQuadElementIndicesBuffer ); this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.unitQuadVerticesBuffer); this.gl.enableVertexAttribArray(shaders.solidAttributes.unitQuadVertex); this.gl.vertexAttribPointer( shaders.solidAttributes.unitQuadVertex, 2, this.gl.FLOAT, false, 0, 0 ); this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.solidInstancesBuffer); this.gl.enableVertexAttribArray(shaders.solidAttributes.targetOrigin); this.gl.vertexAttribPointer( shaders.solidAttributes.targetOrigin, 2, this.gl.FLOAT, false, SOLID_INSTANCE_SIZE_IN_BYTES, 0 ); this.gl.vertexAttribDivisor(shaders.solidAttributes.targetOrigin, 1); this.gl.enableVertexAttribArray(shaders.solidAttributes.targetSize); this.gl.vertexAttribPointer( shaders.solidAttributes.targetSize, 2, this.gl.FLOAT, false, SOLID_INSTANCE_SIZE_IN_BYTES, 2 * Float32Array.BYTES_PER_ELEMENT ); this.gl.vertexAttribDivisor(shaders.solidAttributes.targetSize, 1); this.gl.enableVertexAttribArray(shaders.solidAttributes.colorRGBA); this.gl.vertexAttribPointer( shaders.solidAttributes.colorRGBA, 4, this.gl.FLOAT, false, SOLID_INSTANCE_SIZE_IN_BYTES, 4 * Float32Array.BYTES_PER_ELEMENT ); this.gl.vertexAttribDivisor(shaders.solidAttributes.colorRGBA, 1); return vao; } layoutLine(line) { let x = 0; const glyphWidths = new Array(line.length); for (let i = 0; i < line.length; i++) { const variantIndex = Math.round(x * SUBPIXEL_DIVISOR) % SUBPIXEL_DIVISOR; const glyph = this.atlas.getGlyph(line[i], variantIndex); glyphWidths[i] = glyph.subpixelWidth; x += glyph.subpixelWidth; } return glyphWidths; } draw({ canvasHeight, canvasWidth, scrollTop, scrollLeft, paddingLeft, firstVisibleRow, totalRowCount, lines, selections, showLocalCursors, selectionColors, cursorColors }) { const { dpiScale } = this.style; const viewportScaleX = 2 / canvasWidth; const viewportScaleY = -2 / canvasHeight; scrollLeft = Math.round(scrollLeft * dpiScale); const textColor = { r: 0, g: 0, b: 0, a: 255 }; const cursorWidth = 2; const gutterWidth = this.getGutterWidth(totalRowCount); const xPositions = new Map(); for (let i = 0; i < selections.length; i++) { const { start, end } = selections[i]; xPositions.set(keyForPoint(start), 0); xPositions.set(keyForPoint(end), 0); } const gutterGlyphCount = this.populateGutterGlyphInstances( scrollTop, firstVisibleRow, firstVisibleRow + lines.length, totalRowCount, textColor ); const lineGlyphCount = this.populateLineGlyphInstances( scrollTop, firstVisibleRow, gutterWidth + paddingLeft, lines, selections, textColor, xPositions ); const { selectionSolidCount, cursorSolidCount } = this.populateSelectionSolidInstances( scrollTop, canvasWidth, gutterWidth + paddingLeft, selections, xPositions, selectionColors, cursorColors, cursorWidth, showLocalCursors ); this.atlas.uploadTexture(); this.gl.clearColor(1, 1, 1, 1); this.gl.clear(this.gl.COLOR_BUFFER_BIT); this.gl.viewport(0, 0, canvasWidth, canvasHeight); this.drawSelections( selectionSolidCount, scrollLeft, viewportScaleX, viewportScaleY ); this.drawText( this.lineGlyphInstances, lineGlyphCount, scrollLeft, viewportScaleX, viewportScaleY ); this.drawCursors( cursorSolidCount, scrollLeft, viewportScaleX, viewportScaleY ); this.clearRectangle( 0, 0, gutterWidth, canvasHeight, viewportScaleX, viewportScaleY ); this.drawText( this.gutterGlyphInstances, gutterGlyphCount, 0, viewportScaleX, viewportScaleY ); } clearRectangle(x, y, width, height, viewportScaleX, viewportScaleY) { this.gl.bindVertexArray(this.solidVAO); this.gl.disable(this.gl.BLEND); this.gl.useProgram(this.solidProgram); this.gl.uniform2f( this.solidViewportScaleLocation, viewportScaleX, viewportScaleY ); this.gl.uniform1f(this.solidScrollLeftLocation, 0); this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.solidInstancesBuffer); this.updateSolidInstance( this.opaqueRectangleInstances, 0, x, y, width, height, { r: 255, g: 255, b: 255, a: 1 } ); this.gl.bufferData( this.gl.ARRAY_BUFFER, this.opaqueRectangleInstances, this.gl.STREAM_DRAW ); this.gl.drawElementsInstanced( this.gl.TRIANGLES, 6, this.gl.UNSIGNED_BYTE, 0, 1 ); } drawSelections( selectionSolidCount, scrollLeft, viewportScaleX, viewportScaleY ) { this.gl.bindVertexArray(this.solidVAO); this.gl.enable(this.gl.BLEND); this.gl.useProgram(this.solidProgram); this.gl.uniform2f( this.solidViewportScaleLocation, viewportScaleX, viewportScaleY ); this.gl.uniform1f(this.solidScrollLeftLocation, scrollLeft); this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.solidInstancesBuffer); this.gl.bufferData( this.gl.ARRAY_BUFFER, this.selectionSolidInstances, this.gl.STREAM_DRAW ); this.gl.blendFuncSeparate( this.gl.SRC_ALPHA, this.gl.ONE_MINUS_SRC_ALPHA, this.gl.ONE, this.gl.ONE ); this.gl.drawElementsInstanced( this.gl.TRIANGLES, 6, this.gl.UNSIGNED_BYTE, 0, selectionSolidCount ); } drawText( glyphInstances, glyphCount, scrollLeft, viewportScaleX, viewportScaleY ) { this.gl.bindVertexArray(this.textBlendVAO); this.gl.enable(this.gl.BLEND); this.gl.useProgram(this.textBlendPass1Program); this.gl.uniform2f( this.textBlendPass1ViewportScaleLocation, viewportScaleX, viewportScaleY ); this.gl.uniform1f(this.textBlendPass1ScrollLeftLocation, scrollLeft); this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.glyphInstancesBuffer); this.gl.bufferData( this.gl.ARRAY_BUFFER, glyphInstances, this.gl.STREAM_DRAW ); this.gl.blendFuncSeparate( this.gl.ZERO, this.gl.ONE_MINUS_SRC_COLOR, this.gl.ZERO, this.gl.ONE ); this.gl.drawElementsInstanced( this.gl.TRIANGLES, 6, this.gl.UNSIGNED_BYTE, 0, glyphCount ); this.gl.useProgram(this.textBlendPass2Program); this.gl.blendFuncSeparate( this.gl.ONE, this.gl.ONE, this.gl.ZERO, this.gl.ONE ); this.gl.uniform2f( this.textBlendPass2ViewportScaleLocation, viewportScaleX, viewportScaleY ); this.gl.uniform1f(this.textBlendPass2ScrollLeftLocation, scrollLeft); this.gl.drawElementsInstanced( this.gl.TRIANGLES, 6, this.gl.UNSIGNED_BYTE, 0, glyphCount ); } drawCursors(cursorSolidCount, scrollLeft, viewportScaleX, viewportScaleY) { this.gl.bindVertexArray(this.solidVAO); this.gl.disable(this.gl.BLEND); this.gl.useProgram(this.solidProgram); this.gl.uniform2f( this.solidViewportScaleLocation, viewportScaleX, viewportScaleY ); this.gl.uniform1f(this.solidScrollLeftLocation, scrollLeft); this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.solidInstancesBuffer); this.gl.bufferData( this.gl.ARRAY_BUFFER, this.cursorSolidInstances, this.gl.STREAM_DRAW ); this.gl.drawElementsInstanced( this.gl.TRIANGLES, 6, this.gl.UNSIGNED_BYTE, 0, cursorSolidCount ); } populateGutterGlyphInstances( scrollTop, firstVisibleRow, lastVisibleRow, totalRowCount, textColor ) { const firstVisibleRowY = firstVisibleRow * this.style.computedLineHeight; let glyphCount = 0; let y = Math.round((firstVisibleRowY - scrollTop) * this.style.dpiScale); for (let row = firstVisibleRow; row < lastVisibleRow; row++) { const text = (row + 1).toString(); let x = 0; for (let i = 0; i < text.length; i++) { const char = text[i]; const variantIndex = Math.round(x * SUBPIXEL_DIVISOR) % SUBPIXEL_DIVISOR; const glyph = this.atlas.getGlyph(char, variantIndex); this.updateGlyphInstance( this.gutterGlyphInstances, glyphCount++, Math.round(x - glyph.variantOffset), y, glyph, textColor ); x += glyph.subpixelWidth; } y += Math.round(this.style.computedLineHeight * this.style.dpiScale); } if (glyphCount > MAX_INSTANCES) { console.error( `glyphCount of ${glyphCount} exceeds MAX_INSTANCES of ${MAX_INSTANCES}` ); } return glyphCount; } populateLineGlyphInstances( scrollTop, firstVisibleRow, paddingLeft, lines, selections, textColor, xPositions ) { const firstVisibleRowY = firstVisibleRow * this.style.computedLineHeight; let glyphCount = 0; let selectionIndex = 0; let y = Math.round((firstVisibleRowY - scrollTop) * this.style.dpiScale); const position = {}; for (var i = 0; i < lines.length; i++) { position.row = firstVisibleRow + i; let x = paddingLeft; const line = lines[i]; for ( position.column = 0; position.column <= line.length; position.column++ ) { { const key = keyForPoint(position); if (xPositions.has(key)) xPositions.set(key, x); } if (position.column < line.length) { const char = line[position.column]; const variantIndex = Math.round(x * SUBPIXEL_DIVISOR) % SUBPIXEL_DIVISOR; const glyph = this.atlas.getGlyph(char, variantIndex); this.updateGlyphInstance( this.lineGlyphInstances, glyphCount++, Math.round(x - glyph.variantOffset), y, glyph, textColor ); x += glyph.subpixelWidth; } } y += Math.round(this.style.computedLineHeight * this.style.dpiScale); } if (glyphCount > MAX_INSTANCES) { console.error( `glyphCount of ${glyphCount} exceeds MAX_INSTANCES of ${MAX_INSTANCES}` ); } return glyphCount; } updateGlyphInstance(glyphInstances, i, x, y, glyph, color) { const startOffset = 12 * i; // targetOrigin glyphInstances[0 + startOffset] = x; glyphInstances[1 + startOffset] = y; // targetSize glyphInstances[2 + startOffset] = glyph.width; glyphInstances[3 + startOffset] = glyph.height; // textColorRGBA glyphInstances[4 + startOffset] = color.r; glyphInstances[5 + startOffset] = color.g; glyphInstances[6 + startOffset] = color.b; glyphInstances[7 + startOffset] = color.a; // atlasOrigin glyphInstances[8 + startOffset] = glyph.textureU; glyphInstances[9 + startOffset] = glyph.textureV; // atlasSize glyphInstances[10 + startOffset] = glyph.textureWidth; glyphInstances[11 + startOffset] = glyph.textureHeight; } populateSelectionSolidInstances( scrollTop, canvasWidth, paddingLeft, selections, xPositions, selectionColors, cursorColors, cursorWidth, showLocalCursors ) { const { dpiScale, computedLineHeight } = this.style; let selectionSolidCount = 0; let cursorSolidCount = 0; for (var i = 0; i < selections.length; i++) { const selection = selections[i]; const colorIndex = selection.user_id % selectionColors.length; const selectionColor = selectionColors[colorIndex]; const cursorColor = cursorColors[colorIndex]; if (comparePoints(selection.start, selection.end) !== 0) { const rowSpan = selection.end.row - selection.start.row; const startX = xPositions.get(keyForPoint(selection.start)); const endX = xPositions.get(keyForPoint(selection.end)); if (rowSpan === 0) { this.updateSolidInstance( this.selectionSolidInstances, selectionSolidCount++, Math.round(startX), yForRow(selection.start.row), Math.round(endX - startX), yForRow(selection.start.row + 1) - yForRow(selection.start.row), selectionColor ); } else { // First line of selection this.updateSolidInstance( this.selectionSolidInstances, selectionSolidCount++, Math.round(startX), yForRow(selection.start.row), Math.round(canvasWidth - startX), yForRow(selection.start.row + 1) - yForRow(selection.start.row), selectionColor ); // Lines entirely spanned by selection if (rowSpan > 1) { this.updateSolidInstance( this.selectionSolidInstances, selectionSolidCount++, paddingLeft, yForRow(selection.start.row + 1), Math.round(canvasWidth), yForRow(selection.end.row) - yForRow(selection.start.row + 1), selectionColor ); } // Last line of selection this.updateSolidInstance( this.selectionSolidInstances, selectionSolidCount++, paddingLeft, yForRow(selection.end.row), Math.round(endX - paddingLeft), yForRow(selection.end.row + 1) - yForRow(selection.end.row), selectionColor ); } } if (showLocalCursors || selection.remote) { const cursorPoint = selection.reversed ? selection.start : selection.end; const startX = xPositions.get(keyForPoint(cursorPoint)); const endX = startX + cursorWidth; this.updateSolidInstance( this.cursorSolidInstances, cursorSolidCount++, Math.round(startX), yForRow(cursorPoint.row), Math.round(endX - startX), yForRow(cursorPoint.row + 1) - yForRow(cursorPoint.row), cursorColor ); } } function yForRow(row) { return Math.round((row * computedLineHeight - scrollTop) * dpiScale); } if (selectionSolidCount > MAX_INSTANCES) { console.error( `selectionSolidCount of ${selectionSolidCount} exceeds MAX_INSTANCES of ${MAX_INSTANCES}` ); } if (cursorSolidCount > MAX_INSTANCES) { console.error( `cursorSolidCount of ${cursorSolidCount} exceeds MAX_INSTANCES of ${MAX_INSTANCES}` ); } return { selectionSolidCount, cursorSolidCount }; } updateSolidInstance(arrayBuffer, i, x, y, width, height, color) { const startOffset = 8 * i; // targetOrigin arrayBuffer[0 + startOffset] = x; arrayBuffer[1 + startOffset] = y; // targetSize arrayBuffer[2 + startOffset] = width; arrayBuffer[3 + startOffset] = height; // colorRGBA arrayBuffer[4 + startOffset] = color.r; arrayBuffer[5 + startOffset] = color.g; arrayBuffer[6 + startOffset] = color.b; arrayBuffer[7 + startOffset] = color.a; } getGutterWidth(totalRowCount) { const digitsCount = Math.floor(Math.log10(totalRowCount)) + 1; return Math.ceil(digitsCount * this.atlas.getGlyph("9", 0).subpixelWidth); } createProgram(vertexShader, fragmentShader) { const program = this.gl.createProgram(); this.gl.attachShader(program, vertexShader); this.gl.attachShader(program, fragmentShader); this.gl.linkProgram(program); if (!this.gl.getProgramParameter(program, this.gl.LINK_STATUS)) { var info = this.gl.getProgramInfoLog(program); throw "Could not compile WebGL program: \n\n" + info; } return program; } createShader(source, type) { const shader = this.gl.createShader(type); this.gl.shaderSource(shader, source); this.gl.compileShader(shader); if (!this.gl.getShaderParameter(shader, this.gl.COMPILE_STATUS)) { var info = this.gl.getShaderInfoLog(shader); throw "Could not compile WebGL program: \n\n" + info; } return shader; } } class Atlas { constructor(gl, style) { this.textureSize = 512 * style.dpiScale; this.uvScale = 1 / this.textureSize; this.style = style; this.glyphPadding = 2; this.nextX = 0; this.nextY = 0; this.glyphs = new Map(); this.gl = gl; this.glyphCanvas = document.createElement("canvas"); this.glyphCanvas.width = this.textureSize; this.glyphCanvas.height = this.textureSize; this.glyphCtx = this.glyphCanvas.getContext("2d", { alpha: false }); this.glyphCtx.fillStyle = "white"; this.glyphCtx.fillRect( 0, 0, this.glyphCanvas.width, this.glyphCanvas.height ); this.glyphCtx.font = `${this.style.fontSize}px ${this.style.fontFamily}`; this.glyphCtx.fillStyle = "black"; this.glyphCtx.textBaseline = "bottom"; this.glyphCtx.scale(style.dpiScale, style.dpiScale); this.shouldUploadTexture = false; this.texture = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, this.texture); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); gl.texImage2D( gl.TEXTURE_2D, 0, gl.RGBA, this.textureSize, this.textureSize, 0, gl.RGBA, gl.UNSIGNED_BYTE, this.glyphCanvas ); // document.body.appendChild(this.glyphCanvas) // this.glyphCanvas.style.position = 'absolute' // this.glyphCanvas.style.top = 0 // this.glyphCanvas.style.right = 0 } getGlyph(text, variantIndex) { let glyphVariants = this.glyphs.get(text); if (!glyphVariants) { glyphVariants = new Map(); this.glyphs.set(text, glyphVariants); } let glyph = glyphVariants.get(variantIndex); if (!glyph) { glyph = this.rasterizeGlyph(text, variantIndex); glyphVariants.set(variantIndex, glyph); } return glyph; } rasterizeGlyph(text, variantIndex) { this.shouldUploadTexture = true; const { dpiScale, computedLineHeight } = this.style; const variantOffset = variantIndex / SUBPIXEL_DIVISOR; const height = computedLineHeight; const { width: subpixelWidth } = this.glyphCtx.measureText(text); const width = Math.ceil(variantOffset) + Math.ceil(subpixelWidth); if ((this.nextX + width) * dpiScale > this.textureSize) { this.nextX = 0; this.nextY = Math.ceil(this.nextY + height + this.glyphPadding); } if ((this.nextY + height) * dpiScale > this.textureSize) { throw new Error("Texture is too small"); } const x = this.nextX; const y = this.nextY; this.glyphCtx.fillText(text, x + variantOffset, y + height); this.nextX += width; return { textureU: x * dpiScale * this.uvScale, textureV: y * dpiScale * this.uvScale, textureWidth: width * dpiScale * this.uvScale, textureHeight: height * dpiScale * this.uvScale, width: width * dpiScale, height: height * dpiScale, subpixelWidth: subpixelWidth * dpiScale, variantOffset }; } uploadTexture() { if (this.shouldUploadTexture) { this.gl.texImage2D( this.gl.TEXTURE_2D, 0, this.gl.RGBA, this.textureSize, this.textureSize, 0, this.gl.RGBA, this.gl.UNSIGNED_BYTE, this.glyphCanvas ); this.shouldUploadTexture = false; } } } function comparePoints(a, b) { return a.row - b.row || a.column - b.column; } function keyForPoint(point) { return `${point.row}.${point.column}`; } ================================================ FILE: xray_ui/lib/theme_provider.js ================================================ const React = require("react"); const PropTypes = require("prop-types"); class ThemeProvider extends React.Component { render() { return this.props.children } getChildContext() { return { theme: this.props.theme }; } } ThemeProvider.childContextTypes = { theme: PropTypes.object }; module.exports = ThemeProvider; ================================================ FILE: xray_ui/lib/view.js ================================================ const propTypes = require("prop-types"); const React = require("react"); const $ = React.createElement; const ViewRegistry = require("./view_registry"); class View extends React.Component { constructor(props) { super(props); this.dispatchAction = this.dispatchAction.bind(this); this.state = { version: 0, viewId: props.id }; } componentWillReceiveProps(props, context) { const { viewRegistry } = context; if (this.state.viewId !== props.id) { this.setState({ viewId: props.id }); this.watch(props, context); } } componentDidMount() { this.watch(this.props, this.context); } componentWillUnmount() { if (this.disposePropsWatch) this.disposePropsWatch(); if (this.disposeFocusWatch) this.disposeFocusWatch(); } render() { const { viewRegistry } = this.context; const { id } = this.props; const component = viewRegistry.getComponent(id); const props = viewRegistry.getProps(id); return $( component, Object.assign({}, props, { ref: component => (this.component = component), dispatch: this.dispatchAction, key: id }) ); } dispatchAction(action) { const { viewRegistry } = this.context; const { id } = this.props; viewRegistry.dispatchAction(id, action); } watch(props, context) { if (this.disposePropsWatch) this.disposePropsWatch(); if (this.disposeFocusWatch) this.disposeFocusWatch(); this.disposePropsWatch = context.viewRegistry.watchProps(props.id, () => { this.setState({ version: this.state.version + 1 }); }); this.disposeFocusWatch = context.viewRegistry.watchFocus(props.id, () => { if (this.component.focus) this.component.focus(); }); } getChildContext() { return { dispatchAction: this.dispatchAction }; } } View.childContextTypes = { dispatchAction: propTypes.func }; View.contextTypes = { viewRegistry: propTypes.instanceOf(ViewRegistry) }; module.exports = View; ================================================ FILE: xray_ui/lib/view_registry.js ================================================ const assert = require("assert"); module.exports = class ViewRegistry { constructor({ onAction } = {}) { this.onAction = onAction; this.componentsByName = new Map(); this.viewsById = new Map(); this.propListenersByViewId = new Map(); this.focusListenersByViewId = new Map(); } addComponent(name, component) { assert(!this.componentsByName.has(name)); this.componentsByName.set(name, component); } getComponent(id) { const view = this.viewsById.get(id); assert(view); const component = this.componentsByName.get(view.component_name); assert(component); return component; } removeComponent(name) { this.componentsByName.delete(name); } update({ updated, removed, focused }) { for (let i = 0; i < updated.length; i++) { const view = updated[i]; this.viewsById.set(view.view_id, view); const listeners = this.propListenersByViewId.get(view.view_id); if (listeners) { for (let i = 0; i < listeners.length; i++) { listeners[i](); } } } for (var i = 0; i < removed.length; i++) { const viewId = removed[i]; this.viewsById.delete(viewId); this.propListenersByViewId.delete(viewId); } if (focused != null) { const focusListener = this.focusListenersByViewId.get(focused); if (focusListener) { this.pendingFocus = null; focusListener(); } else { this.pendingFocus = focused; } } } getProps(id) { const view = this.viewsById.get(id); assert(view); return view.props; } watchProps(id, callback) { assert(this.viewsById.has(id)); let listeners = this.propListenersByViewId.get(id); if (!listeners) { listeners = []; this.propListenersByViewId.set(id, listeners); } listeners.push(callback); return () => { const callbackIndex = listeners.indexOf(callback); if (callbackIndex !== -1) listeners.splice(callbackIndex, 1); }; } watchFocus(id, callback) { assert(!this.focusListenersByViewId.has(id)); this.focusListenersByViewId.set(id, callback); if (this.pendingFocus === id) { this.pendingFocus = null; callback(); } return () => this.focusListenersByViewId.delete(id); } dispatchAction(id, action) { assert(this.viewsById.has(id)); this.onAction({ view_id: id, action }); } }; ================================================ FILE: xray_ui/lib/workspace.js ================================================ const propTypes = require("prop-types"); const React = require("react"); const ReactDOM = require("react-dom"); const { styled } = require("styletron-react"); const Modal = require("./modal"); const View = require("./view"); const { ActionContext, Action } = require("./action_dispatcher"); const $ = React.createElement; const Root = styled("div", { position: "relative", width: "100%", height: "100%", padding: 0, margin: 0, display: "flex" }); const LeftPanel = styled("div", { width: "300px", height: "100%" }); const Pane = styled("div", { flex: 1, position: "relative" }); const PaneInner = styled("div", { position: "absolute", left: 0, top: 0, bottom: 0, right: 0 }); const BackgroundTip = styled("div", { fontFamily: "sans-serif", height: "100%", display: "flex", alignItems: "center", justifyContent: "center" }); class Workspace extends React.Component { constructor() { super(); } render() { let modal; if (this.props.modal) { modal = $(Modal, null, $(View, { id: this.props.modal })); } let leftPanel; if (this.props.left_panel) { leftPanel = $(LeftPanel, null, $(View, { id: this.props.left_panel })); } let centerItem; if (this.props.center_pane) { centerItem = $(View, { id: this.props.center_pane }); } else if (this.context.inBrowser) { centerItem = $(BackgroundTip, {}, "Press Ctrl-T to browse files"); } return $( ActionContext, { context: "Workspace" }, $( Root, { tabIndex: -1 }, leftPanel, $(Pane, null, $(PaneInner, null, centerItem)), modal, $(Action, { type: "ToggleFileFinder" }), $(Action, { type: "SaveActiveBuffer" }) ) ); } componentDidMount() { ReactDOM.findDOMNode(this).focus(); } } Workspace.contextTypes = { inBrowser: propTypes.bool }; module.exports = Workspace; ================================================ FILE: xray_ui/package.json ================================================ { "name": "xray_ui", "version": "0.0.0", "main": "lib/index.js", "license": "MIT", "scripts": { "test": "electron-mocha --ui=tdd --renderer test/**/*.test.js", "itest": "electron-mocha --ui=tdd --renderer --interactive test/**/*.test.js" }, "dependencies": { "prop-types": "^15.6.0", "react": "^16.3.0", "react-autosize-textarea": "^3.0.3", "react-component-octicons": "^1.6.0", "react-dom": "^16.3.0", "styletron-engine-atomic": "1.0.4", "styletron-react": "4.0.3" }, "resolutions": { "styletron-react/styletron-react-core": "1.0.0" }, "devDependencies": { "electron": "2.0.0-beta.7", "electron-mocha": "^6.0.1", "enzyme": "^3.3.0", "enzyme-adapter-react-16": "^1.1.1" } } ================================================ FILE: xray_ui/test/action_dispatcher.test.js ================================================ const assert = require("assert"); const propTypes = require("prop-types"); const React = require("react"); const { mount } = require("./helpers/component_helpers"); const $ = React.createElement; const { ActionDispatcher, ActionContext, Action, keystrokeStringForEvent } = require("../lib/action_dispatcher"); suite("ActionDispatcher", () => { test("dispatching an action via a keystroke", () => { const Component = props => $( ActionDispatcher, { keyBindings: props.keyBindings }, $( "div", null, $( ActionContext, { add: ["a", "b"] }, $(Action, { type: "Action1" }), $(Action, { type: "Action2" }), $(Action, { type: "Action3" }), $( "div", null, $( ActionContext, { add: ["c"], remove: ["a"] }, $(Action, { type: "Action4" }), $("div", { id: "target" }) ) ) ) ) ); let dispatchedActions; const keyBindings = [ { key: "ctrl-a", context: "a b", action: "Action1" }, { key: "ctrl-a", context: "b c", action: "Action4" }, { key: "ctrl-b", context: "a b", action: "Action2" }, { key: "ctrl-c", context: "a b", action: "UnregisteredAction" } ]; const component = mount($(Component, { keyBindings }), { context: { dispatchAction: action => dispatchedActions.push(action.type) }, childContextTypes: { dispatchAction: propTypes.func } }); const target = component.find("#target"); // Dispatch action when finding the first context/keybinding that match the event... dispatchedActions = []; target.simulate("keyDown", { ctrlKey: true, key: "a" }); assert.deepEqual(dispatchedActions, ["Action4"]); // ...and walk up the DOM until a matching context is found. dispatchedActions = []; target.simulate("keyDown", { ctrlKey: true, key: "b" }); assert.deepEqual(dispatchedActions, ["Action2"]); // Override a previous keybinding by specifying it later in the list. dispatchedActions = []; keyBindings.push({ key: "ctrl-b", context: "a b", action: "Action3" }); target.simulate("keyDown", { ctrlKey: true, key: "b" }); assert.deepEqual(dispatchedActions, ["Action3"]); // Simulate a keystroke that matches a context/keybinding but that maps to an unknown action. dispatchedActions = []; target.simulate("keyDown", { ctrlKey: true, key: "c" }); assert.deepEqual(dispatchedActions, []); }); test("pre-dispatch hook", () => { let preDispatchHooks = 0; const Component = props => $( ActionDispatcher, { keyBindings: props.keyBindings }, $( "div", null, $( ActionContext, { add: ["some-context"] }, $(Action, { onWillDispatch: () => preDispatchHooks++, type: "Action" }), $("div", { id: "target" }) ) ) ); const component = mount( $(Component, { keyBindings: [{ key: "a", context: "some-context", action: "Action" }] }), { context: { dispatchAction: () => {} }, childContextTypes: { dispatchAction: propTypes.func } } ); component.find("#target").simulate("keydown", { key: "a" }); assert.equal(preDispatchHooks, 1); }); test("keystrokeStringForEvent", () => { assert.equal(keystrokeStringForEvent({ key: "a" }), "a"); assert.equal(keystrokeStringForEvent({ key: "Backspace" }), "backspace"); assert.equal(keystrokeStringForEvent({ key: "ArrowUp" }), "up"); assert.equal(keystrokeStringForEvent({ key: "ArrowDown" }), "down"); assert.equal(keystrokeStringForEvent({ key: "ArrowLeft" }), "left"); assert.equal(keystrokeStringForEvent({ key: "ArrowRight" }), "right"); assert.equal( keystrokeStringForEvent({ ctrlKey: true, key: "s" }), "ctrl-s" ); assert.equal( keystrokeStringForEvent({ ctrlKey: true, altKey: true, key: "s" }), "ctrl-alt-s" ); assert.equal( keystrokeStringForEvent({ ctrlKey: true, altKey: true, metaKey: true, key: "s" }), "ctrl-alt-cmd-s" ); assert.equal( keystrokeStringForEvent({ ctrlKey: true, altKey: true, metaKey: true, shiftKey: true, key: "s" }), "ctrl-alt-shift-cmd-s" ); }); }); ================================================ FILE: xray_ui/test/file_finder.test.js ================================================ const assert = require("assert"); const {mount, setProps} = require("./helpers/component_helpers"); const FileFinder = require("../lib/file_finder"); const $ = require("react").createElement; suite("FileFinderView", () => { test("basic rendering", async () => { const fileFinder = mount($(FileFinder, { query: '', results: [] })); assert.equal(fileFinder.find("ol li").length, 0); await setProps(fileFinder, { query: 'ce', results: [ {display_path: 'succeed', score: 3, positions: [3, 4]}, {display_path: 'abcdef', score: 2, positions: [2, 4]}, ] }); assert.deepEqual( fileFinder.find("ol li").map(item => item.getDOMNode().innerHTML), [ 'succeed', 'abcdef' ] ) }); }); ================================================ FILE: xray_ui/test/helpers/component_helpers.js ================================================ const enzyme = require("enzyme"); const Adapter = require("enzyme-adapter-react-16"); enzyme.configure({ adapter: new Adapter() }); module.exports = { shallow(node, options) { return enzyme.shallow(node, addStyletronToContext(options)); }, mount(node, options) { return enzyme.mount(node, addStyletronToContext(options)); }, setProps(wrapper, props) { return new Promise(resolve => wrapper.setProps(props, resolve)); } }; function addStyletronToContext(options = {}) { options.context = Object.assign( { styletron: { renderStyle() {} } }, options.context ); options.childContextTypes = Object.assign( { styletron: function() {} }, options.childContextTypes ); return options; } ================================================ FILE: xray_ui/test/modal.test.js ================================================ const assert = require("assert"); const { mount } = require("./helpers/component_helpers"); const React = require("react"); const $ = React.createElement; const Modal = require("../lib/modal"); suite("Modal", () => { let attachedElements; beforeEach(() => { attachedElements = []; }); afterEach(() => { while ((element = attachedElements.pop())) { element.remove(); } }); test("closing dialog while it's focused", () => { const outerComponent = mount($(FocusableComponent), { attachTo: buildAndAttachElement("div") }); outerComponent.getDOMNode().focus(); assert.equal(document.activeElement, outerComponent.getDOMNode()); const modal = mount($(Modal, {}, $(FocusableComponent)), { attachTo: buildAndAttachElement("div") }); const innerComponent = modal.find(FocusableComponent); innerComponent.getDOMNode().focus(); assert.equal(document.activeElement, innerComponent.getDOMNode()); modal.unmount(); assert.equal(document.activeElement, outerComponent.getDOMNode()); }); test("closing dialog when it's not focused", () => { const outerComponent1 = mount($(FocusableComponent, { id: 1 }), { attachTo: buildAndAttachElement("div") }); const outerComponent2 = mount($(FocusableComponent, { id: 2 }), { attachTo: buildAndAttachElement("div") }); outerComponent2.getDOMNode().focus(); assert.equal(document.activeElement, outerComponent2.getDOMNode()); const modal = mount($(Modal, {}, $(FocusableComponent)), { attachTo: buildAndAttachElement("div") }); outerComponent1.getDOMNode().focus(); assert.equal(document.activeElement, outerComponent1.getDOMNode()); modal.unmount(); assert.equal(document.activeElement, outerComponent1.getDOMNode()); }); class FocusableComponent extends React.Component { render() { return $("div", { id: this.props.id, tabIndex: -1 }); } } function buildAndAttachElement(tagName) { const element = document.createElement(tagName); document.body.appendChild(element); attachedElements.push(element); return element; } }); ================================================ FILE: xray_ui/test/view.test.js ================================================ const assert = require("assert"); const React = require("react"); const $ = require("react").createElement; const { mount, shallow } = require("./helpers/component_helpers"); const View = require("../lib/view"); const ViewRegistry = require("../lib/view_registry"); suite("View", () => { test("basic rendering", () => { const viewRegistry = new ViewRegistry(); viewRegistry.addComponent("comp-1", props => $("div", {}, props.text)); viewRegistry.addComponent("comp-2", props => $("label", {}, props.text)); viewRegistry.update({ updated: [ { component_name: "comp-1", view_id: 1, props: { text: "text-1" } }, { component_name: "comp-2", view_id: 2, props: { text: "text-2" } } ], removed: [] }); // Initial rendering const view = shallow($(View, { id: 1 }), { context: { viewRegistry } }); assert.equal(view.html(), "
text-1
"); // Changing view id view.setProps({ id: 2 }); assert.equal(view.html(), ""); // Updating view props viewRegistry.update({ updated: [ { component_name: "comp-2", view_id: 2, props: { text: "text-3" } } ], removed: [] }); view.update(); assert.equal(view.html(), ""); }); test("action dispatching", () => { const actions = []; const viewRegistry = new ViewRegistry({ onAction: a => actions.push(a) }); viewRegistry.update({ updated: [{ component_name: "component", view_id: 42, props: {} }], removed: [] }); let dispatch; viewRegistry.addComponent("component", props => { dispatch = props.dispatch; return $("div"); }); const view = shallow($(View, { id: 42 }), { context: { viewRegistry } }); assert.equal(view.html(), "
"); dispatch({ type: "foo" }); dispatch({ type: "bar" }); assert.deepEqual(actions, [ { view_id: 42, action: { type: "foo" } }, { view_id: 42, action: { type: "bar" } } ]); }); test("focus", () => { const focusedViewIds = []; const viewRegistry = new ViewRegistry({ onAction: a => focusedViewIds.push(a.view_id) }); class TestComponent extends React.Component { render() { return $("div"); } focus() { this.props.dispatch({}); } } viewRegistry.addComponent("component", TestComponent); viewRegistry.update({ updated: [ { component_name: "component", view_id: 1, props: {} }, { component_name: "component", view_id: 2, props: {} } ], removed: [], focused: 1 }); mount($(View, { id: 1 }), { context: { viewRegistry } }); mount($(View, { id: 2 }), { context: { viewRegistry } }); assert.deepEqual(focusedViewIds, [1]); viewRegistry.update({ updated: [], removed: [], focused: 2 }); assert.deepEqual(focusedViewIds, [1, 2]); viewRegistry.update({ updated: [], removed: [], focused: 1 }); assert.deepEqual(focusedViewIds, [1, 2, 1]); }); }); ================================================ FILE: xray_ui/test/view_registry.test.js ================================================ const assert = require("assert"); const ViewRegistry = require("../lib/view_registry"); suite("ViewRegistry", () => { test("props", () => { const registry = new ViewRegistry(); // Adding initial views registry.update({ updated: [ { component_name: "component-1", view_id: 1, props: { a: 1 } }, { component_name: "component-2", view_id: 2, props: { b: 2 } } ], removed: [] }); assert.deepEqual(registry.getProps(1), { a: 1 }); assert.deepEqual(registry.getProps(2), { b: 2 }); assert.throws(() => registry.getProps(3)); const propChanges = []; const disposeProps1Watch = registry.watchProps(1, () => propChanges.push("component-1") ); const disposeProps2Watch = registry.watchProps(2, () => propChanges.push("component-2") ); assert.throws(() => registry.watchProps(3, () => {})); // Updating existing view, removing existing view, adding a new view registry.update({ updated: [ { component_name: "component-2", view_id: 2, props: { b: 3 } }, { component_name: "component-3", view_id: 3, props: { c: 4 } } ], removed: [1] }); assert.throws(() => registry.getProps(1)); assert.deepEqual(registry.getProps(2), { b: 3 }); assert.deepEqual(registry.getProps(3), { c: 4 }); assert.throws(() => registry.watchProps(1, () => {})); assert.deepEqual(propChanges, ["component-2"]); // Stop watching props for a view propChanges.length = 0; disposeProps2Watch(); disposeProps2Watch(); // ensure disposing is idempotent registry.update({ updated: [{ component_name: "component-2", view_id: 2, props: { b: 4 } }], removed: [] }); assert.deepEqual(propChanges, []); }); test("components", () => { const registry = new ViewRegistry(); registry.update({ updated: [ { component_name: "comp-1", view_id: 1, props: {} }, { component_name: "comp-2", view_id: 2, props: {} }, { component_name: "comp-3", view_id: 3, props: {} } ], removed: [] }); const comp1A = () => {}; const comp2A = () => {}; registry.addComponent("comp-1", comp1A); registry.addComponent("comp-2", comp2A); assert.equal(registry.getComponent(1), comp1A); assert.equal(registry.getComponent(2), comp2A); assert.throws(() => registry.getComponent(3)); registry.removeComponent("comp-1"); assert.throws(() => registry.getComponent(1)); assert.equal(registry.getComponent(2), comp2A); const comp1B = () => {}; const comp2B = () => {}; registry.addComponent("comp-1", comp1B); assert.throws(() => registry.addComponent("comp-2", comp2B)); assert.equal(registry.getComponent(1), comp1B); }); test("dispatching actions", () => { const actions = []; const registry = new ViewRegistry({ onAction: a => actions.push(a) }); registry.update({ updated: [ { component_name: "component-1", view_id: 1, props: {} }, { component_name: "component-2", view_id: 2, props: {} } ], removed: [] }); registry.dispatchAction(1, { a: 1, b: 2 }); registry.dispatchAction(2, { c: 3 }); assert.throws(() => registry.dispatchAction(3, { d: 4 })); assert.deepEqual(actions, [ { view_id: 1, action: { a: 1, b: 2 } }, { view_id: 2, action: { c: 3 } } ]); }); test("focus", () => { const registry = new ViewRegistry({ onAction: a => actions.push(a) }); const focusRequests = []; registry.update({ updated: [], removed: [], focused: 2 }); registry.update({ updated: [], removed: [], focused: 1 }); registry.update({ updated: [], removed: [], focused: 1 }); const disposeWatch1 = registry.watchFocus(1, () => focusRequests.push(1)); registry.watchFocus(2, () => focusRequests.push(2)); registry.update({ updated: [], removed: [], focused: 1 }); registry.update({ updated: [], removed: [], focused: 2 }); assert.deepEqual(focusRequests, [1, 1, 2]); assert.throws(() => registry.watchFocus(1)); disposeWatch1() registry.update({ updated: [], removed: [], focused: 1 }); registry.update({ updated: [], removed: [], focused: 2 }); assert.doesNotThrow(() => registry.watchFocus(1)); assert.deepEqual(focusRequests, [1, 1, 2, 2]); }); }); ================================================ FILE: xray_wasm/.gitignore ================================================ .cargo ================================================ FILE: xray_wasm/Cargo.toml ================================================ [package] name = "xray_wasm" version = "0.1.0" authors = ["Antonio Scandurra "] [lib] crate-type = ["cdylib"] [dependencies] bytes = "0.4" futures = "0.1" serde = "1.0" serde_derive = "1.0" serde_json = "1.0" wasm-bindgen = "0.2" xray_core = { path = "../xray_core" } [features] js-tests = [] ================================================ FILE: xray_wasm/lib/main.js ================================================ export let xray = import("../dist/xray_wasm"); export { JsSink } from "./support"; ================================================ FILE: xray_wasm/lib/support.js ================================================ export class JsSink { constructor({ send, close }) { if (send) this._send = send; if (close) this._close = close; } send(message) { if (this._send) this._send(message); } close() { if (this._close) this._close(); } } const promise = Promise.resolve(); export function notifyOnNextTick(notifyHandle) { promise.then(() => notifyHandle.notify_from_js_on_next_tick()); } ================================================ FILE: xray_wasm/package.json ================================================ { "name": "xray_wasm", "version": "0.0.0", "description": "Xray server packaged for use in JavaScript", "main": "lib/main.js", "scripts": { "test": "script/test" }, "repository": { "type": "git", "url": "git+https://github.com/atom/xray.git" }, "license": "MIT", "devDependencies": { "mocha": "^5.1.1", "source-map-support": "^0.5.4", "webpack": "^4.6.0", "webpack-cli": "^2.0.14" } } ================================================ FILE: xray_wasm/script/build ================================================ #!/usr/bin/env bash LOCAL_CRATE_PATH=./.cargo PATH=$LOCAL_CRATE_PATH/bin:$PATH WASM_BINDGEN_VERSION=0.2.33 setup_wasm_bindgen() { if (command -v wasm-bindgen) && $(wasm-bindgen --version | grep --silent $WASM_BINDGEN_VERSION); then echo 'Using existing installation of wasm-bindgen' else cargo install --force wasm-bindgen-cli --version $WASM_BINDGEN_VERSION --root $LOCAL_CRATE_PATH fi } rustup target add wasm32-unknown-unknown setup_wasm_bindgen mkdir -p dist CARGO_INCREMENTAL=0 RUSTFLAGS="-C debuginfo=0 -C opt-level=s -C lto -C panic=abort" cargo build --release --target wasm32-unknown-unknown wasm-bindgen ../target/wasm32-unknown-unknown/release/xray_wasm.wasm --out-dir dist ================================================ FILE: xray_wasm/script/test ================================================ #!/usr/bin/env bash LOCAL_CRATE_PATH=./.cargo PATH=$LOCAL_CRATE_PATH/bin:$PATH set -e rm -rf dist mkdir -p dist cargo build --release --target wasm32-unknown-unknown --features js-tests wasm-bindgen ../target/wasm32-unknown-unknown/release/xray_wasm.wasm --out-dir dist yarn install node_modules/.bin/webpack --target=node --mode=development --devtool="source-map" test/tests.js node_modules/.bin/mocha --require source-map-support/register --ui=tdd dist/main.js ================================================ FILE: xray_wasm/src/lib.rs ================================================ extern crate bytes; extern crate futures; extern crate wasm_bindgen; #[macro_use] extern crate xray_core; extern crate serde; #[macro_use] extern crate serde_derive; extern crate serde_json; use bytes::Bytes; use futures::executor::{self, Notify, Spawn}; use futures::unsync::mpsc; use futures::{future, stream}; use futures::{Async, AsyncSink, Future, Poll, Sink, Stream}; use std::cell::RefCell; use std::collections::HashMap; use std::io; use std::mem; use std::rc::{Rc, Weak}; use std::sync::Arc; use wasm_bindgen::prelude::*; use xray_core::app::Command; use xray_core::{cross_platform, App, ViewId, WindowId, WindowUpdate}; #[derive(Serialize, Debug)] #[serde(tag = "type")] enum OutgoingMessage { OpenWindow { window_id: WindowId }, UpdateWindow(WindowUpdate), Error { description: String }, } #[derive(Deserialize, Debug)] #[serde(tag = "type")] enum IncomingMessage { Action { view_id: ViewId, action: serde_json::Value, }, } #[derive(Clone)] pub struct Executor(Rc>); struct ExecutorState { next_spawn_id: usize, futures: HashMap>>>>, pending: HashMap>>>>, notify_handle: Option>, } #[wasm_bindgen] #[derive(Clone)] pub struct NotifyHandle(Weak>); #[wasm_bindgen] pub struct Channel { sender: Option, receiver: Option, } #[wasm_bindgen] #[derive(Clone)] pub struct Sender(Option>); #[wasm_bindgen] pub struct Receiver(mpsc::UnboundedReceiver); #[wasm_bindgen] pub struct Server { executor: Executor, app: Rc>, } struct FileProvider; // The leading ./ is redundant here, but it's needed due to a limitation in wasm_bindgen which // would otherwise interpret lib/support as an NPM module. #[wasm_bindgen(module = "./../lib/support")] extern "C" { #[wasm_bindgen(js_name = notifyOnNextTick)] fn notify_on_next_tick(notify: NotifyHandle); pub type JsSink; #[wasm_bindgen(method)] fn send(this: &JsSink, message: Vec); #[wasm_bindgen(method)] fn close(this: &JsSink); } impl Executor { fn new() -> Self { let state = Rc::new(RefCell::new(ExecutorState { next_spawn_id: 0, futures: HashMap::new(), pending: HashMap::new(), notify_handle: None, })); state.borrow_mut().notify_handle = Some(Arc::new(NotifyHandle(Rc::downgrade(&state)))); Executor(state) } } impl> future::Executor for Executor { fn execute(&self, future: F) -> Result<(), future::ExecuteError> { let id; let notify_handle; // Drop the dynamic borrow of state before polling the future for the first time, // because polling might cause a reentrant call to this method. { let mut state = self.0.borrow_mut(); id = state.next_spawn_id; state.next_spawn_id += 1; notify_handle = state.notify_handle.as_ref().unwrap().clone(); } let mut spawn = executor::spawn(future); match spawn.poll_future_notify(¬ify_handle, id) { Ok(Async::NotReady) => { self.0 .borrow_mut() .futures .insert(id, Rc::new(RefCell::new(spawn))); } _ => {} } Ok(()) } } #[wasm_bindgen] impl NotifyHandle { pub fn notify_from_js_on_next_tick(&self) { if let Some(state) = self.0.upgrade() { let notify_handle; let mut pending = HashMap::new(); { let mut state = state.borrow_mut(); notify_handle = state.notify_handle.as_ref().unwrap().clone(); mem::swap(&mut state.pending, &mut pending); } for (id, task) in pending { if let Ok(mut task) = task.try_borrow_mut() { match task.poll_future_notify(¬ify_handle, id) { Ok(Async::NotReady) => {} _ => { state.borrow_mut().futures.remove(&id); } } } } } } } impl Notify for NotifyHandle { fn notify(&self, id: usize) { if let Some(state) = self.0.upgrade() { let mut state = state.borrow_mut(); if !state.pending.contains_key(&id) { if let Some(task) = state.futures.get(&id).cloned() { state.pending.insert(id, task); if state.pending.len() == 1 { notify_on_next_tick(self.clone()); } } } } } } // The only convenient way of calling poll_future_notify is to wrap our notify handle in an Arc, // which requires Notify to be Send and Sync. However, because we are integrating with JavaScript, // we know that all of this code will be run in a single thread. unsafe impl Send for NotifyHandle {} unsafe impl Sync for NotifyHandle {} #[wasm_bindgen] impl Channel { pub fn new() -> Channel { let (tx, rx) = mpsc::unbounded(); Self { sender: Some(Sender(Some(tx))), receiver: Some(Receiver(rx)), } } pub fn take_sender(&mut self) -> Sender { self.sender.take().unwrap() } pub fn take_receiver(&mut self) -> Receiver { self.receiver.take().unwrap() } } #[wasm_bindgen] impl Sender { pub fn send(&mut self, message: Vec) -> bool { if let Some(ref mut tx) = self.0 { tx.unbounded_send(Bytes::from(message)).is_ok() } else { false } } pub fn dispose(&mut self) { self.0.take(); } } impl Stream for Receiver { type Item = Bytes; type Error = (); fn poll(&mut self) -> Poll, Self::Error> { self.0.poll() } } impl Sink for JsSink { type SinkItem = Vec; type SinkError = (); fn start_send( &mut self, item: Self::SinkItem, ) -> Result, Self::SinkError> { JsSink::send(self, item); Ok(AsyncSink::Ready) } fn poll_complete(&mut self) -> Result, Self::SinkError> { Ok(Async::Ready(())) } fn close(&mut self) -> Result, Self::SinkError> { JsSink::close(self); Ok(Async::Ready(())) } } #[wasm_bindgen] impl Server { pub fn new() -> Self { let foreground_executor = Rc::new(Executor::new()); // TODO: use a requestIdleCallback-based executor here instead. let background_executor = foreground_executor.clone(); Server { app: App::new( false, foreground_executor.clone(), background_executor.clone(), FileProvider, ), executor: Executor::new(), } } pub fn start_app(&mut self, outgoing: JsSink) { use futures::future::Executor; let executor = self.executor.clone(); if let Some(commands) = self.app.borrow_mut().commands() { let outgoing_commands = commands .map(|command| match command { Command::OpenWindow(window_id) => OutgoingMessage::OpenWindow { window_id }, }) .map(|command| serde_json::to_vec(&command).unwrap()) .map_err(|_| unreachable!()); executor .execute(Box::new(outgoing.send_all(outgoing_commands)).then(|_| Ok(()))) .unwrap(); } else { eprintln!("connect_app can only be called once") } } pub fn start_window(&mut self, window_id: WindowId, incoming: Receiver, outgoing: JsSink) { use futures::future::Executor; let app = self.app.clone(); let receive_incoming = incoming .map(|message| serde_json::from_slice(&message).unwrap()) .for_each(move |message| { match message { IncomingMessage::Action { view_id, action } => { app.borrow_mut().dispatch_action(window_id, view_id, action); } } Ok(()) }) .then(|_| Ok(())); self.executor.execute(Box::new(receive_incoming)).unwrap(); match self.app.borrow_mut().start_window(&window_id, 0_f64) { Ok(updates) => { let serialized_updates = updates.map(|update| { serde_json::to_vec(&OutgoingMessage::UpdateWindow(update)).unwrap() }); self.executor .execute( outgoing .send_all(serialized_updates.map_err(|_| unreachable!())) .then(|_| Ok(())), ) .unwrap(); } Err(_) => { let error = stream::once(Ok(OutgoingMessage::Error { description: format!("No window exists for id {}", window_id), })).map(|message| serde_json::to_vec(&message).unwrap()); self.executor .execute(Box::new(outgoing.send_all(error).then(|_| Ok(())))) .unwrap(); } }; } pub fn connect_to_peer(&mut self, incoming: Receiver, outgoing: JsSink) { use futures::future::Executor; let executor = self.executor.clone(); let connect_future = self.app .borrow_mut() .connect_to_server(incoming.map_err(|_| unreachable!())) .map_err(|error| eprintln!("RPC error: {}", error)) .and_then(move |connection| { executor .execute(Box::new( outgoing.send_all( connection // TODO: go back to using Vec for outgoing messages in xray_core. .map(|bytes| bytes.to_vec()) .map_err(|_| unreachable!()), ).then(|_| Ok(())), )) .unwrap(); Ok(()) }); self.executor.execute(Box::new(connect_future)).unwrap(); } } impl xray_core::fs::FileProvider for FileProvider { fn open( &self, _: &cross_platform::Path, ) -> Box, Error = io::Error>> { unimplemented!() } } #[wasm_bindgen] #[cfg(feature = "js-tests")] pub struct Test { executor: Executor, } #[wasm_bindgen] #[cfg(feature = "js-tests")] impl Test { pub fn new() -> Self { Self { executor: Executor::new(), } } pub fn echo_stream(&self, incoming: Receiver, outgoing: JsSink) { use futures::future::Executor; self.executor .execute(Box::new( outgoing.send_all(incoming.map(|bytes| bytes.to_vec())) .then(|_| Ok(())), )) .unwrap(); } } ================================================ FILE: xray_wasm/test/tests.js ================================================ import assert from "assert"; import { xray as xrayPromise, JsSink } from "../lib/main"; suite("Server", () => { let xray; before(async () => { xray = await xrayPromise; }); test("channels and sinks", endTest => { const test = xray.Test.new(); const messages = []; const sink = new JsSink({ send(message) { assert.equal(message.length, 1); messages.push(message[0]); }, close() { assert.deepEqual(messages, [0, 1, 2, 3, 4]); endTest(); } }); const channel = xray.Channel.new(); test.echo_stream(channel.take_receiver(), sink); const sender = channel.take_sender(); let i = 0; let intervalId = setInterval(() => { if (i === 5) { sender.dispose(); clearInterval(intervalId); } sender.send([i++]); }, 1); }); });