├── .babelrc ├── .gitignore ├── .travis.yml ├── Cargo.lock ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── api.md ├── build.rs ├── demo.md ├── design.md ├── find-all-uses.png ├── ident-search.png ├── overview.png ├── package-lock.json ├── package.json ├── right-click.png ├── rustw.toml ├── src ├── bin │ └── cargo-src.rs ├── build.rs ├── config.rs ├── file_controller │ ├── mod.rs │ └── results.rs ├── highlight.rs ├── lib.rs ├── listings.rs └── server.rs ├── static ├── .eslintignore ├── .eslintrc ├── app.js ├── breadCrumbs.tsx ├── contentPanel.js ├── dirView.tsx ├── favicon.ico ├── file.png ├── folder.png ├── fonts │ ├── FiraCode-Bold.woff │ ├── FiraCode-Light.woff │ ├── FiraCode-Medium.woff │ ├── FiraCode-Regular.woff │ ├── FiraSans-Light.woff │ ├── FiraSans-Medium.woff │ └── FiraSans-Regular.woff ├── index.html ├── libs │ ├── jquery-2.2.2.min.js │ └── marked.min.js ├── menus.js ├── rustw.css ├── rustw.out.js ├── rustw.out.js.map ├── rustw.ts ├── search.js ├── searchPanel.js ├── sidebar.js ├── srcView.js ├── symbolPanel.js ├── treePanel.js └── utils.js ├── tsconfig.json ├── type-on-hover.png └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "react", 4 | ["env", { 5 | "targets": { 6 | "browsers": ["last 2 versions", "safari >= 10"] 7 | } 8 | }] 9 | ], 10 | "plugins": ["transform-object-rest-spread"] 11 | } 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | target 3 | node_modules 4 | watch.sh 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: rust 2 | rust: 3 | - nightly 4 | 5 | install: 6 | - npm install 7 | - yarn build 8 | - npm install travis-github-lint-status eslint eslint-plugin-react 9 | 10 | script: 11 | - cargo build 12 | - cd static && ./../node_modules/travis-github-lint-status/index.js 13 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cargo-src" 3 | version = "0.1.8" 4 | authors = ["Nick Cameron "] 5 | repository = "https://github.com/nrc/cargo-src" 6 | readme = "README.md" 7 | license = "Apache-2.0/MIT" 8 | build = "build.rs" 9 | categories = ["development-tools"] 10 | description = "Semantic code navigation for Rust" 11 | 12 | [[bin]] 13 | name = "cargo-src" 14 | 15 | [dependencies] 16 | cargo_metadata = "0.6.4" 17 | url = "2.1" 18 | serde = "1.0" 19 | serde_json = "1.0" 20 | serde_derive = "1.0" 21 | toml = "0.4" 22 | rls-analysis = "0.13" 23 | rls-blacklist = "0.1.2" 24 | rls-span = "0.4" 25 | rls-vfs = "0.4" 26 | log = "0.3" 27 | env_logger = "0.3" 28 | rustdoc-highlight = "0.1.10" 29 | futures = "0.1.14" 30 | hyper="0.11" 31 | comrak="0.2.13" 32 | 33 | [build-dependencies] 34 | walkdir = "2" 35 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 The Rustw Project Developers 2 | 3 | Permission is hereby granted, free of charge, to any 4 | person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the 6 | Software without restriction, including without 7 | limitation the rights to use, copy, modify, merge, 8 | publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software 10 | is furnished to do so, subject to the following 11 | conditions: 12 | 13 | The above copyright notice and this permission notice 14 | shall be included in all copies or substantial portions 15 | of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 18 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 19 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 21 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 24 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 25 | DEALINGS IN THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cargo src 2 | 3 | [![Build Status](https://travis-ci.org/nrc/cargo-src.svg?branch=master)](https://travis-ci.org/nrc/cargo-src) 4 | 5 | A Rust source browser. Explore your Rust project with semantic understanding of 6 | the code. Features: 7 | 8 | * syntax highlighting 9 | * jump to def (click on a reference) 10 | * find all references (click on a definition) 11 | * smart identifier search 12 | * types (and field info) on hover 13 | * smart usage highlighting 14 | * find all impls (right click on a type or trait name) 15 | * jump to docs for standard library references 16 | * directory browsing 17 | * symbol browsing 18 | 19 | Uses knowledge from the RLS. 20 | 21 | This is work-in-progress, pre-release software, expect bugs and plenty of rough 22 | edges. 23 | 24 | ## Contents: 25 | 26 | * [Installing and running](#installing) 27 | * [Customisation](#customisation) 28 | * [Building](#building) 29 | * [Contributing](#contributing) 30 | 31 | ### Screenshots 32 | 33 | ![cargo src screenshot - source view](overview.png) 34 | 35 | ### Hover to show the type of an identifier: 36 | 37 | ![cargo src screenshot - source view](type-on-hover.png) 38 | 39 | ### Search for an identifier name (shows definitions, and all references to each definition): 40 | 41 | ![cargo src screenshot - source view](ident-search.png) 42 | 43 | ### Find all uses of a definition: 44 | 45 | ![cargo src screenshot - source view](find-all-uses.png) 46 | 47 | ### Right click an identifier to show more options 48 | 49 | ![cargo src screenshot - source view](right-click.png) 50 | 51 | 52 | ## Installing and running 53 | 54 | Requires a nightly version of Rust. 55 | 56 | To install, run `cargo install cargo-src`. 57 | 58 | Then, to run: `cargo src` in a directory where you would usually run `cargo build`. 59 | You can run `cargo src --open` to open the output of `cargo src` directly in your 60 | web browser. 61 | 62 | `cargo src` will start a web server and build (`cargo check`) and index your code. 63 | This may take some time (depending on your crate, up to twice as long as a normal 64 | build). You can browse the source in whilst indexing, but you'll be missing all 65 | the good stuff like jump-to-def and search. 66 | 67 | 68 | ## Customisation 69 | 70 | Create a `rustw.toml` file in your project's directory. See [src/config.rs](src/config.rs) 71 | or run `cargo src -- -h` for the options available and their defaults. 72 | 73 | Some features **need** configuration in the rustw.toml before they can be 74 | used. 75 | 76 | ``` 77 | edit_command = "subl $file:$line" 78 | ``` 79 | 80 | To be able to open files in your local editor. This example works for sublime 81 | text (`subl`). Use the `$file` and `$line` variables as appropriate for your 82 | editor. 83 | 84 | ``` 85 | vcs_link = "https://github.com/nrc/rustw-test/blob/master/$file#L$line" 86 | ``` 87 | 88 | For links to the code in version control, GitHub, in the example. 89 | 90 | 91 | ## Building 92 | 93 | Requires a nightly version of Rust. 94 | 95 | Get the source code from https://github.com/nrc/cargo-src. 96 | 97 | * Install the dependencies: `npm install` or `yarn` 98 | * Build the JS components: `npm run build` or `yarn build` 99 | * Build the Rust parts: `cargo build --release` 100 | 101 | ### Running 102 | 103 | Run `CARGO=cargo //rustw/target/release/cargo-src` in your 104 | project's directory (i.e., the directory you would normally use `cargo build` 105 | from). 106 | 107 | Running `cargo src` will start a web server and display a URL in the console. To 108 | terminate the server, use `ctrl + c`. If you point your browser at the provided 109 | URL, it will list the directories and files from your project, which you can then 110 | use to view the code. The terminal is only used to display logging, it can 111 | be ignored. 112 | 113 | You can use `--open` to open the cargo src browser directly in your web browser. 114 | 115 | Currently, cargo src has only been tested on Firefox on Linux and MacOS 116 | ([issue 48](https://github.com/nrc/cargo-src/issues/48)). 117 | 118 | 119 | ### Troubleshooting 120 | 121 | If you get an error like `error while loading shared libraries` while starting 122 | up cargo src you should try the following: 123 | 124 | On Linux: 125 | 126 | ``` 127 | export LD_LIBRARY_PATH=$(rustc --print sysroot)/lib:$LD_LIBRARY_PATH 128 | ``` 129 | 130 | On MacOS: 131 | 132 | ``` 133 | export DYLD_LIBRARY_PATH=$(rustc --print sysroot)/lib:$DYLD_LIBRARY_PATH 134 | ``` 135 | 136 | ## Contributing 137 | 138 | Cargo src is open source (dual-licensed under the Apache 2.0 and MIT licenses) 139 | and contributions are welcome! You can help by testing and 140 | [reporting issues](https://github.com/nrc/cargo-src/issues/new). Code, tests, and 141 | documentation are very welcome, you can browse [all issues](https://github.com/nrc/cargo-src/issues) 142 | or [easy issues](https://github.com/nrc/cargo-src/issues?q=is%3Aopen+is%3Aissue+label%3Aeasy) 143 | to find something to work on. 144 | 145 | If you'd like help or want to talk about cargo, you can find me on the 146 | rust-dev-tools irc channel (nrc), email (my irc handle @mozilla.com), or 147 | twitter ([@nick_r_cameron](https://twitter.com/nick_r_cameron)). 148 | 149 | The cargo src server is written in Rust and uses Hyper. It runs `cargo check` as 150 | a separate process and only really deals with output on stdout/stderr. 151 | 152 | The cargo src frontend is a single page web app written in HTML and Javascript. It 153 | uses React and JQuery. 154 | -------------------------------------------------------------------------------- /api.md: -------------------------------------------------------------------------------- 1 | **Search** 2 | ---- 3 | Returns json data definitions and references for an identifier via text or numeric id search. 4 | 5 | * **URL** 6 | 7 | `/search?needle=:needle` or `/search?id=:id` 8 | 9 | * **Method:** 10 | 11 | `GET` 12 | 13 | * **URL Params** 14 | 15 | **Required:** 16 | 17 | `needle=[string]` or `id=[integer]` 18 | 19 | * **Data Params** 20 | 21 | None 22 | 23 | * **Success Response:** 24 | 25 | * **Code:** 200
26 | **Content:** `{ defs : [ { file_name: , lines: [] } ], 27 | refs : [ { file_name: , lines: [] } ] }` 28 | 29 | * **Error Response:** 30 | 31 | * **Code:** 500 Internal Server Error
32 | **Content:** `"Bad search string"` 33 | 34 | OR 35 | 36 | * **Code:** 500 Internal Server Error
37 | **Content:** `"Bad id: "` 38 | 39 | * **Sample Call:** 40 | 41 | ```javascript 42 | $.ajax({ 43 | url: "/search?needle=off_t", 44 | dataType: "json", 45 | type : "GET", 46 | success : function(r) { 47 | console.log(r); 48 | } 49 | }); 50 | ``` 51 | 52 | **Source** 53 | ---- 54 | Returns json data for source directory or file. 55 | 56 | * **URL** 57 | 58 | `/src/:path/:filename` 59 | 60 | * **Method:** 61 | 62 | `GET` 63 | 64 | * **URL Params** 65 | 66 | None 67 | 68 | * **Data Params** 69 | 70 | None 71 | 72 | * **Success Response:** 73 | 74 | * **Code:** 200
75 | **Content:** `{ Directory : { path : [], files: [] } }` 76 | 77 | OR 78 | 79 | * **Code:** 200
80 | **Content:** `{ Source : { path : [], lines: [] } }` 81 | 82 | * **Error Response:** 83 | 84 | * **Code:** 500 Internal Server Error
85 | **Content:** `io::Error reading or writing path` 86 | 87 | * **Sample Call:** 88 | 89 | ```javascript 90 | $.ajax({ 91 | url: "/src/src/lib.rs", 92 | dataType: "json", 93 | type : "GET", 94 | success : function(r) { 95 | console.log(r); 96 | } 97 | }); 98 | ``` 99 | 100 | **Find impls** 101 | ---- 102 | Returns json array of impls for a struct or enum by id. 103 | 104 | * **URL** 105 | 106 | `/find?impls=:id` 107 | 108 | * **Method:** 109 | 110 | `GET` 111 | 112 | * **URL Params** 113 | 114 | **Required:** 115 | 116 | `impls=[integer]` 117 | 118 | * **Data Params** 119 | 120 | None 121 | 122 | * **Success Response:** 123 | 124 | * **Code:** 200
125 | **Content:** `{ results : [ { file_name : , lines: [] } ] }` 126 | 127 | * **Error Response:** 128 | 129 | * **Code:** 500 Internal Server Error
130 | **Content:** `"Unknown argument to find"` 131 | 132 | OR 133 | 134 | * **Code:** 500 Internal Server Error
135 | **Content:** `"Bad id: "` 136 | 137 | * **Sample Call:** 138 | 139 | ```javascript 140 | $.ajax({ 141 | url: "/find?impls=8", 142 | dataType: "json", 143 | type : "GET", 144 | success : function(r) { 145 | console.log(r); 146 | } 147 | }); 148 | ``` 149 | -------------------------------------------------------------------------------- /build.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2016 The Rustw Project Developers. 2 | // 3 | // Licensed under the Apache License, Version 2.0 or the MIT license 5 | // , at your 6 | // option. This file may not be copied, modified, or distributed 7 | // except according to those terms. 8 | 9 | extern crate walkdir; 10 | use std::env; 11 | use std::fs::File; 12 | use std::io::Write; 13 | use std::path::Path; 14 | use walkdir::WalkDir; 15 | 16 | /// This build script creates a function for looking up static data to be served 17 | /// by the web server. The contents of the static directory are included in the 18 | /// binary via `include_bytes` and exposed to the rest of the program by 19 | /// `lookup_static_file`. 20 | fn main() { 21 | let from = env::var("CARGO_MANIFEST_DIR").unwrap(); 22 | let from = Path::new(&from); 23 | let from = from.join("static"); 24 | 25 | let mut out_path = Path::new(&env::var("OUT_DIR").unwrap()).to_owned(); 26 | out_path.push("lookup_static.rs"); 27 | 28 | // Don't rerun on every build, but do rebuild if the build script changes 29 | // or something changes in the static directory. 30 | println!("cargo:rerun-if-changed=build.rs"); 31 | println!("cargo:rerun-if-changed=static"); 32 | 33 | let mut out_file = File::create(&out_path).unwrap(); 34 | out_file.write(PREFIX.as_bytes()).unwrap(); 35 | 36 | for entry in WalkDir::new(&from) 37 | .into_iter() 38 | .filter_map(|e| e.ok()) 39 | .filter(|e| !e.path_is_symlink() && e.file_type().is_file()) 40 | { 41 | let relative = entry.path().strip_prefix(&from).unwrap(); 42 | write!( 43 | out_file, 44 | "\nr#\"{}\"# => Ok(include_bytes!(r#\"{}\"#)),", 45 | relative.display(), 46 | entry.path().display() 47 | ) 48 | .unwrap(); 49 | } 50 | 51 | out_file.write(SUFFIX.as_bytes()).unwrap(); 52 | } 53 | 54 | const PREFIX: &str = " 55 | pub fn lookup_static_file(path: &str) -> Result<&'static [u8], ()> { 56 | match path { 57 | "; 58 | 59 | const SUFFIX: &str = " 60 | _ => Err(()), 61 | } 62 | } 63 | "; 64 | -------------------------------------------------------------------------------- /demo.md: -------------------------------------------------------------------------------- 1 | # How to make a demo 2 | 3 | * You'll need a server. 4 | * Clone the rustw repo. build it (see README.md). 5 | * You'll want some code to demonstrate on too. 6 | * In that directory create a rustw.toml. You'll want at least: 7 | 8 | ``` 9 | demo_mode = true 10 | demo_mode_root_path = "rustw/" 11 | ``` 12 | 13 | Or whatever you'll use for your root path. 14 | 15 | * You'll need to edit index.html and change all the URLs, e.g., `/static/favicon.ico` to `/rustw/static/favicon.ico`. 16 | * In rustw.js, find `onLoad()`, you'll need to change the `/config` url to include the root path, e.g, `/rustw/config`. 17 | * Likewise, update the URLs for the fonts in rustw.css. 18 | * Update the test data in `build.rs` with the JSON output from building your project. 19 | * Build rustw (see README.md). 20 | * Assuming you're running Apache, you'll need to add modify your configuration. I changed `000-default.conf` in `/etc/apache2/sites-enabled`, by adding 21 | 22 | ``` 23 | ProxyPass /rustw http://localhost:2348 24 | ProxyPassReverse /rustw http://localhost:2348 25 | ``` 26 | 27 | to the `` element. Note that you'll want the port here to match the port you specify in rustw.toml. 28 | 29 | * Run it on your server: in the project directory, `/path/to/rustw/target/debug/rustw`. 30 | * It should now be live at, e.g., `www.ncameron.org/rustw`. 31 | -------------------------------------------------------------------------------- /design.md: -------------------------------------------------------------------------------- 1 | # rustw design doc 2 | 3 | This is mostly covering rustw as it should be, rather than as it is. I'm 4 | contemplating yet another reboot (hopefully not too much of a rewrite) and I 5 | thought it would be good to do some upfront design. 6 | 7 | ## Goals 8 | 9 | ### Code browsing 10 | 11 | Many engineers prefer code browser support to IDEs. This would be popular with 12 | Gecko and Servo devs. Also a nice improvement to Rustdoc (especially with 13 | docs.rs integration.) 14 | 15 | I see two ways to use this tool: 16 | 17 | * primarily as a reference (similar to rustdoc output or GitHub search/browsing) 18 | * primarily interactive 19 | 20 | The former use case suggests a hosted solution (which makes network speed 21 | important). Search and doc integration are probably the most important features. 22 | Only needs to work with compiling code, speed of indexing not really an issue. 23 | 24 | The second use case suggests a local solution. Could either watch and rebuild or 25 | requires easy rebuild (e.g., on refresh). Needs to work with non-compiling code 26 | and needs to index fast (similar constraints to an IDE). Features for debugging 27 | such as macro exploration and borrow visualisation would fit well. 28 | 29 | 30 | ### Better error messages, etc. 31 | 32 | By using the web rather than the console we can do better for error messages. We 33 | can link to err code info, jump to source, open in the editor, even do quick 34 | fixes (though I actually removed this recently), or apply suggestions. We can 35 | also expand error messages and do nice layout, syntax highlighting of snippets, 36 | etc. I hoped this would be really useful for beginners. 37 | 38 | However, the workflow is a bit unergonomic. And people who work on the console, 39 | don't like this, and people who work in the editor want IDEs, so it seems there 40 | may not be an audience at all. 41 | 42 | In theory, we could host a Rust env on a server and do remote builds with rustw 43 | as the interface, but this would take a lot more backend work (this can be 44 | hacked atm, but security is a real issue - e.g., proc macros). 45 | 46 | I had wanted to provide a kind of GUI for rustup too. 47 | 48 | 49 | ### Questions and implications 50 | 51 | rustw currently priorities the build screen and only offers code browsing via 52 | links. Should we prioritise code browsing? Should we go all in and abandon the 53 | error messages work? Or might this be useful if we did it right? 54 | 55 | If we go down the code browsing path, should we aim for a reference or 56 | interactive mode? There seems more demand for the former, but the latter could 57 | do more interesting things (high risk, high reward). 58 | 59 | 60 | ### New features 61 | 62 | Some possible important features we could implement (these mostly have issues 63 | but I'm too lazy to find them). 64 | 65 | Building: 66 | 67 | * Apply suggestions 68 | * Quick edit (restore editing features) 69 | * Overlay errors/warnings on source code 70 | 71 | Browsing: 72 | 73 | * Macro expansion 74 | * Borrow visualisation 75 | * More advanced searches (sub-/super-traits, etc., search by type (like Hoogle), full text search, fuzzy search, regex search) 76 | * summary view (is this just docs in the end?) 77 | * side-by-side scrolling source + summary/docs 78 | * peek (for searches, defintion, etc) 79 | * smart history (provide a tree history view rather than just browser back/forward) 80 | * navigate backtraces (from debuggers and rust backtrace) 81 | * Version control integration (integrated blame etc.) 82 | * semantic syntax highlighting 83 | * C++/other lang integration (perhaps by making it work with searchfox or whatever, see below) 84 | 85 | Misc: 86 | 87 | * Mobile use (tablets, rather than phones) - responsive design, UI (can't right click?) 88 | * Embedding - would be cool to embed our source view into other stuff (e.g., directly in rustdoc, rather than a separate source view mode) 89 | * Integration with other search tools (e.g., GitHub, DXR, searchfox, Google's code search thing. Lots of options for how this might work) 90 | 91 | Non-goals: 92 | 93 | * General editing (just use an IDE) 94 | * Refactoring (well, maybe, but mostly just use an IDE) 95 | * Fancy backend building stuff - distributed builds etc. (just too far out of scope) 96 | - although perhaps some kind of integration with sccache would help with interactive-ish remote code browsing 97 | * Debugger frontend (although this would be cool) 98 | 99 | ## Layout 100 | 101 | ### Current 102 | 103 | ``` 104 | +-------------------------------------------+ 105 | | topbar | 106 | +-------------------------------------------+ 107 | | main | 108 | | | 109 | | | 110 | | | 111 | | | 112 | | | 113 | | | 114 | | | 115 | | | 116 | +-------------------------------------------+ 117 | ``` 118 | 119 | We use the (non-scrolling) topbar for a few links/buttons and a search box 120 | 121 | We use the main area for everything else - the errors screen, various code browsing/search screens, etc. 122 | 123 | We use the thin bar between the topbar and main area as a progress indicator when building. 124 | 125 | We have custom popup menus on right click and for options, etc. 126 | 127 | 128 | ### Future 129 | 130 | I think we require some non-scrolling navigational features - search box is 131 | useful, home (build results), perhaps breadcrumbs should be non-scrolling too. 132 | Would be nice to make the topbar feel more lightweight. 133 | 134 | We could use pop-over panels more - e.g., for error code info, or to peek stuff 135 | in code browsing mode. Could also use side-by-side panels for this (we rarely 136 | use the rhs of the main panel). 137 | 138 | I would like to allow side-by-side scrolling of docs and source code 139 | 140 | ``` 141 | +-------------------------------------------+ 142 | | topbar | 143 | +------------------------+------------------+ 144 | | main | side panel | 145 | | | | 146 | | | | 147 | | | | 148 | | | | 149 | | | | 150 | | | | 151 | | | | 152 | | | | 153 | +------------------------+------------------+ 154 | ``` 155 | 156 | 157 | ## Pages/modes 158 | 159 | Current: 160 | 161 | * empty (on startup) 162 | * 'Internal error' 163 | * 'loading' 164 | * build results 165 | * error code explanation 166 | * view source file 167 | * view directory 168 | * search results 169 | - one list 170 | - defs/refs (might reorg this at some point) 171 | * summary (something like rustdoc, but more source-oriented) 172 | 173 | Possible changes: 174 | 175 | If we priorities code browsing, home screen could be the top directory view. 176 | Build results could be removed completely or made into a panel (pop-over or side 177 | panel). 178 | 179 | Error code explanation should be a panel (or removed, if we prioritise code browsing) 180 | 181 | Could remove summary for now (it is currently broken, plus rustdoc2 is happening) 182 | 183 | 184 | ## URLs/routes 185 | 186 | TODO depends on above 187 | 188 | 189 | ## Data 190 | 191 | I.e., what does our Redux store look like? What data do we need to keep locally. 192 | 193 | TODO depends on above 194 | 195 | 196 | ## Architectural considerations 197 | 198 | We currently move big chunks of data to the client. E.g., for error pages, we 199 | include all the error explanations; for source browsing we keep a lot of 200 | information inside links. We could use smaller ajax requests to get this kind of 201 | info (and thus make quicker responding pages). And hopefully fewer frontend 202 | hacks. 203 | 204 | 205 | ## Colours and other graphic design issues 206 | 207 | I kind of hate the pale yellow background colour. 208 | 209 | Also hate the thick black bar between topbar and main panel (although I like 210 | using it for progress indicator) 211 | 212 | Buttons? Menus? Both look kind of old to me. Perhaps links instead of buttons. 213 | Not sure how to improve menus. Ideally we wouldn't use right click. But having 214 | left-click naviagate rather than opening a menu is nice. 215 | -------------------------------------------------------------------------------- /find-all-uses.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rust-dev-tools/cargo-src/328f783ac40245c298ee6c1e9654f4d4e8d46e7b/find-all-uses.png -------------------------------------------------------------------------------- /ident-search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rust-dev-tools/cargo-src/328f783ac40245c298ee6c1e9654f4d4e8d46e7b/ident-search.png -------------------------------------------------------------------------------- /overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rust-dev-tools/cargo-src/328f783ac40245c298ee6c1e9654f4d4e8d46e7b/overview.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "build": "webpack", 4 | "lint:es": "eslint -c ./static/.eslintrc --ignore-path ./static/.eslintignore static/ || true" 5 | }, 6 | "dependencies": { 7 | "@types/react": "^16.8.1", 8 | "@types/react-router-dom": "^4.3.1", 9 | "react": "^16.7.0", 10 | "react-dom": "^16.7.0", 11 | "react-router-dom": "^4.3.1", 12 | "react-tabs": "^3.0.0", 13 | "react-treebeard": "^3.1.0", 14 | "react-sanitized-html": "^2.0.0", 15 | "sanitize-html": "^1.20.0", 16 | "typescript": "^3.3.1" 17 | }, 18 | "devDependencies": { 19 | "babel-core": "^6.26.3", 20 | "babel-loader": "^7.1.5", 21 | "babel-plugin-transform-object-rest-spread": "^6.26.0", 22 | "babel-preset-env": "^1.2.1", 23 | "babel-preset-es2015": "^6.24.1", 24 | "babel-preset-react": "^6.24.1", 25 | "eslint": "^5.13.0", 26 | "eslint-plugin-no-jquery": "^2.5.0", 27 | "eslint-plugin-react": "^7.12.4", 28 | "immutable": "^3.8.2", 29 | "ts-loader": "^5.3.2", 30 | "webpack": "^4.29.0", 31 | "webpack-cli": "^3.2.1" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /right-click.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rust-dev-tools/cargo-src/328f783ac40245c298ee6c1e9654f4d4e8d46e7b/right-click.png -------------------------------------------------------------------------------- /rustw.toml: -------------------------------------------------------------------------------- 1 | build_command = "cargo build" 2 | edit_command = "subl $file:$line" 3 | port = 3000 4 | -------------------------------------------------------------------------------- /src/bin/cargo-src.rs: -------------------------------------------------------------------------------- 1 | extern crate cargo_src; 2 | extern crate env_logger; 3 | extern crate log; 4 | #[macro_use] 5 | extern crate serde_derive; 6 | extern crate serde_json; 7 | 8 | use cargo_src::BuildArgs; 9 | use std::env; 10 | use std::process::Command; 11 | 12 | fn main() { 13 | env_logger::init().unwrap(); 14 | 15 | let mut args = env::args().peekable(); 16 | let _prog = args.next().expect("No program name?"); 17 | 18 | // Remove `src` from the args, if present. 19 | let mut has_src = false; 20 | if let Some(s) = args.peek() { 21 | if s == "src" { 22 | has_src = true; 23 | } 24 | } 25 | if has_src { 26 | args.next().unwrap(); 27 | } 28 | 29 | let args: Vec<_> = args.collect(); 30 | 31 | if args.contains(&"--help".to_owned()) { 32 | print_help(); 33 | return; 34 | } 35 | 36 | let workspace_root = match workspace_root() { 37 | Ok(root) => root, 38 | Err(_) => { 39 | println!("Error: could not find workspace root"); 40 | println!("`cargo src` run outside a Cargo project"); 41 | std::process::exit(1); 42 | } 43 | }; 44 | 45 | let build_args = BuildArgs { 46 | program: env::var("CARGO").expect("Missing $CARGO var"), 47 | args, 48 | workspace_root, 49 | }; 50 | 51 | cargo_src::run_server(build_args); 52 | } 53 | 54 | fn print_help() { 55 | println!("cargo-src"); 56 | println!("Browse a program's source code\n"); 57 | println!("USAGE:"); 58 | println!(" cargo src [OPTIONS]\n"); 59 | println!("OPTIONS:"); 60 | println!(" --help show this message"); 61 | println!(" --open open the cargo-src frontend in your web browser"); 62 | println!("\nOther options follow `cargo check`, see `cargo check --help` for more."); 63 | } 64 | 65 | fn workspace_root() -> Result { 66 | let output = Command::new("cargo") 67 | .args(&["metadata", "--format-version", "1"]) 68 | .output(); 69 | let stdout = String::from_utf8(output.expect("error executing `cargo metadata`").stdout) 70 | .expect("unexpected output"); 71 | let json: Metadata = serde_json::from_str(&stdout)?; 72 | Ok(json.workspace_root) 73 | } 74 | 75 | #[derive(Deserialize)] 76 | struct Metadata { 77 | workspace_root: String, 78 | } 79 | -------------------------------------------------------------------------------- /src/build.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2016 The Rustw Project Developers. 2 | // 3 | // Licensed under the Apache License, Version 2.0 or the MIT license 5 | // , at your 6 | // option. This file may not be copied, modified, or distributed 7 | // except according to those terms. 8 | 9 | use config::Config; 10 | 11 | use cargo_metadata; 12 | use std::collections::HashMap; 13 | use std::fs::{read_dir, remove_file}; 14 | use std::path::Path; 15 | use std::process::Command; 16 | use std::sync::Arc; 17 | 18 | // FIXME use `join` not `/` 19 | const TARGET_DIR: &str = "target/rls"; 20 | 21 | #[derive(Clone)] 22 | pub struct Builder { 23 | config: Arc, 24 | build_args: BuildArgs, 25 | } 26 | 27 | #[derive(Clone, Debug)] 28 | pub struct BuildArgs { 29 | pub program: String, 30 | pub args: Vec, 31 | pub workspace_root: String, 32 | } 33 | 34 | impl Builder { 35 | pub fn new(config: Arc, build_args: BuildArgs) -> Builder { 36 | Builder { config, build_args } 37 | } 38 | 39 | fn init_cmd(&self) -> Command { 40 | let mut cmd = Command::new(&self.build_args.program); 41 | cmd.arg("check"); 42 | cmd.args(&self.build_args.args); 43 | // FIXME(#170) configure save-analysis 44 | cmd.env("RUSTFLAGS", "-Zunstable-options -Zsave-analysis"); 45 | cmd.env("CARGO_TARGET_DIR", TARGET_DIR); 46 | cmd.env("RUST_LOG", ""); 47 | 48 | cmd 49 | } 50 | 51 | pub fn build(&self) -> Option { 52 | let mut cmd = self.init_cmd(); 53 | let status = cmd.status().expect("Running build failed"); 54 | let result = status.code(); 55 | self.clean_analysis(); 56 | result 57 | } 58 | 59 | // Remove any old or duplicate json files. 60 | fn clean_analysis(&self) { 61 | let crate_names = cargo_metadata::metadata_deps(None, true) 62 | .map(|metadata| metadata.packages) 63 | .unwrap_or_default() 64 | .into_iter() 65 | .map(|package| package.name.replace("-", "_")) 66 | .collect::>(); 67 | 68 | let analysis_dir = Path::new(&TARGET_DIR) 69 | .join("debug") 70 | .join("deps") 71 | .join("save-analysis"); 72 | 73 | if let Ok(dir_contents) = read_dir(&analysis_dir) { 74 | // We're going to put all files for the same crate in one bucket, then delete duplicates. 75 | let mut buckets = HashMap::new(); 76 | for entry in dir_contents { 77 | let entry = entry.expect("unexpected error reading save-analysis directory"); 78 | let name = entry.file_name(); 79 | let name = name.to_str().unwrap(); 80 | 81 | if !name.ends_with("json") { 82 | continue; 83 | } 84 | 85 | let hyphen = name.find('-'); 86 | let hyphen = match hyphen { 87 | Some(h) => h, 88 | None => continue, 89 | }; 90 | let name = &name[..hyphen]; 91 | let match_name = if name.starts_with("lib") { 92 | &name[3..] 93 | } else { 94 | &name 95 | }; 96 | // The JSON file does not correspond with any crate from `cargo 97 | // metadata`, so it is presumably an old dep that has been removed. 98 | // So, we should delete it. 99 | if !crate_names.iter().any(|name| name == match_name) { 100 | info!("deleting {:?}", entry.path()); 101 | if let Err(e) = remove_file(entry.path()) { 102 | debug!("Error deleting file, {:?}: {}", entry.file_name(), e); 103 | } 104 | 105 | continue; 106 | } 107 | 108 | buckets 109 | .entry(name.to_owned()) 110 | .or_insert_with(|| vec![]) 111 | .push(( 112 | entry.path(), 113 | entry 114 | .metadata() 115 | .expect("no file metadata") 116 | .modified() 117 | .expect("no modified time"), 118 | )) 119 | } 120 | 121 | for bucket in buckets.values_mut() { 122 | if bucket.len() <= 1 { 123 | continue; 124 | } 125 | 126 | // Sort by date created (JSON files are effectively read only) 127 | bucket.sort_by(|a, b| b.1.cmp(&a.1)); 128 | // And delete all but the newest file. 129 | for &(ref path, _) in &bucket[1..] { 130 | info!("deleting {:?}", path); 131 | if let Err(e) = remove_file(path) { 132 | debug!("Error deleting file, {:?}: {}", path, e); 133 | } 134 | } 135 | } 136 | } 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/config.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2016 The Rustw Project Developers. 2 | // 3 | // Licensed under the Apache License, Version 2.0 or the MIT license 5 | // , at your 6 | // option. This file may not be copied, modified, or distributed 7 | // except according to those terms. 8 | 9 | use toml; 10 | 11 | // Copy-pasta from rustfmt. 12 | 13 | // This trait and the following impl blocks are there so that we an use 14 | // UCFS inside the get_docs() function on types for configs. 15 | pub trait ConfigType { 16 | fn get_variant_names() -> String; 17 | } 18 | 19 | impl ConfigType for bool { 20 | fn get_variant_names() -> String { 21 | String::from("") 22 | } 23 | } 24 | 25 | impl ConfigType for usize { 26 | fn get_variant_names() -> String { 27 | String::from("") 28 | } 29 | } 30 | 31 | impl ConfigType for String { 32 | fn get_variant_names() -> String { 33 | String::from("") 34 | } 35 | } 36 | 37 | impl ConfigType for Option { 38 | fn get_variant_names() -> String { 39 | String::from(" | null") 40 | } 41 | } 42 | 43 | macro_rules! create_config { 44 | ($($i:ident: $ty:ty, $def:expr, $unstable:expr, $( $dstring:expr ),+ );+ $(;)*) => ( 45 | #[derive(Serialize, Deserialize, Clone)] 46 | pub struct Config { 47 | $(pub $i: $ty),+ 48 | } 49 | 50 | // Just like the Config struct but with each property wrapped 51 | // as Option. This is used to parse a rustfmt.toml that doesn't 52 | // specity all properties of `Config`. 53 | // We first parse into `ParsedConfig`, then create a default `Config` 54 | // and overwrite the properties with corresponding values from `ParsedConfig` 55 | #[derive(Deserialize, Clone)] 56 | pub struct ParsedConfig { 57 | $(pub $i: Option<$ty>),+ 58 | } 59 | 60 | impl Config { 61 | 62 | fn fill_from_parsed_config(mut self, parsed: ParsedConfig) -> Config { 63 | $( 64 | if let Some(val) = parsed.$i { 65 | self.$i = val; 66 | } 67 | )+ 68 | self 69 | } 70 | 71 | pub fn from_toml(s: &str) -> Config { 72 | let parsed_config: ParsedConfig = toml::from_str(s).expect("Could not parse TOML"); 73 | Config::default().fill_from_parsed_config(parsed_config) 74 | } 75 | 76 | pub fn print_docs() { 77 | use std::cmp; 78 | 79 | let max = 0; 80 | $( let max = cmp::max(max, stringify!($i).len()+1); )+ 81 | let mut space_str = String::with_capacity(max); 82 | for _ in 0..max { 83 | space_str.push(' '); 84 | } 85 | println!("Configuration Options:"); 86 | $( 87 | if !$unstable { 88 | let name_raw = stringify!($i); 89 | let mut name_out = String::with_capacity(max); 90 | for _ in name_raw.len()..max-1 { 91 | name_out.push(' ') 92 | } 93 | name_out.push_str(name_raw); 94 | name_out.push(' '); 95 | println!("{}{} Default: {:?}", 96 | name_out, 97 | <$ty>::get_variant_names(), 98 | $def); 99 | $( 100 | println!("{}{}", space_str, $dstring); 101 | )+ 102 | println!(""); 103 | } 104 | )+ 105 | } 106 | } 107 | 108 | // Template for the default configuration 109 | impl Default for Config { 110 | fn default() -> Config { 111 | Config { 112 | $( 113 | $i: $def, 114 | )+ 115 | } 116 | } 117 | } 118 | ) 119 | } 120 | 121 | create_config! { 122 | build_command: String, "cargo check".to_owned(), false, "command to call to build"; 123 | edit_command: String, String::new(), false, 124 | "command to call to edit; can use $file, $line, and $col."; 125 | unstable_features: bool, false, false, "Enable unstable features"; 126 | ip: String, "127.0.0.1".to_owned(), false, "ip address to launch server"; 127 | port: usize, 7878, false, "port to run rustw on"; 128 | demo_mode: bool, false, true, "run in demo mode"; 129 | demo_mode_root_path: String, String::new(), true, "path to use in URLs in demo mode"; 130 | context_lines: usize, 2, false, "lines of context to show before and after code snippets"; 131 | build_on_load: bool, true, false, "build on page load and refresh"; 132 | workspace_root: Option, None: Option, false, "root of the project workspace"; 133 | vcs_link: String, String::new(), false, "link to use for VCS; should use $file and $line."; 134 | } 135 | -------------------------------------------------------------------------------- /src/file_controller/mod.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2016 The Rustw Project Developers. 2 | // 3 | // Licensed under the Apache License, Version 2.0 or the MIT license 5 | // , at your 6 | // option. This file may not be copied, modified, or distributed 7 | // except according to those terms. 8 | 9 | use cargo_metadata; 10 | use std::collections::HashMap; 11 | use std::env; 12 | use std::path::{Path, PathBuf}; 13 | use std::str; 14 | use std::sync::Arc; 15 | 16 | use analysis::{AnalysisHost, Id, Target}; 17 | use config::Config; 18 | use span; 19 | use vfs::Vfs; 20 | 21 | use super::highlight; 22 | 23 | // FIXME maximum size and evication policy 24 | // FIXME keep timestamps and check on every read. Then don't empty on build. 25 | 26 | mod results; 27 | use file_controller::results::{ 28 | DefResult, FileResult, FindResult, LineResult, SearchResult, SymbolResult, CONTEXT_SIZE, 29 | }; 30 | 31 | pub struct Cache { 32 | config: Arc, 33 | files: Vfs, 34 | analysis: AnalysisHost, 35 | project_dir: PathBuf, 36 | } 37 | 38 | type Span = span::Span; 39 | 40 | #[derive(Debug, Clone)] 41 | pub struct Highlighted { 42 | pub source: Option>, 43 | pub rendered: Option, 44 | } 45 | 46 | // Our data which we attach to files in the VFS. 47 | struct VfsUserData { 48 | highlighted: Option, 49 | } 50 | 51 | impl VfsUserData { 52 | fn new() -> Self { 53 | VfsUserData { highlighted: None } 54 | } 55 | } 56 | 57 | macro_rules! vfs_err { 58 | ($e:expr) => {{ 59 | let r: Result<_, String> = $e.map_err(|e| e.into()); 60 | r 61 | }}; 62 | } 63 | 64 | impl Cache { 65 | pub fn new(config: Arc) -> Cache { 66 | Cache { 67 | config, 68 | files: Vfs::new(), 69 | analysis: AnalysisHost::new(Target::Debug), 70 | project_dir: env::current_dir().unwrap(), 71 | } 72 | } 73 | 74 | pub fn get_lines( 75 | &self, 76 | path: &Path, 77 | line_start: span::Row, 78 | line_end: span::Row, 79 | ) -> Result { 80 | vfs_err!(self.files.load_file(path))?; 81 | vfs_err!(self.files.load_lines(path, line_start, line_end)) 82 | } 83 | 84 | pub fn get_highlighted(&self, path: &Path) -> Result { 85 | fn raw_lines(text: &str) -> Vec { 86 | let mut highlighted: Vec = text.lines().map(|s| s.to_owned()).collect(); 87 | if text.ends_with('\n') { 88 | highlighted.push(String::new()); 89 | } 90 | 91 | highlighted 92 | } 93 | 94 | vfs_err!(self.files.load_file(path))?; 95 | vfs_err!(self 96 | .files 97 | .ensure_user_data(path, |_| Ok(VfsUserData::new())))?; 98 | vfs_err!(self.files.with_user_data(path, |u| { 99 | let (text, u) = u?; 100 | 101 | if u.highlighted.is_none() { 102 | if let Some(ext) = path.extension() { 103 | if ext == "rs" { 104 | let text = match text { 105 | Some(t) => t, 106 | None => return Err(::vfs::Error::BadFileKind), 107 | }; 108 | 109 | let highlighted = highlight::highlight( 110 | &self.analysis, 111 | &self.project_dir, 112 | path.to_str().unwrap().to_owned(), 113 | text.to_owned(), 114 | ); 115 | 116 | let mut highlighted = highlighted 117 | .lines() 118 | .map(|line| line.replace("
", "\n")) 119 | .collect::>(); 120 | 121 | if text.ends_with('\n') { 122 | highlighted.push(String::new()); 123 | } 124 | 125 | u.highlighted = Some(Highlighted { 126 | source: Some(highlighted), 127 | rendered: None, 128 | }); 129 | } else if ext == "md" || ext == "markdown" { 130 | let text = match text { 131 | Some(t) => t, 132 | None => return Err(::vfs::Error::BadFileKind), 133 | }; 134 | 135 | u.highlighted = Some(Highlighted { 136 | rendered: Some(::comrak::markdown_to_html(text, &Default::default())), 137 | source: Some(raw_lines(text)), 138 | }); 139 | } else if ext == "png" 140 | || ext == "jpg" 141 | || ext == "jpeg" 142 | || ext == "gif" 143 | || ext == "ico" 144 | || ext == "svg" 145 | || ext == "apng" 146 | || ext == "bmp" 147 | { 148 | if let Ok(path) = path.strip_prefix(&self.project_dir) { 149 | u.highlighted = Some(Highlighted { 150 | source: None, 151 | rendered: Some(format!( 152 | r#""#, 153 | &*path.to_string_lossy() 154 | )), 155 | }); 156 | } 157 | } 158 | } 159 | 160 | // Don't try to highlight non-Rust files (and cope with highlighting failure). 161 | if u.highlighted.is_none() { 162 | let text = match text { 163 | Some(t) => t, 164 | None => return Err(::vfs::Error::BadFileKind.into()), 165 | }; 166 | 167 | u.highlighted = Some(Highlighted { 168 | source: Some(raw_lines(text)), 169 | rendered: None, 170 | }); 171 | } 172 | } 173 | 174 | Ok(u.highlighted.clone().unwrap()) 175 | })) 176 | } 177 | 178 | pub fn update_analysis(&self) { 179 | println!("Processing analysis..."); 180 | let workspace_root = self 181 | .config 182 | .workspace_root 183 | .as_ref() 184 | .map(|s| Path::new(s).to_owned()) 185 | .unwrap_or(self.project_dir.clone()); 186 | self.analysis 187 | .reload_with_blacklist( 188 | &self.project_dir, 189 | &workspace_root, 190 | &::blacklist::CRATE_BLACKLIST, 191 | ) 192 | .unwrap(); 193 | 194 | // FIXME Possibly extreme, could invalidate by crate or by file. Also, only 195 | // need to invalidate Rust files. 196 | self.files.clear(); 197 | 198 | println!("done"); 199 | } 200 | 201 | // FIXME we should cache this information rather than compute every time. 202 | pub fn get_symbol_roots(&self) -> Result, String> { 203 | let all_crates = self 204 | .analysis 205 | .def_roots() 206 | .unwrap_or_else(|_| vec![]) 207 | .into_iter() 208 | .filter_map(|(id, name)| { 209 | let span = self.analysis.get_def(id).ok()?.span; 210 | Some(SymbolResult { 211 | id: id.to_string(), 212 | name, 213 | file_name: self.make_file_path(&span).display().to_string(), 214 | line_start: span.range.row_start.one_indexed().0, 215 | }) 216 | }); 217 | 218 | // FIXME Unclear ot sure if we should include dep crates or not here. 219 | // Need to test on workspace crates. Might be nice to have deps in any 220 | // case, in which case we should return the primary crate(s) first. 221 | let metadata = match cargo_metadata::metadata_deps(None, false) { 222 | Ok(metadata) => metadata, 223 | Err(_) => return Err("Could not access cargo metadata".to_owned()), 224 | }; 225 | 226 | let names: Vec = metadata.packages.into_iter().map(|p| p.name).collect(); 227 | 228 | Ok(all_crates.filter(|sr| names.contains(&sr.name)).collect()) 229 | } 230 | 231 | // FIXME we should indicate whether the symbol has children or not 232 | pub fn get_symbol_children(&self, id: Id) -> Result, String> { 233 | self.analysis 234 | .for_each_child_def(id, |id, def| { 235 | let span = &def.span; 236 | SymbolResult { 237 | id: id.to_string(), 238 | name: def.name.clone(), 239 | file_name: self.make_file_path(&span).display().to_string(), 240 | line_start: span.range.row_start.one_indexed().0, 241 | } 242 | }) 243 | .map_err(|e| e.to_string()) 244 | } 245 | 246 | pub fn id_search(&self, id: Id) -> Result { 247 | self.ids_search(vec![id]) 248 | } 249 | 250 | pub fn ident_search(&self, needle: &str) -> Result { 251 | // First see if the needle corresponds to any definitions, if it does, get a list of the 252 | // ids, otherwise, return an empty search result. 253 | let ids = match self.analysis.search_for_id(needle) { 254 | Ok(ids) => ids.to_owned(), 255 | Err(_) => { 256 | return Ok(SearchResult { defs: vec![] }); 257 | } 258 | }; 259 | 260 | self.ids_search(ids) 261 | } 262 | 263 | pub fn find_impls(&self, id: Id) -> Result { 264 | let impls = self 265 | .analysis 266 | .find_impls(id) 267 | .map_err(|_| "No impls found".to_owned())?; 268 | Ok(FindResult { 269 | results: self.make_search_results(impls)?, 270 | }) 271 | } 272 | 273 | fn ids_search(&self, ids: Vec) -> Result { 274 | let mut defs = Vec::new(); 275 | 276 | for id in ids { 277 | // If all_refs.len() > 0, the first entry will be the def. 278 | let all_refs = self.analysis.find_all_refs_by_id(id); 279 | let mut all_refs = match all_refs { 280 | Err(_) => return Err("Error finding references".to_owned()), 281 | Ok(ref all_refs) if all_refs.is_empty() => continue, 282 | Ok(all_refs) => all_refs.into_iter(), 283 | }; 284 | 285 | let def_span = all_refs.next().unwrap(); 286 | let def_path = self.make_file_path(&def_span); 287 | let line = self.make_line_result(&def_path, &def_span)?; 288 | 289 | defs.push(DefResult { 290 | file: def_path.display().to_string(), 291 | line, 292 | refs: self.make_search_results(all_refs.collect())?, 293 | }); 294 | } 295 | 296 | // We then save each bucket of defs/refs as a vec, and put it together to return. 297 | return Ok(SearchResult { defs }); 298 | } 299 | 300 | fn make_file_path(&self, span: &Span) -> PathBuf { 301 | let file_path = Path::new(&span.file); 302 | file_path 303 | .strip_prefix(&self.project_dir) 304 | .unwrap_or(file_path) 305 | .to_owned() 306 | } 307 | 308 | fn make_line_result(&self, file_path: &Path, span: &Span) -> Result { 309 | let (text, pre, post) = match self.get_highlighted(file_path) { 310 | Ok(Highlighted { 311 | source: Some(lines), 312 | .. 313 | }) => { 314 | let line = span.range.row_start.0 as i32; 315 | let text = lines[line as usize].clone(); 316 | 317 | let mut ctx_start = line - CONTEXT_SIZE; 318 | if ctx_start < 0 { 319 | ctx_start = 0; 320 | } 321 | let mut ctx_end = line + CONTEXT_SIZE; 322 | if ctx_end >= lines.len() as i32 { 323 | ctx_end = lines.len() as i32 - 1; 324 | } 325 | let pre = lines[ctx_start as usize..line as usize].join("\n"); 326 | let post = lines[line as usize + 1..=ctx_end as usize].join("\n"); 327 | 328 | (text, pre, post) 329 | } 330 | Ok(_) => { 331 | return Err(format!( 332 | "Not a text file: {}", 333 | &*file_path.to_string_lossy() 334 | )) 335 | } 336 | Err(_) => return Err(format!("Error finding text for {:?}", span)), 337 | }; 338 | Ok(LineResult::new(span, text, pre, post)) 339 | } 340 | 341 | pub fn get_raw(&self, path: &Path) -> Result<::vfs::FileContents, ::vfs::Error> { 342 | self.files.load_file(path) 343 | } 344 | 345 | // Sorts a set of search results into buckets by file. 346 | fn make_search_results(&self, raw: Vec) -> Result, String> { 347 | let mut file_buckets = HashMap::new(); 348 | 349 | for span in &raw { 350 | let file_path = self.make_file_path(span); 351 | let line = match self.make_line_result(&file_path, span) { 352 | Ok(l) => l, 353 | Err(_) => continue, 354 | }; 355 | file_buckets 356 | .entry(file_path.display().to_string()) 357 | .or_insert_with(|| vec![]) 358 | .push(line); 359 | } 360 | 361 | let mut result = vec![]; 362 | for (file_path, mut lines) in file_buckets.into_iter() { 363 | lines.sort(); 364 | let per_file = FileResult { 365 | file_name: file_path, 366 | lines: lines, 367 | }; 368 | result.push(per_file); 369 | } 370 | result.sort(); 371 | Ok(result) 372 | } 373 | } 374 | -------------------------------------------------------------------------------- /src/file_controller/results.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2016 The Rustw Project Developers. 2 | // 3 | // Licensed under the Apache License, Version 2.0 or the MIT license 5 | // , at your 6 | // option. This file may not be copied, modified, or distributed 7 | // except according to those terms. 8 | 9 | use super::Span; 10 | 11 | // The number of lines before and after a result line to use as context. 12 | pub const CONTEXT_SIZE: i32 = 3; 13 | 14 | #[derive(Serialize, Debug, Clone)] 15 | pub struct SearchResult { 16 | pub defs: Vec, 17 | } 18 | 19 | #[derive(Serialize, Debug, Clone)] 20 | pub struct DefResult { 21 | pub file: String, 22 | pub line: LineResult, 23 | pub refs: Vec, 24 | } 25 | 26 | #[derive(Serialize, Debug, Clone, Eq, PartialEq, Ord, PartialOrd)] 27 | pub struct FileResult { 28 | pub file_name: String, 29 | pub lines: Vec, 30 | } 31 | 32 | #[derive(Serialize, Debug, Clone, Eq, PartialEq, Ord, PartialOrd)] 33 | pub struct LineResult { 34 | pub line_start: u32, 35 | pub column_start: u32, 36 | pub column_end: u32, 37 | pub line: String, 38 | pub pre_context: String, 39 | pub post_context: String, 40 | } 41 | 42 | impl LineResult { 43 | pub fn new(span: &Span, line: String, pre_context: String, post_context: String) -> LineResult { 44 | LineResult { 45 | line_start: span.range.row_start.one_indexed().0, 46 | column_start: span.range.col_start.one_indexed().0, 47 | column_end: span.range.col_end.one_indexed().0, 48 | line, 49 | pre_context, 50 | post_context, 51 | } 52 | } 53 | } 54 | 55 | #[derive(Serialize, Debug, Clone)] 56 | pub struct FindResult { 57 | pub results: Vec, 58 | } 59 | 60 | #[derive(Serialize, Debug, Clone)] 61 | pub struct SymbolResult { 62 | pub id: String, 63 | pub name: String, 64 | pub file_name: String, 65 | pub line_start: u32, 66 | } 67 | -------------------------------------------------------------------------------- /src/highlight.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2016 The Rustw Project Developers. 2 | // 3 | // Licensed under the Apache License, Version 2.0 or the MIT license 5 | // , at your 6 | // option. This file may not be copied, modified, or distributed 7 | // except according to those terms. 8 | 9 | // Syntax highlighting. 10 | 11 | use std::collections::HashMap; 12 | use std::fmt::Display; 13 | use std::io::{self, Write}; 14 | use std::path::{Path, PathBuf}; 15 | use std::time::Instant; 16 | 17 | use rustc_parse::lexer; 18 | use rustdoc_highlight::{self as highlight, Class, Classifier}; 19 | use span; 20 | use syntax; 21 | use syntax::sess::ParseSess; 22 | use syntax::source_map::{Loc, SourceMap}; 23 | use syntax::token::Token; 24 | use syntax_expand::config::process_configure_mod; 25 | use syntax_pos::edition; 26 | use syntax_pos::FileName; 27 | 28 | use analysis::AnalysisHost; 29 | use analysis::DefKind; 30 | 31 | type Span = span::Span; 32 | 33 | pub fn with_globals(f: impl FnOnce() -> R) -> R { 34 | syntax::with_globals(edition::DEFAULT_EDITION, f) 35 | } 36 | 37 | pub fn highlight<'a>( 38 | analysis: &'a AnalysisHost, 39 | project_path: &'a Path, 40 | file_name: String, 41 | file_text: String, 42 | ) -> String { 43 | debug!("highlight `{}` in `{}`", file_text, file_name); 44 | 45 | with_globals(|| { 46 | let sess = ParseSess::with_silent_emitter(process_configure_mod); 47 | let fm = sess 48 | .source_map() 49 | .new_source_file(FileName::Real(PathBuf::from(&file_name)), file_text); 50 | 51 | let mut out = Highlighter::new(analysis, project_path, sess.source_map()); 52 | 53 | let t_start = Instant::now(); 54 | 55 | let mut classifier = 56 | Classifier::new(lexer::StringReader::new(&sess, fm, None), sess.source_map()); 57 | classifier.write_source(&mut out).unwrap_or(()); 58 | 59 | let time = t_start.elapsed(); 60 | info!( 61 | "Highlighting {} in {:.3}s", 62 | file_name, 63 | time.as_secs() as f64 + time.subsec_nanos() as f64 / 1_000_000_000.0 64 | ); 65 | 66 | String::from_utf8_lossy(&out.buf).into_owned() 67 | }) 68 | } 69 | 70 | struct Highlighter<'a> { 71 | buf: Vec, 72 | analysis: &'a AnalysisHost, 73 | source_map: &'a SourceMap, 74 | project_path: &'a Path, 75 | } 76 | 77 | impl<'a> Highlighter<'a> { 78 | fn new( 79 | analysis: &'a AnalysisHost, 80 | project_path: &'a Path, 81 | source_map: &'a SourceMap, 82 | ) -> Highlighter<'a> { 83 | Highlighter { 84 | buf: vec![], 85 | analysis, 86 | source_map, 87 | project_path, 88 | } 89 | } 90 | 91 | fn get_link(&self, span: &Span) -> Option { 92 | self.analysis.goto_def(span).ok().and_then(|def_span| { 93 | if span == &def_span { 94 | None 95 | } else { 96 | Some(loc_for_span(&def_span, self.project_path)) 97 | } 98 | }) 99 | } 100 | 101 | fn span_from_locs(&mut self, lo: &Loc, hi: &Loc) -> Span { 102 | Span::new( 103 | span::Row::new_one_indexed(lo.line as u32).zero_indexed(), 104 | span::Row::new_one_indexed(hi.line as u32).zero_indexed(), 105 | span::Column::new_zero_indexed(lo.col.0 as u32), 106 | span::Column::new_zero_indexed(hi.col.0 as u32), 107 | file_path_for_loc(lo), 108 | ) 109 | } 110 | } 111 | 112 | fn file_path_for_loc(loc: &Loc) -> PathBuf { 113 | match loc.file.name { 114 | FileName::Real(ref path) => path.canonicalize().unwrap(), 115 | ref f => panic!("Expected real path, found {:?}", f), 116 | } 117 | } 118 | 119 | pub fn write_span( 120 | buf: &mut Vec, 121 | klass: Class, 122 | extra_class: Option, 123 | text: String, 124 | src_link: bool, 125 | extra: HashMap, 126 | ) -> io::Result<()> { 127 | write!(buf, "{}", text) 148 | } 149 | 150 | fn push_char(buf: &mut Vec, c: char) -> io::Result<()> { 151 | match c { 152 | '>' => write!(buf, ">"), 153 | '<' => write!(buf, "<"), 154 | '&' => write!(buf, "&"), 155 | '\'' => write!(buf, "'"), 156 | '"' => write!(buf, """), 157 | '\n' => write!(buf, "
"), 158 | _ => write!(buf, "{}", c), 159 | } 160 | } 161 | 162 | fn loc_for_span(span: &Span, project_path: &Path) -> String { 163 | let file_name = Path::new(&span.file) 164 | .strip_prefix(project_path) 165 | .ok() 166 | .unwrap_or(&span.file) 167 | .to_str() 168 | .unwrap(); 169 | format!( 170 | "{}:{}:{}:{}:{}", 171 | file_name, 172 | span.range.row_start.one_indexed().0, 173 | span.range.col_start.one_indexed().0, 174 | span.range.row_end.one_indexed().0, 175 | span.range.col_end.one_indexed().0 176 | ) 177 | } 178 | 179 | macro_rules! maybe_insert { 180 | ($h: expr, $k: expr, $v: expr) => { 181 | if let Some(v) = $v { 182 | $h.insert($k.to_owned(), v); 183 | } 184 | }; 185 | } 186 | 187 | impl<'a> highlight::Writer for Highlighter<'a> { 188 | fn enter_span(&mut self, klass: Class) -> io::Result<()> { 189 | write!(self.buf, "", klass.rustdoc_class()) 190 | } 191 | 192 | fn exit_span(&mut self) -> io::Result<()> { 193 | write!(self.buf, "") 194 | } 195 | 196 | fn string(&mut self, text: T, klass: Class, tok: Option) -> io::Result<()> { 197 | let text = text.to_string(); 198 | 199 | match klass { 200 | Class::None => write!(self.buf, "{}", text), 201 | Class::Ident | Class::Self_ => { 202 | match tok { 203 | Some(t) => { 204 | let lo = self.source_map.lookup_char_pos(t.span.lo()); 205 | let hi = self.source_map.lookup_char_pos(t.span.hi()); 206 | // FIXME should be able to get all this info with a single query of analysis 207 | let span = &self.span_from_locs(&lo, &hi); 208 | let ty = self.analysis.show_type(span).ok().and_then(|s| { 209 | if s.is_empty() { 210 | None 211 | } else { 212 | Some(s) 213 | } 214 | }); 215 | let docs = self.analysis.docs(span).ok().and_then(|s| { 216 | if s.is_empty() { 217 | None 218 | } else { 219 | Some(s) 220 | } 221 | }); 222 | let title = match (ty, docs) { 223 | (Some(t), Some(d)) => Some(format!("{}\n\n{}", t, d)), 224 | (Some(t), _) => Some(t), 225 | (_, Some(d)) => Some(d), 226 | (None, None) => None, 227 | }; 228 | let mut link = self.get_link(span); 229 | let doc_link = self.analysis.doc_url(span).ok(); 230 | let src_link = self.analysis.src_url(span).ok(); 231 | 232 | let (css_class, impls) = match self.analysis.id(span) { 233 | Ok(id) => { 234 | if link.is_none() { 235 | link = Some(format!("search:{}", id)); 236 | } 237 | let css_class = format!(" class_id class_id_{}", id); 238 | 239 | let impls = match self.analysis.get_def(id) { 240 | Ok(def) => match def.kind { 241 | DefKind::Enum 242 | | DefKind::Struct 243 | | DefKind::Union 244 | | DefKind::Trait => self 245 | .analysis 246 | .find_impls(id) 247 | .map(|v| v.len()) 248 | .unwrap_or(0), 249 | _ => 0, 250 | }, 251 | Err(_) => 0, 252 | }; 253 | 254 | (Some(css_class), impls) 255 | } 256 | Err(_) => (None, 0), 257 | }; 258 | 259 | let has_link = doc_link.is_some() || link.is_some(); 260 | 261 | let mut extra = HashMap::new(); 262 | maybe_insert!(extra, "title", title); 263 | maybe_insert!(extra, "data-link", link); 264 | maybe_insert!(extra, "data-doc-link", doc_link); 265 | maybe_insert!(extra, "data-src-link", src_link); 266 | extra.insert("data-impls".to_owned(), impls.to_string()); 267 | 268 | write_span( 269 | &mut self.buf, 270 | Class::Ident, 271 | css_class, 272 | text, 273 | has_link, 274 | extra, 275 | ) 276 | } 277 | None => write_span( 278 | &mut self.buf, 279 | Class::Ident, 280 | None, 281 | text, 282 | false, 283 | HashMap::new(), 284 | ), 285 | } 286 | } 287 | Class::RefKeyWord if text == "*" => match tok { 288 | Some(t) => { 289 | let lo = self.source_map.lookup_char_pos(t.span.lo()); 290 | let hi = self.source_map.lookup_char_pos(t.span.hi()); 291 | let span = &self.span_from_locs(&lo, &hi); 292 | let mut extra = HashMap::new(); 293 | extra.insert( 294 | "data-location".to_owned(), 295 | format!("{}:{}", lo.line, lo.col.0 + 1), 296 | ); 297 | maybe_insert!(extra, "title", self.analysis.show_type(span).ok()); 298 | let css_class = Some(" glob".to_owned()); 299 | 300 | write_span(&mut self.buf, Class::Op, css_class, text, false, extra) 301 | } 302 | None => write_span(&mut self.buf, Class::Op, None, text, false, HashMap::new()), 303 | }, 304 | klass => write_span(&mut self.buf, klass, None, text, false, HashMap::new()), 305 | } 306 | } 307 | } 308 | 309 | pub trait GetBuf { 310 | fn get_buf(&self) -> &[u8]; 311 | } 312 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2016 The Rustw Project Developers. 2 | // 3 | // Licensed under the Apache License, Version 2.0 or the MIT license 5 | // , at your 6 | // option. This file may not be copied, modified, or distributed 7 | // except according to those terms. 8 | 9 | #![feature(const_fn)] 10 | #![feature(type_ascription)] 11 | // For libsyntax, which is just a hack to get around rustdoc. 12 | #![feature(rustc_private)] 13 | #![feature(integer_atomics)] 14 | 15 | extern crate cargo_metadata; 16 | extern crate futures; 17 | extern crate hyper; 18 | #[macro_use] 19 | extern crate log; 20 | extern crate rls_analysis as analysis; 21 | extern crate rls_blacklist as blacklist; 22 | extern crate rls_span as span; 23 | extern crate rls_vfs as vfs; 24 | extern crate rustc_parse; 25 | extern crate rustdoc_highlight; 26 | extern crate serde; 27 | #[macro_use] 28 | extern crate serde_derive; 29 | extern crate comrak; 30 | extern crate serde_json; 31 | extern crate syntax; 32 | extern crate syntax_expand; 33 | extern crate syntax_pos; 34 | extern crate toml; 35 | extern crate url; 36 | 37 | use config::Config; 38 | use hyper::server::Http; 39 | use std::fs::File; 40 | use std::io::Read; 41 | use std::process::Command; 42 | 43 | pub use build::BuildArgs; 44 | 45 | mod build; 46 | pub mod config; 47 | mod file_controller; 48 | mod highlight; 49 | mod listings; 50 | mod server; 51 | 52 | pub fn run_server(mut build_args: BuildArgs) { 53 | let config = load_config(&build_args); 54 | let ip = config.ip.clone(); 55 | let port = config.port; 56 | 57 | let url = format!("http://{}:{}", ip, port); 58 | println!("server running on {}", url); 59 | 60 | if let Some(i) = build_args.args.iter().position(|a| a == "--open") { 61 | println!("opening..."); 62 | if let Err(cmds) = open_browser(&url) { 63 | println!("Could not open browser, tried: {:?}", cmds); 64 | } 65 | build_args.args.remove(i); 66 | } 67 | 68 | let addr = format!("{}:{}", ip, port).parse().unwrap(); 69 | let server = server::Server::new(config.clone(), build_args.clone()); 70 | let instance = server::Instance::new(server); 71 | Http::new() 72 | .bind(&addr, move || Ok(instance.clone())) 73 | .unwrap() 74 | .run() 75 | .unwrap(); 76 | } 77 | 78 | fn load_config(build_args: &BuildArgs) -> Config { 79 | let config_file = File::open("rustw.toml"); 80 | let mut toml = String::new(); 81 | if let Ok(mut f) = config_file { 82 | f.read_to_string(&mut toml).unwrap(); 83 | } 84 | let mut config = Config::from_toml(&toml); 85 | 86 | if config.workspace_root.is_none() { 87 | config.workspace_root = Some(build_args.workspace_root.clone()) 88 | } 89 | 90 | config 91 | } 92 | 93 | // The following functions are adapted from `cargo doc`. 94 | // If OK, they return the command line used to launch the browser, if there is a 95 | // failure, they return the command lines tried. 96 | #[cfg(not(any(target_os = "windows", target_os = "macos")))] 97 | fn open_browser(uri: &str) -> Result<&'static str, Vec<&'static str>> { 98 | use std::env; 99 | 100 | let mut methods = Vec::new(); 101 | // trying $BROWSER 102 | match env::var("BROWSER") { 103 | Ok(name) => match Command::new(name).arg(uri).status() { 104 | Ok(_) => return Ok("$BROWSER"), 105 | Err(_) => methods.push("$BROWSER"), 106 | }, 107 | Err(_) => (), // Do nothing here if $BROWSER is not found 108 | } 109 | 110 | for m in ["xdg-open", "gnome-open", "kde-open"].iter() { 111 | match Command::new(m).arg(uri).status() { 112 | Ok(_) => return Ok(m), 113 | Err(_) => methods.push(m), 114 | } 115 | } 116 | 117 | Err(methods) 118 | } 119 | 120 | #[cfg(target_os = "windows")] 121 | fn open_browser(uri: &str) -> Result<&'static str, Vec<&'static str>> { 122 | match Command::new("cmd").arg("/C").arg(uri).status() { 123 | Ok(_) => return Ok("cmd /C"), 124 | Err(_) => return Err(vec!["cmd /C"]), 125 | }; 126 | } 127 | 128 | #[cfg(target_os = "macos")] 129 | fn open_browser(uri: &str) -> Result<&'static str, Vec<&'static str>> { 130 | match Command::new("open").arg(uri).status() { 131 | Ok(_) => return Ok("open"), 132 | Err(_) => return Err(vec!["open"]), 133 | }; 134 | } 135 | -------------------------------------------------------------------------------- /src/listings.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2016 The Rustw Project Developers. 2 | // 3 | // Licensed under the Apache License, Version 2.0 or the MIT license 5 | // , at your 6 | // option. This file may not be copied, modified, or distributed 7 | // except according to those terms. 8 | 9 | use std::cmp::{Ord, Ordering, PartialOrd}; 10 | use std::path::{Path, PathBuf}; 11 | 12 | #[derive(Serialize, Debug, Clone)] 13 | pub struct DirectoryListing { 14 | pub path: PathBuf, 15 | pub files: Vec, 16 | } 17 | 18 | #[derive(Serialize, Debug, Clone, Eq, PartialEq, Ord, PartialOrd)] 19 | pub struct Listing { 20 | pub kind: ListingKind, 21 | pub name: String, 22 | pub path: String, 23 | } 24 | 25 | #[derive(Serialize, Debug, Clone, Eq, PartialEq)] 26 | pub enum ListingKind { 27 | Directory, 28 | DirectoryTree(Vec), 29 | File, 30 | } 31 | 32 | impl PartialOrd for ListingKind { 33 | fn partial_cmp(&self, other: &ListingKind) -> Option { 34 | Some(self.cmp(other)) 35 | } 36 | } 37 | impl Ord for ListingKind { 38 | fn cmp(&self, other: &ListingKind) -> Ordering { 39 | if *self == ListingKind::File && *other == ListingKind::File { 40 | Ordering::Equal 41 | } else if *self == ListingKind::File { 42 | Ordering::Greater 43 | } else if *other == ListingKind::File { 44 | Ordering::Less 45 | } else { 46 | Ordering::Equal 47 | } 48 | } 49 | } 50 | 51 | impl DirectoryListing { 52 | pub fn from_path(path: &Path, recurse: bool) -> Result { 53 | Ok(DirectoryListing { 54 | path: path.to_owned(), 55 | files: Self::list_files(path, recurse)?, 56 | }) 57 | } 58 | 59 | fn list_files(path: &Path, recurse: bool) -> Result, String> { 60 | let mut files = vec![]; 61 | let dir = match path.read_dir() { 62 | Ok(d) => d, 63 | Err(s) => return Err(s.to_string()), 64 | }; 65 | for entry in dir { 66 | if let Ok(entry) = entry { 67 | let name = entry.file_name().to_str().unwrap().to_owned(); 68 | let path = entry.path().to_str().unwrap().to_owned(); 69 | if let Ok(file_type) = entry.file_type() { 70 | if file_type.is_dir() { 71 | if recurse { 72 | let nested = Self::list_files(&entry.path(), true)?; 73 | files.push(Listing { 74 | kind: ListingKind::DirectoryTree(nested), 75 | name, 76 | path, 77 | }); 78 | } else { 79 | files.push(Listing { 80 | kind: ListingKind::Directory, 81 | name, 82 | path, 83 | }); 84 | } 85 | } else if file_type.is_file() { 86 | files.push(Listing { 87 | kind: ListingKind::File, 88 | name, 89 | path, 90 | }); 91 | } 92 | } 93 | } 94 | } 95 | 96 | files.sort(); 97 | Ok(files) 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/server.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2016-2018 The Rustw Project Developers. 2 | // 3 | // Licensed under the Apache License, Version 2.0 or the MIT license 5 | // , at your 6 | // option. This file may not be copied, modified, or distributed 7 | // except according to those terms. 8 | 9 | //! This module handles the server responsibilities - it routes incoming requests 10 | //! and dispatches them. It also handles pushing events to the client during 11 | //! builds and making pull-able data available to the client post-build. 12 | 13 | use analysis; 14 | use build::{self, BuildArgs}; 15 | use config::Config; 16 | use file_controller::Cache; 17 | use futures; 18 | use futures::Future; 19 | use listings::{DirectoryListing, Listing}; 20 | 21 | use std::fmt; 22 | use std::path::{Path, PathBuf}; 23 | use std::process::{self, Command}; 24 | use std::str::FromStr; 25 | use std::sync::{ 26 | atomic::{AtomicU32, Ordering}, 27 | Arc, Mutex, 28 | }; 29 | use std::thread; 30 | 31 | use hyper::error::Error; 32 | use hyper::header::ContentType; 33 | use hyper::server::Request; 34 | use hyper::server::Response; 35 | use hyper::server::Service; 36 | use hyper::StatusCode; 37 | use serde_json; 38 | use span; 39 | 40 | // Generated by the build script. 41 | include!(concat!(env!("OUT_DIR"), "/lookup_static.rs")); 42 | 43 | /// An instance of the server. Runs a session of rustw. 44 | pub struct Server { 45 | builder: build::Builder, 46 | pub config: Arc, 47 | file_cache: Arc, 48 | status: Status, 49 | } 50 | 51 | #[derive(Clone)] 52 | pub struct Instance { 53 | server: Arc>, 54 | } 55 | 56 | impl Server { 57 | pub(super) fn new(config: Config, build_args: BuildArgs) -> Server { 58 | let config = Arc::new(config); 59 | 60 | let mut instance = Server { 61 | builder: build::Builder::new(config.clone(), build_args), 62 | file_cache: Arc::new(Cache::new(config.clone())), 63 | config, 64 | status: Status::new(), 65 | }; 66 | 67 | instance.run_analysis(); 68 | 69 | instance 70 | } 71 | 72 | fn run_analysis(&mut self) { 73 | let file_cache = self.file_cache.clone(); 74 | let status = self.status.clone(); 75 | let builder = self.builder.clone(); 76 | 77 | thread::spawn(move || { 78 | println!("Building..."); 79 | status.start_build(); 80 | let code = builder.build().unwrap(); 81 | status.finish_build(); 82 | 83 | // Test specifically for `1` rather than `!= 0` since a compilation 84 | // failure gives `101`, and we want to continue then. 85 | if code == 1 { 86 | process::exit(1); 87 | } 88 | status.start_analysis(); 89 | file_cache.update_analysis(); 90 | status.finish_analysis(); 91 | }); 92 | } 93 | } 94 | 95 | impl Instance { 96 | pub fn new(server: Server) -> Instance { 97 | Instance { 98 | server: Arc::new(Mutex::new(server)), 99 | } 100 | } 101 | } 102 | 103 | impl Service for Instance { 104 | type Request = Request; 105 | type Response = Response; 106 | type Error = Error; 107 | type Future = Box>; 108 | 109 | fn call(&self, req: Request) -> Self::Future { 110 | let uri = req.uri().clone(); 111 | self.server 112 | .lock() 113 | .unwrap() 114 | .route(uri.path(), uri.query(), req) 115 | } 116 | } 117 | 118 | struct Status_ { 119 | build: AtomicU32, 120 | analysis: AtomicU32, 121 | blocked: Mutex>>, 122 | } 123 | 124 | #[derive(Clone)] 125 | pub struct Status { 126 | internal: Arc, 127 | } 128 | 129 | impl Status { 130 | fn new() -> Status { 131 | Status { 132 | internal: Arc::new(Status_ { 133 | build: AtomicU32::new(0), 134 | analysis: AtomicU32::new(0), 135 | blocked: Mutex::new(Vec::new()), 136 | }), 137 | } 138 | } 139 | 140 | fn block(&self) -> impl Future { 141 | let (c, o) = futures::oneshot(); 142 | { 143 | let mut blocked = self.internal.blocked.lock().unwrap(); 144 | if self.internal.build.load(Ordering::SeqCst) == 0 145 | && self.internal.analysis.load(Ordering::SeqCst) == 0 146 | { 147 | return futures::future::Either::A(futures::future::ok(())); 148 | } 149 | blocked.push(c); 150 | } 151 | futures::future::Either::B(o) 152 | } 153 | fn start_build(&self) { 154 | self.internal.build.fetch_add(1, Ordering::SeqCst); 155 | } 156 | fn start_analysis(&self) { 157 | self.internal.analysis.fetch_add(1, Ordering::SeqCst); 158 | } 159 | fn finish_build(&self) { 160 | self.internal.build.fetch_sub(1, Ordering::SeqCst); 161 | } 162 | fn finish_analysis(&self) { 163 | self.internal.analysis.fetch_sub(1, Ordering::SeqCst); 164 | let mut blocked = self.internal.blocked.lock().unwrap(); 165 | blocked.drain(..).for_each(|c| c.send(()).unwrap()); 166 | } 167 | } 168 | 169 | impl fmt::Display for Status { 170 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 171 | if self.internal.build.load(Ordering::SeqCst) > 0 { 172 | write!(f, "Building") 173 | } else if self.internal.analysis.load(Ordering::SeqCst) > 0 { 174 | write!(f, "Analysis") 175 | } else { 176 | write!(f, "Done") 177 | } 178 | } 179 | } 180 | 181 | impl Server { 182 | fn route( 183 | &self, 184 | mut path: &str, 185 | query: Option<&str>, 186 | req: Request, 187 | ) -> ::Future { 188 | trace!("route: path: {:?}, query: {:?}", path, query); 189 | 190 | if path.starts_with('/') { 191 | path = &path[1..]; 192 | } 193 | let path: Vec<_> = path.split('/').collect(); 194 | 195 | let result = if path[0] == GET_STATUS { 196 | self.handle_status(req) 197 | } else if path[0] == STATIC_REQUEST { 198 | self.handle_static(req, &path[1..]) 199 | } else if path[0] == CONFIG_REQUEST { 200 | self.handle_config(req) 201 | } else if path[0] == SOURCE_REQUEST || path[0] == TREE_REQUEST { 202 | let recurse = path[0] == TREE_REQUEST; 203 | let path = &path[1..]; 204 | // Because a URL ending in "/." is normalised to "/", we miss out on "." as a source path. 205 | // We try to correct for that here. 206 | let arg = if path.len() == 1 && path[0] == "" { 207 | &["."] 208 | } else { 209 | path 210 | }; 211 | 212 | self.handle_src(req, arg, recurse) 213 | } else if path[0] == PLAIN_TEXT { 214 | self.handle_plain_text(req, query) 215 | } else if path[0] == RAW_REQUEST { 216 | self.handle_raw(req, &path[1..]) 217 | } else if path[0] == SEARCH_REQUEST { 218 | self.handle_search(req, query) 219 | } else if path[0] == FIND_REQUEST { 220 | self.handle_find(req, query) 221 | } else if path[0] == SYMBOL_ROOTS { 222 | return Box::new(self.handle_sym_roots(req)); 223 | } else if path[0] == SYMBOL_CHILDREN { 224 | self.handle_sym_childen(req, query) 225 | } else if !self.config.demo_mode && path[0] == EDIT_REQUEST { 226 | self.handle_edit(req, query) 227 | } else { 228 | self.handle_index(req) 229 | }; 230 | 231 | Box::new(futures::future::ok(result)) 232 | } 233 | 234 | fn handle_error(&self, _req: Request, status: StatusCode, msg: String) -> Response { 235 | debug!("ERROR: {} ({})", msg, status); 236 | 237 | Response::new().with_status(status).with_body(msg) 238 | } 239 | 240 | fn handle_status(&self, _req: Request) -> Response { 241 | let mut res = Response::new(); 242 | res.headers_mut().set(ContentType::plaintext()); 243 | res.with_body(format!("{{\"status\":\"{}\"}}", self.status)) 244 | } 245 | 246 | fn handle_index(&self, _req: Request) -> Response { 247 | self.handle_static(_req, &["index.html"]) 248 | } 249 | 250 | fn handle_static(&self, req: Request, path: &[&str]) -> Response { 251 | let mut path_buf = PathBuf::new(); 252 | for p in path { 253 | path_buf.push(p); 254 | } 255 | trace!("handle_static: requesting `{}`", path_buf.to_str().unwrap()); 256 | 257 | let content_type = match path_buf.extension() { 258 | Some(s) if s.to_str().unwrap() == "html" => ContentType::html(), 259 | Some(s) if s.to_str().unwrap() == "css" => ContentType("text/css".parse().unwrap()), 260 | Some(s) if s.to_str().unwrap() == "json" => ContentType::json(), 261 | _ => ContentType("application/octet-stream".parse().unwrap()), 262 | }; 263 | 264 | let file_contents = lookup_static_file(&path_buf.to_str().unwrap()); 265 | if let Ok(bytes) = file_contents { 266 | trace!( 267 | "handle_static: serving `{}`. {} bytes, {}", 268 | path_buf.to_str().unwrap(), 269 | bytes.len(), 270 | content_type 271 | ); 272 | let mut res = Response::new(); 273 | res.headers_mut().set(content_type); 274 | return res.with_body(bytes); 275 | } 276 | 277 | trace!("404 {:?}", file_contents); 278 | self.handle_error(req, StatusCode::NotFound, "Page not found".to_owned()) 279 | } 280 | 281 | fn handle_raw(&self, req: Request, path: &[&str]) -> Response { 282 | for p in path { 283 | // In demo mode this might reveal the contents of the server outside 284 | // the source directory (really, rustw should run in a sandbox, but 285 | // hey, FIXME). 286 | if p.contains("..") || *p == "/" { 287 | return self.handle_error( 288 | req, 289 | StatusCode::InternalServerError, 290 | "Bad path, found `..`".to_owned(), 291 | ); 292 | } 293 | } 294 | 295 | match self.file_cache.get_raw(&path.iter().collect::()) { 296 | Ok(::vfs::FileContents::Text(text)) => { 297 | let mut res = Response::new(); 298 | res.headers_mut().set(ContentType::plaintext()); 299 | res.with_body(text) 300 | } 301 | Ok(::vfs::FileContents::Binary(bin)) => { 302 | let res = Response::new(); 303 | res.with_body(bin) 304 | } 305 | Err(e) => self.handle_error(req, StatusCode::NotFound, e.to_string()), 306 | } 307 | } 308 | 309 | fn handle_src(&self, req: Request, mut path: &[&str], recurse: bool) -> Response { 310 | use file_controller::Highlighted; 311 | 312 | fn path_parts(path: &Path) -> Vec { 313 | path.components() 314 | .map(|c| c.as_os_str().to_str().unwrap().to_owned()) 315 | .collect() 316 | } 317 | 318 | for p in path { 319 | // In demo mode this might reveal the contents of the server outside 320 | // the source directory (really, rustw should run in a sandbox, but 321 | // hey, FIXME). 322 | if p.contains("..") || *p == "/" { 323 | return self.handle_error( 324 | req, 325 | StatusCode::InternalServerError, 326 | "Bad path, found `..`".to_owned(), 327 | ); 328 | } 329 | } 330 | 331 | let mut path_buf = PathBuf::new(); 332 | if path[0].is_empty() { 333 | path_buf.push("/"); 334 | path = &path[1..]; 335 | } 336 | for p in path { 337 | if cfg!(windows) { 338 | let mut chars = p.chars(); 339 | match (chars.next(), chars.next(), chars.next()) { 340 | (Some(drive_letter), Some(colon), None) 341 | if drive_letter.is_ascii_alphabetic() && colon == ':' => 342 | { 343 | path_buf.push(&[drive_letter, colon, '\\'].iter().collect::()); 344 | } 345 | _ => path_buf.push(p), 346 | } 347 | } else { 348 | path_buf.push(p); 349 | } 350 | } 351 | 352 | // FIXME should cache directory listings too 353 | if path_buf.is_dir() { 354 | match DirectoryListing::from_path(&path_buf, recurse) { 355 | Ok(listing) => { 356 | let mut res = Response::new(); 357 | res.headers_mut().set(ContentType::json()); 358 | let path = path_parts(&listing.path); 359 | let result = SourceResult::Directory { 360 | path, 361 | files: listing.files, 362 | }; 363 | res.with_body(serde_json::to_string(&result).unwrap()) 364 | } 365 | Err(msg) => self.handle_error(req, StatusCode::InternalServerError, msg), 366 | } 367 | } else { 368 | match self.file_cache.get_highlighted(&path_buf) { 369 | Ok(Highlighted { 370 | ref source, 371 | ref rendered, 372 | }) => { 373 | let mut res = Response::new(); 374 | res.headers_mut().set(ContentType::json()); 375 | let path = path_parts(&path_buf); 376 | let result = SourceResult::File { 377 | path, 378 | lines: source.as_ref().map(|s| s.as_ref()), 379 | rendered: rendered.as_ref().map(|s| s.as_ref()), 380 | }; 381 | res.with_body(serde_json::to_string(&result).unwrap()) 382 | } 383 | Err(msg) => self.handle_error(req, StatusCode::InternalServerError, msg), 384 | } 385 | } 386 | } 387 | 388 | fn handle_config(&self, _req: Request) -> Response { 389 | let text = serde_json::to_string(&*self.config).unwrap(); 390 | let mut res = Response::new(); 391 | res.headers_mut().set(ContentType::json()); 392 | return res.with_body(text); 393 | } 394 | 395 | fn handle_edit(&self, _req: Request, query: Option<&str>) -> Response { 396 | assert!(!self.config.demo_mode, "Edit shouldn't happen in demo mode"); 397 | assert!(self.config.unstable_features, "Edit is unstable"); 398 | 399 | match parse_query_value(query, "file=") { 400 | Some(location) => { 401 | // Split the 'filename' on colons for line and column numbers. 402 | let args = parse_location_string(&location); 403 | 404 | let cmd_line = &self.config.edit_command; 405 | if !cmd_line.is_empty() { 406 | let cmd_line = cmd_line 407 | .replace("$file", &args[0]) 408 | .replace("$line", &args[1]) 409 | .replace("$col", &args[2]); 410 | 411 | let mut splits = cmd_line.split(' '); 412 | 413 | let mut cmd = Command::new(splits.next().unwrap()); 414 | for arg in splits { 415 | cmd.arg(arg); 416 | } 417 | 418 | match cmd.spawn() { 419 | Ok(_) => debug!("edit, launched successfully"), 420 | Err(e) => debug!("edit, launch failed: `{:?}`, command: `{}`", e, cmd_line), 421 | } 422 | } 423 | 424 | let mut res = Response::new(); 425 | res.headers_mut().set(ContentType::json()); 426 | return res.with_body("{}".as_bytes()); 427 | } 428 | None => { 429 | return self.handle_error( 430 | _req, 431 | StatusCode::InternalServerError, 432 | format!("Bad query string: {:?}", query), 433 | ); 434 | } 435 | } 436 | } 437 | 438 | fn handle_search(&self, _req: Request, query: Option<&str>) -> Response { 439 | match ( 440 | parse_query_value(query, "needle="), 441 | parse_query_value(query, "id="), 442 | ) { 443 | (Some(needle), None) => { 444 | // Identifier search. 445 | match self.file_cache.ident_search(&needle) { 446 | Ok(data) => { 447 | let mut res = Response::new(); 448 | res.headers_mut().set(ContentType::json()); 449 | return res.with_body(serde_json::to_string(&data).unwrap()); 450 | } 451 | Err(s) => { 452 | return self.handle_error(_req, StatusCode::InternalServerError, s); 453 | } 454 | } 455 | } 456 | (None, Some(id)) => { 457 | // Search by id. 458 | let id = match u64::from_str(&id) { 459 | Ok(l) => l, 460 | Err(_) => { 461 | return self.handle_error( 462 | _req, 463 | StatusCode::InternalServerError, 464 | format!("Bad id: {}", id), 465 | ); 466 | } 467 | }; 468 | match self.file_cache.id_search(analysis::Id::new(id)) { 469 | Ok(data) => { 470 | let mut res = Response::new(); 471 | res.headers_mut().set(ContentType::json()); 472 | return res.with_body(serde_json::to_string(&data).unwrap()); 473 | } 474 | Err(s) => { 475 | return self.handle_error(_req, StatusCode::InternalServerError, s); 476 | } 477 | } 478 | } 479 | _ => { 480 | return self.handle_error( 481 | _req, 482 | StatusCode::InternalServerError, 483 | "Bad search string".to_owned(), 484 | ); 485 | } 486 | } 487 | } 488 | 489 | fn handle_find(&self, _req: Request, query: Option<&str>) -> Response { 490 | match parse_query_value(query, "impls=") { 491 | Some(id) => { 492 | let id = match u64::from_str(&id) { 493 | Ok(l) => l, 494 | Err(_) => { 495 | return self.handle_error( 496 | _req, 497 | StatusCode::InternalServerError, 498 | format!("Bad id: {}", id), 499 | ); 500 | } 501 | }; 502 | match self.file_cache.find_impls(analysis::Id::new(id)) { 503 | Ok(data) => { 504 | let mut res = Response::new(); 505 | res.headers_mut().set(ContentType::json()); 506 | return res.with_body(serde_json::to_string(&data).unwrap()); 507 | } 508 | Err(s) => { 509 | return self.handle_error(_req, StatusCode::InternalServerError, s); 510 | } 511 | } 512 | } 513 | _ => { 514 | return self.handle_error( 515 | _req, 516 | StatusCode::InternalServerError, 517 | "Unknown argument to find".to_owned(), 518 | ); 519 | } 520 | } 521 | } 522 | 523 | fn handle_sym_roots(&self, _req: Request) -> impl Future { 524 | let file_cache = self.file_cache.clone(); 525 | self.status 526 | .block() 527 | .then(move |_| match file_cache.get_symbol_roots() { 528 | Ok(data) => { 529 | let mut res = Response::new(); 530 | res.headers_mut().set(ContentType::json()); 531 | futures::future::ok(res.with_body(serde_json::to_string(&data).unwrap())) 532 | } 533 | Err(s) => futures::future::ok( 534 | Response::new() 535 | .with_status(StatusCode::InternalServerError) 536 | .with_body(s), 537 | ), 538 | }) 539 | } 540 | 541 | fn handle_sym_childen(&self, _req: Request, query: Option<&str>) -> Response { 542 | match parse_query_value(query, "id=") { 543 | Some(id) => { 544 | let id = match u64::from_str(&id) { 545 | Ok(l) => l, 546 | Err(_) => { 547 | return self.handle_error( 548 | _req, 549 | StatusCode::InternalServerError, 550 | format!("Bad id: {}", id), 551 | ); 552 | } 553 | }; 554 | match self.file_cache.get_symbol_children(analysis::Id::new(id)) { 555 | Ok(data) => { 556 | let mut res = Response::new(); 557 | res.headers_mut().set(ContentType::json()); 558 | return res.with_body(serde_json::to_string(&data).unwrap()); 559 | } 560 | Err(s) => { 561 | return self.handle_error(_req, StatusCode::InternalServerError, s); 562 | } 563 | } 564 | } 565 | _ => { 566 | return self.handle_error( 567 | _req, 568 | StatusCode::InternalServerError, 569 | "Unknown argument to symbol_children".to_owned(), 570 | ); 571 | } 572 | } 573 | } 574 | 575 | fn handle_plain_text(&self, _req: Request, query: Option<&str>) -> Response { 576 | match ( 577 | parse_query_value(query, "file="), 578 | parse_query_value(query, "line="), 579 | ) { 580 | (Some(file_name), Some(line)) => { 581 | let line = match usize::from_str(&line) { 582 | Ok(l) => l, 583 | Err(_) => { 584 | return self.handle_error( 585 | _req, 586 | StatusCode::InternalServerError, 587 | format!("Bad line number: {}", line), 588 | ) 589 | } 590 | }; 591 | 592 | // Hard-coded 2 lines of context before and after target line. 593 | let line_start = line.saturating_sub(3); 594 | let line_end = line + 2; 595 | 596 | match self.file_cache.get_lines( 597 | &Path::new(&file_name), 598 | span::Row::new_zero_indexed(line_start as u32), 599 | span::Row::new_zero_indexed(line_end as u32), 600 | ) { 601 | Ok(ref lines) => { 602 | let mut res = Response::new(); 603 | res.headers_mut().set(ContentType::json()); 604 | let result = TextResult { 605 | text: lines, 606 | file_name: file_name, 607 | line_start: line_start + 1, 608 | line_end: line_end, 609 | }; 610 | return res.with_body(serde_json::to_string(&result).unwrap()); 611 | } 612 | Err(msg) => { 613 | return self.handle_error(_req, StatusCode::InternalServerError, msg); 614 | } 615 | } 616 | } 617 | _ => { 618 | return self.handle_error( 619 | _req, 620 | StatusCode::InternalServerError, 621 | "Bad query string".to_owned(), 622 | ); 623 | } 624 | } 625 | } 626 | } 627 | 628 | // The below data types are used to pass data to the client. 629 | 630 | #[derive(Serialize, Debug)] 631 | pub enum SourceResult<'a> { 632 | File { 633 | path: Vec, 634 | lines: Option<&'a [String]>, 635 | rendered: Option<&'a str>, 636 | }, 637 | Directory { 638 | path: Vec, 639 | files: Vec, 640 | }, 641 | } 642 | 643 | #[derive(Serialize, Debug)] 644 | pub struct TextResult<'a> { 645 | text: &'a str, 646 | file_name: String, 647 | line_start: usize, 648 | line_end: usize, 649 | } 650 | 651 | pub fn parse_location_string(input: &str) -> [String; 5] { 652 | let mut args = input.split(':').map(|s| s.to_owned()); 653 | [ 654 | args.next().unwrap(), 655 | args.next().unwrap_or(String::new()), 656 | args.next().unwrap_or(String::new()), 657 | args.next().unwrap_or(String::new()), 658 | args.next().unwrap_or(String::new()), 659 | ] 660 | } 661 | 662 | // key should include `=` suffix. 663 | fn parse_query_value(query: Option<&str>, key: &str) -> Option { 664 | match query { 665 | Some(q) => { 666 | let start = match q.find(key) { 667 | Some(i) => i + key.len(), 668 | None => { 669 | return None; 670 | } 671 | }; 672 | let end = q[start..].find("&").map(|e| e + start).unwrap_or(q.len()); 673 | let value = &q[start..end]; 674 | Some(value.to_owned()) 675 | } 676 | None => None, 677 | } 678 | } 679 | 680 | const STATIC_REQUEST: &str = "static"; 681 | const RAW_REQUEST: &str = "raw"; 682 | const SOURCE_REQUEST: &str = "src"; 683 | const TREE_REQUEST: &str = "tree"; 684 | const PLAIN_TEXT: &str = "plain_text"; 685 | const CONFIG_REQUEST: &str = "config"; 686 | const EDIT_REQUEST: &str = "edit"; 687 | const SEARCH_REQUEST: &str = "search"; 688 | const FIND_REQUEST: &str = "find"; 689 | const GET_STATUS: &str = "status"; 690 | const SYMBOL_ROOTS: &str = "symbol_roots"; 691 | const SYMBOL_CHILDREN: &str = "symbol_children"; 692 | -------------------------------------------------------------------------------- /static/.eslintignore: -------------------------------------------------------------------------------- 1 | libs 2 | *.out.js 3 | *.map 4 | *.json 5 | *.tsx 6 | *.css 7 | *.png 8 | -------------------------------------------------------------------------------- /static/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "globals": { 3 | "CONFIG": true, 4 | "CHAR_WIDTH": true 5 | }, 6 | "env": { 7 | "browser": true, 8 | "jquery": true 9 | }, 10 | "rules": { 11 | "react/prop-types": 0, 12 | "no-console": 0 13 | }, 14 | "plugins": [ 15 | "react", 16 | "no-jquery" 17 | ], 18 | "extends": [ 19 | "eslint:recommended", 20 | "plugin:react/recommended", 21 | "plugin:no-jquery/recommended", 22 | "plugin:no-jquery/deprecated-2.2", 23 | ], 24 | "parserOptions": { 25 | "ecmaVersion": 8, 26 | "sourceType": "module", 27 | "ecmaFeatures": { 28 | "jsx": true, 29 | "experimentalObjectRestSpread": true, 30 | } 31 | }, 32 | "parser": "espree" 33 | } 34 | 35 | -------------------------------------------------------------------------------- /static/app.js: -------------------------------------------------------------------------------- 1 | // Copyright 2017 The Rustw Project Developers. 2 | // 3 | // Licensed under the Apache License, Version 2.0 or the MIT license 5 | // , at your 6 | // option. This file may not be copied, modified, or distributed 7 | // except according to those terms. 8 | 9 | import React from 'react'; 10 | import ReactDOM from 'react-dom'; 11 | import { BrowserRouter, Route } from 'react-router-dom'; 12 | 13 | import { request } from './utils'; 14 | import { Sidebar } from './sidebar'; 15 | import { makeTreeData } from './symbolPanel'; 16 | import { ContentPanel, Page } from './contentPanel'; 17 | 18 | 19 | export class RustwApp extends React.Component { 20 | constructor() { 21 | super(); 22 | this.state = { page: Page.START, fileTreeData: [], symbols: {}, status: null, hasConfig: false }; 23 | } 24 | 25 | componentDidMount() { 26 | $.ajax({ 27 | dataType: "json", 28 | url: "/config", 29 | success: (data) => { 30 | CONFIG = data; 31 | this.loadFileTreeData(); 32 | this.loadSymbols(); 33 | }, 34 | async: false 35 | }); 36 | this.setState({ hasConfig: true }); 37 | } 38 | 39 | loadFileTreeData() { 40 | let self = this; 41 | request( 42 | 'tree/' + CONFIG.workspace_root.replace('\\', '/'), 43 | function(json) { 44 | if (json.Directory) { 45 | self.setState({ fileTreeData: json }) 46 | } else { 47 | console.log("Unexpected tree data.") 48 | console.log(json); 49 | } 50 | }, 51 | "Error with tree request", 52 | null, 53 | ); 54 | } 55 | 56 | loadSymbols() { 57 | const self = this; 58 | request( 59 | 'symbol_roots', 60 | function(json) { 61 | self.setState({ symbols: makeTreeData(json) }); 62 | }, 63 | "Error with symbol_roots request", 64 | null 65 | ); 66 | } 67 | 68 | refreshStatus() { 69 | const self = this; 70 | request( 71 | "status", 72 | function (data) { 73 | self.setState({ status: data.status }); 74 | }, 75 | "Could not fetch status", 76 | null, 77 | ); 78 | } 79 | 80 | 81 | getSearch(needle) { 82 | const self = this; 83 | return request( 84 | 'search?needle=' + needle, 85 | function(json) { 86 | self.refreshStatus(); 87 | self.setState({ search: { defs: json.defs, refs: json.refs, results: null, searchTerm: needle }}); 88 | }, 89 | "Error with search request for " + needle, 90 | null 91 | ); 92 | } 93 | 94 | getUses(needle) { 95 | const self = this; 96 | return request( 97 | 'search?id=' + needle, 98 | function(json) { 99 | self.refreshStatus(); 100 | self.setState({ search: { defs: json.defs, refs: json.refs, results: null, searchTerm: null }}); 101 | }, 102 | "Error with search (uses) request for " + needle, 103 | null 104 | ); 105 | } 106 | 107 | getImpls(needle) { 108 | const self = this; 109 | return request( 110 | 'find?impls=' + needle, 111 | function(json) { 112 | self.refreshStatus(); 113 | self.setState({ search: { results: json.results, defs: null, refs: null }}); 114 | }, 115 | "Error with find (impls) request for " + needle, 116 | null 117 | ); 118 | } 119 | 120 | 121 | showLoading() { 122 | this.setState({ page: Page.LOADING}); 123 | } 124 | 125 | showError() { 126 | this.setState({ page: Page.INTERNAL_ERROR}); 127 | } 128 | 129 | loadSource(path, highlight) { 130 | if (!path.startsWith('/')) { 131 | path = CONFIG.workspace_root + '/' + path; 132 | } 133 | const location = { 134 | pathname: path, 135 | state: { highlight } 136 | }; 137 | if (this.props.location.pathname == path) { 138 | this.props.history.replace(location); 139 | } else { 140 | this.props.history.push(location); 141 | } 142 | } 143 | 144 | render() { 145 | let contentPanel = "Loading..."; 146 | if (this.state.hasConfig) { 147 | let srcHighlight = null; 148 | if (this.props.location.state && this.props.location.state.highlight) { 149 | srcHighlight = this.props.location.state.highlight; 150 | } 151 | contentPanel = ; 152 | } 153 | return
154 |
155 | 156 | {contentPanel} 157 |
158 |
; 159 | } 160 | } 161 | 162 | export function renderApp() { 163 | ReactDOM.render( 164 | 165 | 166 | , 167 | document.getElementById('container') 168 | ); 169 | } 170 | -------------------------------------------------------------------------------- /static/breadCrumbs.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2017 The Rustw Project Developers. 2 | // 3 | // Licensed under the Apache License, Version 2.0 or the MIT license 5 | // , at your 6 | // option. This file may not be copied, modified, or distributed 7 | // except according to those terms. 8 | 9 | import * as React from 'react'; 10 | import { RustwApp } from './app.js'; 11 | 12 | declare var CONFIG: any; 13 | 14 | export interface BreadCrumbProps { 15 | app: RustwApp, 16 | path: Array, 17 | } 18 | 19 | export const BreadCrumbs: React.SFC = (props) => { 20 | // The root path for the workspace. 21 | let root = CONFIG.workspace_root.split('/'); 22 | if (root[0] === '') { 23 | root[0] = '/'; 24 | } 25 | root.pop(); 26 | root.reverse(); 27 | 28 | let path = "", 29 | crumbs = props.path.map((p: string) => { 30 | if (path.length > 0 && path != '/') { 31 | path += '/'; 32 | } 33 | path += p; 34 | 35 | // Don't display the workspace root prefix. 36 | if (p === root.pop()) { 37 | return null; 38 | } 39 | root = []; 40 | 41 | const pathCopy = path; 42 | const onClick = () => { 43 | props.app.loadSource(pathCopy); 44 | } 45 | return ( > {p}); 46 | }); 47 | return
48 | {crumbs} 49 |
; 50 | } 51 | -------------------------------------------------------------------------------- /static/contentPanel.js: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Rustw Project Developers. 2 | // 3 | // Licensed under the Apache License, Version 2.0 or the MIT license 5 | // , at your 6 | // option. This file may not be copied, modified, or distributed 7 | // except according to those terms. 8 | 9 | import React from 'react'; 10 | 11 | import { DirView } from './dirView'; 12 | import { SourceView } from './srcView'; 13 | import { request } from './utils'; 14 | 15 | export const Page = { 16 | START: 'START', 17 | FILE: 'FILE', 18 | SOURCE_DIR: 'SOURCE_DIR', 19 | SEARCH: 'SEARCH', 20 | FIND: 'FIND', 21 | LOADING: 'LOADING', 22 | INTERNAL_ERROR: 'INTERNAL_ERROR', 23 | }; 24 | 25 | export class ContentPanel extends React.Component { 26 | constructor() { 27 | super(); 28 | this.state = { page: Page.LOADING } 29 | } 30 | 31 | componentDidMount() { 32 | this.query_api(this.props.path); 33 | } 34 | 35 | componentDidUpdate() { 36 | document.title = 'cargo src - ' + this.props.path; 37 | } 38 | 39 | UNSAFE_componentWillReceiveProps(nextProps) { 40 | if (nextProps.path == this.props.path) { 41 | return; 42 | } 43 | this.query_api(nextProps.path); 44 | } 45 | 46 | query_api(path) { 47 | if (!path || path === '/') { 48 | path = CONFIG.workspace_root; 49 | } 50 | 51 | const app = this.props.app; 52 | const self = this; 53 | 54 | request( 55 | 'src/' + path, 56 | function(json) { 57 | if (json.Directory) { 58 | self.setState({ page: Page.SOURCE_DIR, params: { path: json.Directory.path, files: json.Directory.files }}); 59 | } else if (json.File) { 60 | app.refreshStatus(); 61 | self.setState({ 62 | page: Page.FILE, 63 | params: { 64 | path: json.File.path, 65 | lines: json.File.lines, 66 | rendered: json.File.rendered 67 | }, 68 | }); 69 | } else { 70 | console.log("Unexpected source data.") 71 | console.log(json); 72 | } 73 | }, 74 | 'Error with source request for ' + '/src' + path, 75 | app 76 | ); 77 | } 78 | 79 | render() { 80 | let divMain; 81 | switch (this.state.page) { 82 | case Page.FILE: 83 | divMain = ; 84 | break; 85 | case Page.SOURCE_DIR: 86 | divMain = ; 87 | break; 88 | case Page.LOADING: 89 | divMain =
Loading...
; 90 | break; 91 | case Page.INTERNAL_ERROR: 92 | divMain = "Server error?"; 93 | break; 94 | case Page.START: 95 | default: 96 | divMain = null; 97 | } 98 | 99 | return divMain; 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /static/dirView.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2017 The Rustw Project Developers. 2 | // 3 | // Licensed under the Apache License, Version 2.0 or the MIT license 5 | // , at your 6 | // option. This file may not be copied, modified, or distributed 7 | // except according to those terms. 8 | 9 | import * as React from 'react'; 10 | 11 | import { BreadCrumbs } from './breadCrumbs'; 12 | import { RustwApp } from './app.js'; 13 | 14 | export interface DirViewProps { 15 | app: RustwApp, 16 | files: Array, 17 | path: Array, 18 | } 19 | 20 | export const DirView: React.SFC = (props) => { 21 | const dirPath = props.path.join('/').replace('//', '/'); 22 | let files: any = props.files.map((f: any) => { 23 | const onClick = () => props.app.loadSource(`${dirPath}/${f.name}`); 24 | const className = f.kind === "Directory" ? 'div_entry_name div_dir_entry' : 'div_entry_name div_file_entry'; 25 | return ( 26 |
27 | {f.name} 28 |
29 | ); 30 | }); 31 | if (files.length == 0) { 32 | files =
<Empty directory>
33 | } 34 | return
35 | 36 |
37 |
38 | {files} 39 |
40 |
41 |
; 42 | } 43 | -------------------------------------------------------------------------------- /static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rust-dev-tools/cargo-src/328f783ac40245c298ee6c1e9654f4d4e8d46e7b/static/favicon.ico -------------------------------------------------------------------------------- /static/file.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rust-dev-tools/cargo-src/328f783ac40245c298ee6c1e9654f4d4e8d46e7b/static/file.png -------------------------------------------------------------------------------- /static/folder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rust-dev-tools/cargo-src/328f783ac40245c298ee6c1e9654f4d4e8d46e7b/static/folder.png -------------------------------------------------------------------------------- /static/fonts/FiraCode-Bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rust-dev-tools/cargo-src/328f783ac40245c298ee6c1e9654f4d4e8d46e7b/static/fonts/FiraCode-Bold.woff -------------------------------------------------------------------------------- /static/fonts/FiraCode-Light.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rust-dev-tools/cargo-src/328f783ac40245c298ee6c1e9654f4d4e8d46e7b/static/fonts/FiraCode-Light.woff -------------------------------------------------------------------------------- /static/fonts/FiraCode-Medium.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rust-dev-tools/cargo-src/328f783ac40245c298ee6c1e9654f4d4e8d46e7b/static/fonts/FiraCode-Medium.woff -------------------------------------------------------------------------------- /static/fonts/FiraCode-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rust-dev-tools/cargo-src/328f783ac40245c298ee6c1e9654f4d4e8d46e7b/static/fonts/FiraCode-Regular.woff -------------------------------------------------------------------------------- /static/fonts/FiraSans-Light.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rust-dev-tools/cargo-src/328f783ac40245c298ee6c1e9654f4d4e8d46e7b/static/fonts/FiraSans-Light.woff -------------------------------------------------------------------------------- /static/fonts/FiraSans-Medium.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rust-dev-tools/cargo-src/328f783ac40245c298ee6c1e9654f4d4e8d46e7b/static/fonts/FiraSans-Medium.woff -------------------------------------------------------------------------------- /static/fonts/FiraSans-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rust-dev-tools/cargo-src/328f783ac40245c298ee6c1e9654f4d4e8d46e7b/static/fonts/FiraSans-Regular.woff -------------------------------------------------------------------------------- /static/index.html: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | 13 | 14 | 15 | cargo src 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 | 26 | 27 | 41 | 42 | -------------------------------------------------------------------------------- /static/libs/marked.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * marked - a markdown parser 3 | * Copyright (c) 2011-2014, Christopher Jeffrey. (MIT Licensed) 4 | * https://github.com/chjj/marked 5 | */ 6 | (function(){var block={newline:/^\n+/,code:/^( {4}[^\n]+\n*)+/,fences:noop,hr:/^( *[-*_]){3,} *(?:\n+|$)/,heading:/^ *(#{1,6}) *([^\n]+?) *#* *(?:\n+|$)/,nptable:noop,lheading:/^([^\n]+)\n *(=|-){2,} *(?:\n+|$)/,blockquote:/^( *>[^\n]+(\n(?!def)[^\n]+)*\n*)+/,list:/^( *)(bull) [\s\S]+?(?:hr|def|\n{2,}(?! )(?!\1bull )\n*|\s*$)/,html:/^ *(?:comment *(?:\n|\s*$)|closed *(?:\n{2,}|\s*$)|closing *(?:\n{2,}|\s*$))/,def:/^ *\[([^\]]+)\]: *]+)>?(?: +["(]([^\n]+)[")])? *(?:\n+|$)/,table:noop,paragraph:/^((?:[^\n]+\n?(?!hr|heading|lheading|blockquote|tag|def))+)\n*/,text:/^[^\n]+/};block.bullet=/(?:[*+-]|\d+\.)/;block.item=/^( *)(bull) [^\n]*(?:\n(?!\1bull )[^\n]*)*/;block.item=replace(block.item,"gm")(/bull/g,block.bullet)();block.list=replace(block.list)(/bull/g,block.bullet)("hr","\\n+(?=\\1?(?:[-*_] *){3,}(?:\\n+|$))")("def","\\n+(?="+block.def.source+")")();block.blockquote=replace(block.blockquote)("def",block.def)();block._tag="(?!(?:"+"a|em|strong|small|s|cite|q|dfn|abbr|data|time|code"+"|var|samp|kbd|sub|sup|i|b|u|mark|ruby|rt|rp|bdi|bdo"+"|span|br|wbr|ins|del|img)\\b)\\w+(?!:/|[^\\w\\s@]*@)\\b";block.html=replace(block.html)("comment",//)("closed",/<(tag)[\s\S]+?<\/\1>/)("closing",/])*?>/)(/tag/g,block._tag)();block.paragraph=replace(block.paragraph)("hr",block.hr)("heading",block.heading)("lheading",block.lheading)("blockquote",block.blockquote)("tag","<"+block._tag)("def",block.def)();block.normal=merge({},block);block.gfm=merge({},block.normal,{fences:/^ *(`{3,}|~{3,})[ \.]*(\S+)? *\n([\s\S]*?)\s*\1 *(?:\n+|$)/,paragraph:/^/,heading:/^ *(#{1,6}) +([^\n]+?) *#* *(?:\n+|$)/});block.gfm.paragraph=replace(block.paragraph)("(?!","(?!"+block.gfm.fences.source.replace("\\1","\\2")+"|"+block.list.source.replace("\\1","\\3")+"|")();block.tables=merge({},block.gfm,{nptable:/^ *(\S.*\|.*)\n *([-:]+ *\|[-| :]*)\n((?:.*\|.*(?:\n|$))*)\n*/,table:/^ *\|(.+)\n *\|( *[-:]+[-| :]*)\n((?: *\|.*(?:\n|$))*)\n*/});function Lexer(options){this.tokens=[];this.tokens.links={};this.options=options||marked.defaults;this.rules=block.normal;if(this.options.gfm){if(this.options.tables){this.rules=block.tables}else{this.rules=block.gfm}}}Lexer.rules=block;Lexer.lex=function(src,options){var lexer=new Lexer(options);return lexer.lex(src)};Lexer.prototype.lex=function(src){src=src.replace(/\r\n|\r/g,"\n").replace(/\t/g," ").replace(/\u00a0/g," ").replace(/\u2424/g,"\n");return this.token(src,true)};Lexer.prototype.token=function(src,top,bq){var src=src.replace(/^ +$/gm,""),next,loose,cap,bull,b,item,space,i,l;while(src){if(cap=this.rules.newline.exec(src)){src=src.substring(cap[0].length);if(cap[0].length>1){this.tokens.push({type:"space"})}}if(cap=this.rules.code.exec(src)){src=src.substring(cap[0].length);cap=cap[0].replace(/^ {4}/gm,"");this.tokens.push({type:"code",text:!this.options.pedantic?cap.replace(/\n+$/,""):cap});continue}if(cap=this.rules.fences.exec(src)){src=src.substring(cap[0].length);this.tokens.push({type:"code",lang:cap[2],text:cap[3]||""});continue}if(cap=this.rules.heading.exec(src)){src=src.substring(cap[0].length);this.tokens.push({type:"heading",depth:cap[1].length,text:cap[2]});continue}if(top&&(cap=this.rules.nptable.exec(src))){src=src.substring(cap[0].length);item={type:"table",header:cap[1].replace(/^ *| *\| *$/g,"").split(/ *\| */),align:cap[2].replace(/^ *|\| *$/g,"").split(/ *\| */),cells:cap[3].replace(/\n$/,"").split("\n")};for(i=0;i ?/gm,"");this.token(cap,top,true);this.tokens.push({type:"blockquote_end"});continue}if(cap=this.rules.list.exec(src)){src=src.substring(cap[0].length);bull=cap[2];this.tokens.push({type:"list_start",ordered:bull.length>1});cap=cap[0].match(this.rules.item);next=false;l=cap.length;i=0;for(;i1&&b.length>1)){src=cap.slice(i+1).join("\n")+src;i=l-1}}loose=next||/\n\n(?!\s*$)/.test(item);if(i!==l-1){next=item.charAt(item.length-1)==="\n";if(!loose)loose=next}this.tokens.push({type:loose?"loose_item_start":"list_item_start"});this.token(item,false,bq);this.tokens.push({type:"list_item_end"})}this.tokens.push({type:"list_end"});continue}if(cap=this.rules.html.exec(src)){src=src.substring(cap[0].length);this.tokens.push({type:this.options.sanitize?"paragraph":"html",pre:!this.options.sanitizer&&(cap[1]==="pre"||cap[1]==="script"||cap[1]==="style"),text:cap[0]});continue}if(!bq&&top&&(cap=this.rules.def.exec(src))){src=src.substring(cap[0].length);this.tokens.links[cap[1].toLowerCase()]={href:cap[2],title:cap[3]};continue}if(top&&(cap=this.rules.table.exec(src))){src=src.substring(cap[0].length);item={type:"table",header:cap[1].replace(/^ *| *\| *$/g,"").split(/ *\| */),align:cap[2].replace(/^ *|\| *$/g,"").split(/ *\| */),cells:cap[3].replace(/(?: *\| *)?\n$/,"").split("\n")};for(i=0;i])/,autolink:/^<([^ >]+(@|:\/)[^ >]+)>/,url:noop,tag:/^|^<\/?\w+(?:"[^"]*"|'[^']*'|[^'">])*?>/,link:/^!?\[(inside)\]\(href\)/,reflink:/^!?\[(inside)\]\s*\[([^\]]*)\]/,nolink:/^!?\[((?:\[[^\]]*\]|[^\[\]])*)\]/,strong:/^__([\s\S]+?)__(?!_)|^\*\*([\s\S]+?)\*\*(?!\*)/,em:/^\b_((?:[^_]|__)+?)_\b|^\*((?:\*\*|[\s\S])+?)\*(?!\*)/,code:/^(`+)\s*([\s\S]*?[^`])\s*\1(?!`)/,br:/^ {2,}\n(?!\s*$)/,del:noop,text:/^[\s\S]+?(?=[\\?(?:\s+['"]([\s\S]*?)['"])?\s*/;inline.link=replace(inline.link)("inside",inline._inside)("href",inline._href)();inline.reflink=replace(inline.reflink)("inside",inline._inside)();inline.normal=merge({},inline);inline.pedantic=merge({},inline.normal,{strong:/^__(?=\S)([\s\S]*?\S)__(?!_)|^\*\*(?=\S)([\s\S]*?\S)\*\*(?!\*)/,em:/^_(?=\S)([\s\S]*?\S)_(?!_)|^\*(?=\S)([\s\S]*?\S)\*(?!\*)/});inline.gfm=merge({},inline.normal,{escape:replace(inline.escape)("])","~|])")(),url:/^(https?:\/\/[^\s<]+[^<.,:;"')\]\s])/,del:/^~~(?=\S)([\s\S]*?\S)~~/,text:replace(inline.text)("]|","~]|")("|","|https?://|")()});inline.breaks=merge({},inline.gfm,{br:replace(inline.br)("{2,}","*")(),text:replace(inline.gfm.text)("{2,}","*")()});function InlineLexer(links,options){this.options=options||marked.defaults;this.links=links;this.rules=inline.normal;this.renderer=this.options.renderer||new Renderer;this.renderer.options=this.options;if(!this.links){throw new Error("Tokens array requires a `links` property.")}if(this.options.gfm){if(this.options.breaks){this.rules=inline.breaks}else{this.rules=inline.gfm}}else if(this.options.pedantic){this.rules=inline.pedantic}}InlineLexer.rules=inline;InlineLexer.output=function(src,links,options){var inline=new InlineLexer(links,options);return inline.output(src)};InlineLexer.prototype.output=function(src){var out="",link,text,href,cap;while(src){if(cap=this.rules.escape.exec(src)){src=src.substring(cap[0].length);out+=cap[1];continue}if(cap=this.rules.autolink.exec(src)){src=src.substring(cap[0].length);if(cap[2]==="@"){text=cap[1].charAt(6)===":"?this.mangle(cap[1].substring(7)):this.mangle(cap[1]);href=this.mangle("mailto:")+text}else{text=escape(cap[1]);href=text}out+=this.renderer.link(href,null,text);continue}if(!this.inLink&&(cap=this.rules.url.exec(src))){src=src.substring(cap[0].length);text=escape(cap[1]);href=text;out+=this.renderer.link(href,null,text);continue}if(cap=this.rules.tag.exec(src)){if(!this.inLink&&/^/i.test(cap[0])){this.inLink=false}src=src.substring(cap[0].length);out+=this.options.sanitize?this.options.sanitizer?this.options.sanitizer(cap[0]):escape(cap[0]):cap[0];continue}if(cap=this.rules.link.exec(src)){src=src.substring(cap[0].length);this.inLink=true;out+=this.outputLink(cap,{href:cap[2],title:cap[3]});this.inLink=false;continue}if((cap=this.rules.reflink.exec(src))||(cap=this.rules.nolink.exec(src))){src=src.substring(cap[0].length);link=(cap[2]||cap[1]).replace(/\s+/g," ");link=this.links[link.toLowerCase()];if(!link||!link.href){out+=cap[0].charAt(0);src=cap[0].substring(1)+src;continue}this.inLink=true;out+=this.outputLink(cap,link);this.inLink=false;continue}if(cap=this.rules.strong.exec(src)){src=src.substring(cap[0].length);out+=this.renderer.strong(this.output(cap[2]||cap[1]));continue}if(cap=this.rules.em.exec(src)){src=src.substring(cap[0].length);out+=this.renderer.em(this.output(cap[2]||cap[1]));continue}if(cap=this.rules.code.exec(src)){src=src.substring(cap[0].length);out+=this.renderer.codespan(escape(cap[2],true));continue}if(cap=this.rules.br.exec(src)){src=src.substring(cap[0].length);out+=this.renderer.br();continue}if(cap=this.rules.del.exec(src)){src=src.substring(cap[0].length);out+=this.renderer.del(this.output(cap[1]));continue}if(cap=this.rules.text.exec(src)){src=src.substring(cap[0].length);out+=this.renderer.text(escape(this.smartypants(cap[0])));continue}if(src){throw new Error("Infinite loop on byte: "+src.charCodeAt(0))}}return out};InlineLexer.prototype.outputLink=function(cap,link){var href=escape(link.href),title=link.title?escape(link.title):null;return cap[0].charAt(0)!=="!"?this.renderer.link(href,title,this.output(cap[1])):this.renderer.image(href,title,escape(cap[1]))};InlineLexer.prototype.smartypants=function(text){if(!this.options.smartypants)return text;return text.replace(/---/g,"—").replace(/--/g,"–").replace(/(^|[-\u2014/(\[{"\s])'/g,"$1‘").replace(/'/g,"’").replace(/(^|[-\u2014/(\[{\u2018\s])"/g,"$1“").replace(/"/g,"”").replace(/\.{3}/g,"…")};InlineLexer.prototype.mangle=function(text){if(!this.options.mangle)return text;var out="",l=text.length,i=0,ch;for(;i.5){ch="x"+ch.toString(16)}out+="&#"+ch+";"}return out};function Renderer(options){this.options=options||{}}Renderer.prototype.code=function(code,lang,escaped){if(this.options.highlight){var out=this.options.highlight(code,lang);if(out!=null&&out!==code){escaped=true;code=out}}if(!lang){return"
"+(escaped?code:escape(code,true))+"\n
"}return'
'+(escaped?code:escape(code,true))+"\n
\n"};Renderer.prototype.blockquote=function(quote){return"
\n"+quote+"
\n"};Renderer.prototype.html=function(html){return html};Renderer.prototype.heading=function(text,level,raw){return"'+text+"\n"};Renderer.prototype.hr=function(){return this.options.xhtml?"
\n":"
\n"};Renderer.prototype.list=function(body,ordered){var type=ordered?"ol":"ul";return"<"+type+">\n"+body+"\n"};Renderer.prototype.listitem=function(text){return"
  • "+text+"
  • \n"};Renderer.prototype.paragraph=function(text){return"

    "+text+"

    \n"};Renderer.prototype.table=function(header,body){return"\n"+"\n"+header+"\n"+"\n"+body+"\n"+"
    \n"};Renderer.prototype.tablerow=function(content){return"\n"+content+"\n"};Renderer.prototype.tablecell=function(content,flags){var type=flags.header?"th":"td";var tag=flags.align?"<"+type+' style="text-align:'+flags.align+'">':"<"+type+">";return tag+content+"\n"};Renderer.prototype.strong=function(text){return""+text+""};Renderer.prototype.em=function(text){return""+text+""};Renderer.prototype.codespan=function(text){return""+text+""};Renderer.prototype.br=function(){return this.options.xhtml?"
    ":"
    "};Renderer.prototype.del=function(text){return""+text+""};Renderer.prototype.link=function(href,title,text){if(this.options.sanitize){try{var prot=decodeURIComponent(unescape(href)).replace(/[^\w:]/g,"").toLowerCase()}catch(e){return""}if(prot.indexOf("javascript:")===0||prot.indexOf("vbscript:")===0){return""}}var out='
    ";return out};Renderer.prototype.image=function(href,title,text){var out=''+text+'":">";return out};Renderer.prototype.text=function(text){return text};function Parser(options){this.tokens=[];this.token=null;this.options=options||marked.defaults;this.options.renderer=this.options.renderer||new Renderer;this.renderer=this.options.renderer;this.renderer.options=this.options}Parser.parse=function(src,options,renderer){var parser=new Parser(options,renderer);return parser.parse(src)};Parser.prototype.parse=function(src){this.inline=new InlineLexer(src.links,this.options,this.renderer);this.tokens=src.reverse();var out="";while(this.next()){out+=this.tok()}return out};Parser.prototype.next=function(){return this.token=this.tokens.pop()};Parser.prototype.peek=function(){return this.tokens[this.tokens.length-1]||0};Parser.prototype.parseText=function(){var body=this.token.text;while(this.peek().type==="text"){body+="\n"+this.next().text}return this.inline.output(body)};Parser.prototype.tok=function(){switch(this.token.type){case"space":{return""}case"hr":{return this.renderer.hr()}case"heading":{return this.renderer.heading(this.inline.output(this.token.text),this.token.depth,this.token.text)}case"code":{return this.renderer.code(this.token.text,this.token.lang,this.token.escaped)}case"table":{var header="",body="",i,row,cell,flags,j;cell="";for(i=0;i/g,">").replace(/"/g,""").replace(/'/g,"'")}function unescape(html){return html.replace(/&([#\w]+);/g,function(_,n){n=n.toLowerCase();if(n==="colon")return":";if(n.charAt(0)==="#"){return n.charAt(1)==="x"?String.fromCharCode(parseInt(n.substring(2),16)):String.fromCharCode(+n.substring(1))}return""})}function replace(regex,opt){regex=regex.source;opt=opt||"";return function self(name,val){if(!name)return new RegExp(regex,opt);val=val.source||val;val=val.replace(/(^|[^\[])\^/g,"$1");regex=regex.replace(name,val);return self}}function noop(){}noop.exec=noop;function merge(obj){var i=1,target,key;for(;iAn error occured:

    "+escape(e.message+"",true)+"
    "}throw e}}marked.options=marked.setOptions=function(opt){merge(marked.defaults,opt);return marked};marked.defaults={gfm:true,tables:true,breaks:false,pedantic:false,sanitize:false,sanitizer:null,mangle:true,smartLists:false,silent:false,highlight:null,langPrefix:"lang-",smartypants:false,headerPrefix:"",renderer:new Renderer,xhtml:false};marked.Parser=Parser;marked.parser=Parser.parse;marked.Renderer=Renderer;marked.Lexer=Lexer;marked.lexer=Lexer.lex;marked.InlineLexer=InlineLexer;marked.inlineLexer=InlineLexer.output;marked.parse=marked;if(typeof module!=="undefined"&&typeof exports==="object"){module.exports=marked}else if(typeof define==="function"&&define.amd){define(function(){return marked})}else{this.marked=marked}}).call(function(){return this||(typeof window!=="undefined"?window:global)}()); -------------------------------------------------------------------------------- /static/menus.js: -------------------------------------------------------------------------------- 1 | // Copyright 2017 The Rustw Project Developers. 2 | // 3 | // Licensed under the Apache License, Version 2.0 or the MIT license 5 | // , at your 6 | // option. This file may not be copied, modified, or distributed 7 | // except according to those terms. 8 | 9 | import React from 'react'; 10 | 11 | // props: { id, items: [{id, label, fn, unstable}], location, onClose, target } 12 | // fn: (target: Element, location) -> () 13 | export class Menu extends React.Component { 14 | componentDidUpdate() { 15 | this.didRender(); 16 | } 17 | 18 | componentDidMount() { 19 | this.didRender(); 20 | } 21 | 22 | didRender() { 23 | if (this.items().length == 0) { 24 | this.props.onClose(); 25 | return; 26 | } 27 | 28 | var $menuDiv = $(`#${this.props.id}`); 29 | $menuDiv.offset(this.props.location); 30 | } 31 | 32 | items() { 33 | const self = this; 34 | return this.props 35 | .items 36 | .filter((i) => { return !i.unstable || CONFIG.unstable_features }) 37 | .map((i) => { 38 | const className = `${this.props.id}_link menu_link`; 39 | let onClick = (ev) => { 40 | self.hideMenu(ev); 41 | i.fn(self.props.target, self.props.location); 42 | }; 43 | return
    {i.label}
    ; 44 | }); 45 | } 46 | 47 | hideMenu(event) { 48 | this.props.onClose(); 49 | event.preventDefault(); 50 | event.stopPropagation(); 51 | } 52 | 53 | render() { 54 | let items = this.items(); 55 | 56 | if (items.length === 0) { 57 | return null; 58 | } 59 | 60 | return 61 |
    this.hideMenu(ev) } /> 62 |
    63 | {items} 64 |
    65 | ; 66 | } 67 | } 68 | 69 | export class MenuHost extends React.Component { 70 | constructor(props) { 71 | super(props); 72 | this.state = { menuOpen: null }; 73 | } 74 | 75 | render() { 76 | let menu = null; 77 | if (this.state.menuOpen) { 78 | const onClose = () => this.setState({ menuOpen: null}); 79 | menu = React.createElement(this.menuFn, { location: this.state.menuOpen, onClose: onClose, target: this.state.menuOpen.target, callbacks: this.props.callbacks }); 80 | } 81 | 82 | let contextMenu = (ev) => { 83 | this.setState({ menuOpen: { "top": ev.pageY, "left": ev.pageX, target: ev.target }}); 84 | ev.preventDefault(); 85 | ev.stopPropagation(); 86 | }; 87 | 88 | let onClick = null; 89 | if (this.leftClick) { 90 | onClick = contextMenu; 91 | contextMenu = null; 92 | } 93 | 94 | return ( 95 | 96 | {this.renderInner()} 97 | {menu} 98 | 99 | ); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /static/rustw.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 The Rustw Project Developers. 3 | * 4 | * Licensed under the Apache License, Version 2.0 or the MIT license 6 | * , at your 7 | * option. This file may not be copied, modified, or distributed 8 | * except according to those terms. 9 | */ 10 | 11 | @font-face { 12 | font-family: 'Fira Sans'; 13 | font-style: normal; 14 | font-weight: 300; 15 | src: local('Fira Sans Light'), url("/static/fonts/FiraSans-Light.woff") format('woff'); 16 | } 17 | @font-face { 18 | font-family: 'Fira Sans'; 19 | font-style: normal; 20 | font-weight: 400; 21 | src: local('Fira Sans'), url("/static/fonts/FiraSans-Regular.woff") format('woff'); 22 | } 23 | @font-face { 24 | font-family: 'Fira Sans'; 25 | font-style: normal; 26 | font-weight: 500; 27 | src: local('Fira Sans Medium'), url("/static/fonts/FiraSans-Medium.woff") format('woff'); 28 | } 29 | 30 | @font-face { 31 | font-family: 'Fira Code'; 32 | font-style: normal; 33 | font-weight: 300; 34 | src: local('Fira Code Light'), url("/static/fonts/FiraCode-Light.woff") format('woff'); 35 | } 36 | @font-face { 37 | font-family: 'Fira Code'; 38 | font-style: normal; 39 | font-weight: 400; 40 | src: local('Fira Code'), url("/static/fonts/FiraCode-Regular.woff") format('woff'); 41 | } 42 | @font-face { 43 | font-family: 'Fira Code'; 44 | font-style: normal; 45 | font-weight: 500; 46 | src: local('Fira Code Medium'), url("/static/fonts/FiraCode-Medium.woff") format('woff'); 47 | } 48 | 49 | 50 | 51 | body { 52 | font-family: 'Fira Sans', "Helvetica Neue", Helvetica, Arial, sans-serif; 53 | margin: 0; 54 | height: 100%; 55 | } 56 | html { 57 | height: 100%; 58 | } 59 | 60 | .small_button { 61 | font: bold 0.85em courier; 62 | display: inline-block; 63 | text-align: center; 64 | vertical-align: middle; 65 | width: 1.2em; 66 | height: 1.2em; 67 | line-height: 1.2em; 68 | border-radius: 0.3em; 69 | border: 1px solid black; 70 | cursor: pointer; 71 | } 72 | .button { 73 | font-size: 0.85em; 74 | display: inline-block; 75 | text-align: center; 76 | width: 7em; 77 | height: 2em; 78 | line-height: 2em; 79 | border-radius: 0.3em; 80 | border: 1px solid black; 81 | margin-right: 1em; 82 | background-color: #f0f0f0; 83 | cursor: pointer; 84 | } 85 | .enabled_button { 86 | color: black; 87 | cursor: pointer; 88 | } 89 | .disabled_button { 90 | color: #808080; 91 | cursor: auto; 92 | } 93 | .header_link { 94 | display: inline-block; 95 | margin-right: 1em; 96 | text-decoration: underline; 97 | cursor: pointer; 98 | } 99 | #link_back_container { 100 | width: 20em; 101 | } 102 | .link_hidden { 103 | visibility: hidden; 104 | } 105 | 106 | #div_header_group { 107 | width: 100%; 108 | background-color: white; 109 | opacity: 1; 110 | z-index: 10; 111 | } 112 | #div_header { 113 | width: 100%; 114 | box-sizing: border-box; 115 | background-color: white; 116 | padding: 10px; 117 | } 118 | #div_border { 119 | width: 100%; 120 | height: 3px; 121 | opacity: 1; 122 | } 123 | .div_border_plain { 124 | background-color: black; 125 | } 126 | .div_border_status { 127 | background-color: #e3e9ff; 128 | } 129 | #div_border_animated { 130 | width: 0; 131 | height: 100%; 132 | background-color: #b3d9ff; 133 | opacity: 1; 134 | } 135 | .animated_border { 136 | animation-duration: 30s; 137 | animation-timing-function: linear; 138 | animation-name: border; 139 | animation-iteration-count: infinite; 140 | } 141 | @keyframes border { 142 | from { 143 | width: 0%; 144 | } 145 | 146 | to { 147 | width: 100%; 148 | } 149 | } 150 | #div_main { 151 | box-sizing: border-box; 152 | background-color: #fafafa; 153 | height: 100%; 154 | overflow: hidden; 155 | font-size: small; 156 | display: flex; 157 | } 158 | #div_app { 159 | position: absolute; 160 | top: 0; 161 | bottom: 0; 162 | left: 0; 163 | right: 0; 164 | } 165 | #container { 166 | min-height: 100%; 167 | display: flex; 168 | } 169 | 170 | a.issue_link, a.link { 171 | text-decoration: underline; 172 | cursor: pointer; 173 | } 174 | a.issue_link:link, a.link { color: black } 175 | a.issue_link:visited, a.link { color: black } 176 | a.issue_link:hover, a.link { color: black } 177 | a.issue_link:active, a.link { color: black } 178 | 179 | .span_loc { 180 | display: inline-block; 181 | margin-left: 10px; 182 | font-family: 'Fira Code'; 183 | text-decoration: underline; 184 | cursor: pointer; 185 | vertical-align: top; 186 | } 187 | .div_all_span_src { 188 | font-family: 'Fira Code'; 189 | white-space: nowrap; 190 | } 191 | .div_children .div_all_span_src { 192 | margin-left: -40px; 193 | } 194 | .div_span_src_number { 195 | display: inline-block; 196 | border-right: black 1px solid; 197 | } 198 | .div_span_src { 199 | display: inline-block; 200 | width: calc(100% - 2em - 4px); 201 | } 202 | .span_src_number { 203 | display: block; 204 | padding-right: 3px; 205 | text-align: right; 206 | width: 2em; 207 | color: #808080; 208 | } 209 | .span_src { 210 | display: block; 211 | white-space: pre; 212 | position: relative; 213 | z-index: 1; 214 | width: -moz-max-content; 215 | } 216 | 217 | .div_entry { 218 | margin: 0.5em; 219 | } 220 | .div_dir_entry { 221 | display: inline-block; 222 | background-image: url(/static/folder.png); 223 | background-repeat: no-repeat; 224 | padding-left: 20px; 225 | height: 1.3em; 226 | } 227 | 228 | .div_file_entry { 229 | display: inline-block; 230 | background-image: url(/static/file.png); 231 | background-repeat: no-repeat; 232 | padding-left: 20px; 233 | height: 1.3em; 234 | } 235 | .div_entry_name { 236 | text-decoration: underline; 237 | cursor: pointer; 238 | } 239 | .link_breadcrumb { 240 | text-decoration: underline; 241 | cursor: pointer; 242 | } 243 | 244 | span#measure { 245 | padding: 0; 246 | margin: 0; 247 | border: 0; 248 | white-space: pre; 249 | font-family: monospace, 'Fira Code'; 250 | font-size: small; 251 | } 252 | 253 | .separator { 254 | width: 85%; 255 | margin-top: 40px; 256 | margin-bottom: 40px; 257 | } 258 | 259 | .div_view_selector { 260 | padding-top: 8px; 261 | padding-bottom: 8px; 262 | padding-left: 20px; 263 | background-color: #f3f3a5; 264 | } 265 | 266 | #div_src_contents { 267 | font-family: 'Fira Code'; 268 | margin-top: 2px; 269 | margin-left: -15px; 270 | margin-bottom: 20px; 271 | white-space: nowrap; 272 | } 273 | #div_src_contents.div_src_html { 274 | font-family: 'Fira Sans', "Helvetica Neue", Helvetica, Arial, sans-serif; 275 | width: calc(100% - 30px); 276 | margin-left: 15px; 277 | white-space: unset; 278 | margin-right: 15px; 279 | overflow-x: hidden; 280 | } 281 | #div_src_contents.div_src_html img { 282 | max-width: 100%; 283 | } 284 | .div_src_line_numbers { 285 | display: inline-block; 286 | border-right: black 1px solid; 287 | } 288 | .div_src_line_number { 289 | display: block; 290 | padding-right: 3px; 291 | text-align: right; 292 | width: 3em; 293 | color: #808080; 294 | } 295 | .div_src_lines { 296 | display: inline-block; 297 | font-variant-ligatures: none; 298 | text-rendering: auto; 299 | -moz-font-feature-settings: 'liga=0'; 300 | -moz-font-feature-settings: 'liga' 0; 301 | -webkit-font-feature-settings: 'liga' 0; 302 | -o-font-feature-settings: 'liga' 0; 303 | -ms-font-feature-settings: 'liga' 0; 304 | font-feature-settings: 'liga' 0; 305 | } 306 | 307 | .div_src_line { 308 | display: block; 309 | white-space: pre; 310 | padding-left: 1em; 311 | position: relative; 312 | z-index: 1; 313 | width: -moz-max-content; 314 | } 315 | .div_src_line, .div_src_line_number{ 316 | padding: 0.15em 0.5em; 317 | } 318 | .src_highlight { 319 | font-weight: bold; 320 | } 321 | .selected_search { 322 | background-color: #f3f3a5; 323 | } 324 | .selected_secondary { 325 | background-color: #d5f3b5; 326 | } 327 | .floating_highlight { 328 | margin-left: 1em; 329 | display: block; 330 | position: absolute; 331 | } 332 | .highlight_label { 333 | display: block; 334 | position: absolute; 335 | margin-left: 3em; 336 | font-style: italic; 337 | font-family: 'Fira Sans', "Helvetica Neue", Helvetica, Arial, sans-serif; 338 | background-color: #f0f0f0; 339 | padding: 1px 4px 1px 4px; 340 | } 341 | 342 | /* Syntax highlighting */ 343 | #div_src_view .kw { color: #8959A8; } 344 | #div_src_view .kw-2, #div_src_view .prelude-ty { color: #385D9A; } 345 | #div_src_view .number, #div_src_view .string { color: #587300; } 346 | #div_src_view .boolval, #div_src_view .prelude-val, 347 | #div_src_view .attribute, #div_src_view .attribute .ident { color: #C82829; } 348 | #div_src_view .macro, #div_src_view .macro-nonterminal { color: #167177; } 349 | #div_src_view .lifetime { color: #A35100; } 350 | #div_src_view .comment, #div_src_view .doccomment { color: #707070; } 351 | 352 | .div_all_span_src .kw { color: #8959A8; } 353 | .div_all_span_src .kw-2, .div_all_span_src .prelude-ty { color: #385D9A; } 354 | .div_all_span_src .number, .div_all_span_src .string { color: #587300; } 355 | .div_all_span_src .boolval, .div_all_span_src .prelude-val, 356 | .div_all_span_src .attribute, .div_all_span_src .attribute .ident { color: #C82829; } 357 | .div_all_span_src .macro, .div_all_span_src .macro-nonterminal { color: #167177; } 358 | .div_all_span_src .lifetime { color: #A35100; } 359 | .div_all_span_src .comment, .div_all_span_src .doccomment { color: #707070; } 360 | 361 | .src_link { 362 | text-decoration: underline; 363 | cursor: pointer; 364 | } 365 | 366 | #div_overlay { 367 | opacity: 0; 368 | position: fixed; 369 | top:0; 370 | left:0; 371 | width: 100%; 372 | height: 100%; 373 | } 374 | #link_close_options { 375 | float: right; 376 | } 377 | 378 | .div_menu { 379 | position: absolute; 380 | width: 150px; 381 | height: 100px; 382 | background-color: #fafafa; 383 | box-shadow: 5px 5px 3px 1px #f0f0f0; 384 | border: solid black 1px; 385 | border-radius: 6px; 386 | border-top-left-radius: 0; 387 | padding: 10px; 388 | z-index: 2; 389 | font-size: small; 390 | font-family: 'Fira Sans', "Helvetica Neue", Helvetica, Arial, sans-serif; 391 | } 392 | .menu_link { 393 | text-decoration: underline; 394 | cursor: pointer; 395 | } 396 | .src_menu_link { 397 | text-decoration: underline; 398 | cursor: pointer; 399 | } 400 | 401 | .div_search_title { 402 | font-size: large; 403 | padding-top: 0.5em; 404 | } 405 | .div_search_file_link { 406 | padding-top: 0.2em; 407 | padding-bottom: 0.5em; 408 | text-decoration: underline; 409 | cursor: pointer; 410 | } 411 | .div_search_results { 412 | padding-top: 0.25em; 413 | padding-bottom: 1.5em; 414 | height: 100%; 415 | } 416 | .div_search_group { 417 | border-top: solid black 1px; 418 | padding-top: 0.5em; 419 | padding-bottom: -1em; 420 | } 421 | .div_search_results .div_span_src_number { 422 | cursor: pointer; 423 | } 424 | .div_search_results .span_src { 425 | cursor: pointer; 426 | } 427 | .div_search_group .div_span_src_number { 428 | cursor: pointer; 429 | } 430 | .div_search_group .span_src { 431 | cursor: pointer; 432 | } 433 | 434 | #div_mod_path { 435 | padding-bottom: 1em; 436 | } 437 | .summary_sig_main { 438 | font-size: 1.4em; 439 | display: inline; 440 | white-space: pre; 441 | padding-left: 1em; 442 | position: relative; 443 | z-index: 1; 444 | width: -moz-max-content; 445 | vertical-align: middle; 446 | } 447 | .summary_sig_sub { 448 | font-size: 1.2em; 449 | display: inline; 450 | white-space: pre; 451 | padding-left: 1em; 452 | position: relative; 453 | z-index: 1; 454 | width: -moz-max-content; 455 | vertical-align: middle; 456 | } 457 | .div_summary_children { 458 | padding-top: 0.3em; 459 | padding-left: 2em; 460 | } 461 | .div_summary_doc { 462 | padding-top: 0.5em; 463 | padding-left: 2em; 464 | } 465 | #div_summary_doc_summary { 466 | display: inline-block; 467 | padding-left: 1em; 468 | margin: 0.2em; 469 | } 470 | #div_summary_doc_more { 471 | margin-bottom: 0.2em; 472 | } 473 | #div_summary_doc_summary p { 474 | margin-top: 0; 475 | margin-bottom: 0.2em; 476 | } 477 | #div_summary_doc_more p { 478 | margin-top: 0; 479 | margin-bottom: 0.2em; 480 | } 481 | .div_summary_sub { 482 | margin: 0.2em; 483 | } 484 | .div_summary_doc_sub { 485 | display: block; 486 | padding-left: 1em; 487 | margin: 0.2em; 488 | } 489 | 490 | .hand_cursor { 491 | cursor: pointer; 492 | } 493 | 494 | #div_dir_view, #div_src_view, #div_loading, #div_search_results { 495 | position: relative; 496 | padding: 0.5em 0em; 497 | } 498 | 499 | #div_status_display { 500 | flex: 0; 501 | padding: 10px 20px; 502 | background-color: #CCCCFF; 503 | } 504 | 505 | #div_dir_path { 506 | padding: 10px 20px; 507 | background-color: #CCCCFF; 508 | } 509 | .div_src_view { 510 | 511 | } 512 | .div_sidebar { 513 | flex: 0 0 25%; 514 | background-color: lightgrey; 515 | display: flex; 516 | flex-direction: column; 517 | overflow: hidden; 518 | } 519 | .div_side_tabbar { 520 | flex: 1; 521 | display: flex; 522 | flex-direction: column; 523 | overflow: hidden; 524 | } 525 | .div_sidebar_tabs { 526 | display: flex; 527 | height: fit-content; 528 | margin: 0; 529 | padding: 0; 530 | text-align: center; 531 | } 532 | 533 | .div_sidebar_tab { 534 | flex: 1; 535 | height: fit-content; 536 | background-color: lightslategrey; 537 | padding: 10px 20px; 538 | list-style: none; 539 | cursor: pointer; 540 | } 541 | 542 | .div_sidebar_main { 543 | padding: 10px; 544 | height: 100%; 545 | overflow: scroll; 546 | display: none; 547 | } 548 | 549 | .div_sidebar_main.react-tabs__tab-panel--selected { 550 | display: block; 551 | } 552 | 553 | #search_box { 554 | width:100%; 555 | height: 20px; 556 | box-sizing: border-box; 557 | 558 | } 559 | 560 | .div_search_context_box { 561 | display: block; 562 | position: absolute; 563 | width: 100%; 564 | height: auto; 565 | border: 3px black; 566 | border-style: solid none solid none; 567 | white-space: pre; 568 | background-color: white; 569 | opacity: 1; 570 | z-index: 10; 571 | overflow: hidden; 572 | 573 | animation-duration: 0.5s; 574 | animation-name: search_context_open; 575 | animation-timing-function: linear; 576 | } 577 | 578 | @keyframes search_context_open { 579 | from { 580 | max-height: 0; 581 | } 582 | to { 583 | max-height: 14em; 584 | } 585 | } 586 | 587 | .search_context_highlight { 588 | background-color: lightgrey; 589 | display: inline-block; 590 | width: 100%; 591 | } 592 | 593 | .selected { 594 | background-color: lightgrey; 595 | } 596 | 597 | #src { 598 | flex: 1; 599 | overflow: auto; 600 | } 601 | -------------------------------------------------------------------------------- /static/rustw.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2016-2017 The Rustw Project Developers. 2 | // 3 | // Licensed under the Apache License, Version 2.0 or the MIT license 5 | // , at your 6 | // option. This file may not be copied, modified, or distributed 7 | // except according to those terms. 8 | 9 | import { renderApp as render } from './app'; 10 | 11 | export function renderApp() { 12 | render(); 13 | } 14 | -------------------------------------------------------------------------------- /static/search.js: -------------------------------------------------------------------------------- 1 | // Copyright 2017 The Rustw Project Developers. 2 | // 3 | // Licensed under the Apache License, Version 2.0 or the MIT license 5 | // , at your 6 | // option. This file may not be copied, modified, or distributed 7 | // except according to those terms. 8 | 9 | import React from 'react'; 10 | import { highlight_spans } from './utils'; 11 | 12 | class ResultSet extends React.Component { 13 | componentDidMount() { 14 | this.postRender(); 15 | } 16 | componentDidUpdate() { 17 | this.postRender(); 18 | } 19 | 20 | postRender() { 21 | $(".div_search_results .src_link").removeClass("src_link"); 22 | highlight_needle(this.props.input, this.props.kind); 23 | } 24 | 25 | render() { 26 | const { input, kind } = this.props; 27 | const self = this; 28 | let count = -1; 29 | let result = input.map((r) => { 30 | count += 1; 31 | return ; 32 | }); 33 | 34 | return
    35 | {result} 36 |
    ; 37 | } 38 | } 39 | 40 | class FileResult extends React.Component { 41 | constructor(props) { 42 | super(props); 43 | this.state = { peekContext: null }; 44 | } 45 | 46 | render() { 47 | const { lines, file_name, kind, count, app } = this.props; 48 | const self = this; 49 | let divLines = lines.map((l) => { 50 | const lineId = `snippet_line_number_${kind}_${count}_${l.line_start}`; 51 | const snippetId = `snippet_line_${kind}_${count}_${l.line_start}`; 52 | 53 | // Squash the indent down by a factor of four. 54 | const text = l.line; 55 | let trimmed = text.trimLeft(); 56 | const newIndent = (text.length - trimmed.length) / 4; 57 | const diffIndent = (text.length - trimmed.length) - newIndent; 58 | trimmed = trimmed.padStart(trimmed.length + newIndent); 59 | 60 | const lineClick = (e) => { 61 | const highlight = { 62 | "line_start": l.line_start, 63 | "line_end": l.line_start, 64 | "column_start": 0, 65 | "column_end": 0 66 | }; 67 | app.loadSource(file_name, highlight); 68 | e.preventDefault(); 69 | e.stopPropagation(); 70 | }; 71 | const snippetClick = (e) => { 72 | const highlight = { 73 | "line_start": l.line_start, 74 | "line_end": l.line_end, 75 | "column_start": l.column_start, 76 | "column_end": l.column_end 77 | }; 78 | app.loadSource(file_name, highlight); 79 | e.preventDefault(); 80 | e.stopPropagation(); 81 | }; 82 | 83 | const onMouseOver = (e) => { 84 | self.setState({ peekContext: { pre: l.pre_context, post: l.post_context }}); 85 | e.preventDefault(); 86 | e.stopPropagation(); 87 | } 88 | const onMouseOut = (e) => { 89 | self.setState({ peekContext: null }); 90 | e.preventDefault(); 91 | e.stopPropagation(); 92 | } 93 | 94 | let context = null; 95 | if (this.state.peekContext) { 96 | context = 97 | } 98 | 99 | return
    100 | 101 |
    {l.line_start}
    102 |
    103 | 104 |
    105 | 106 | {context} 107 |
    108 |
    ; 109 | }); 110 | const onClick = (e) => { 111 | this.props.app.loadSource(file_name); 112 | e.preventDefault(); 113 | e.stopPropagation(); 114 | }; 115 | return
    116 |
    {file_name}
    117 |
    118 | {divLines} 119 |
    120 |
    ; 121 | } 122 | } 123 | 124 | class StructuredResultSet extends React.Component { 125 | componentDidMount() { 126 | this.postRender(); 127 | } 128 | componentDidUpdate() { 129 | this.postRender(); 130 | } 131 | 132 | postRender() { 133 | $(".div_search_group .src_link").removeClass("src_link"); 134 | const defFile = { file_name: this.props.input.file, lines: [this.props.input.line] }; 135 | highlight_needle([defFile], "def"); 136 | } 137 | 138 | render() { 139 | const def = ; 140 | const refs = 141 | return
    142 | {def} 143 |
    References
    144 | {refs} 145 |
    ; 146 | } 147 | } 148 | 149 | function noResults() { 150 | return No results found; 151 | } 152 | 153 | export function FindResults(props) { 154 | if (!props.results) { 155 | return noResults(); 156 | } else { 157 | return
    158 |
    Search results
    159 | 160 |
    ; 161 | } 162 | } 163 | 164 | export function SearchResults(props) { 165 | if (!props.defs) { 166 | return noResults(); 167 | } else { 168 | let count = -1; 169 | let defs = props.defs.map((d) => { 170 | count += 1; 171 | return ; 172 | }); 173 | return
    174 |
    Search results
    175 | {defs} 176 |
    ; 177 | } 178 | } 179 | 180 | function highlight_needle(results, tag) { 181 | results.map((file, index) => { 182 | file.lines.map((line) => { 183 | line.line_end = line.line_start; 184 | highlight_spans(line, 185 | null, 186 | `snippet_line_${tag}_${index}_`, 187 | "selected_search"); 188 | }) 189 | }) 190 | } 191 | 192 | function SearchContext(props) { 193 | const text = props.preContext + '\n' + props.line + '\n' + props.postContext; 194 | return
    195 |
    196 | } 197 | -------------------------------------------------------------------------------- /static/searchPanel.js: -------------------------------------------------------------------------------- 1 | // Copyright 2017 The Rustw Project Developers. 2 | // 3 | // Licensed under the Apache License, Version 2.0 or the MIT license 5 | // , at your 6 | // option. This file may not be copied, modified, or distributed 7 | // except according to those terms. 8 | 9 | import React from 'react'; 10 | import { FindResults, SearchResults } from "./search"; 11 | 12 | export class SearchPanel extends React.Component { 13 | render() { 14 | let searchResults = null; 15 | if (this.props.defs || this.props.refs) { 16 | searchResults = ; 17 | } else if (this.props.results) { 18 | searchResults = ; 19 | } 20 | 21 | return
    22 | 23 |
    {searchResults}
    24 |
    ; 25 | } 26 | } 27 | 28 | function SearchBox(props) { 29 | return
    30 | 31 |
    ; 32 | } 33 | -------------------------------------------------------------------------------- /static/sidebar.js: -------------------------------------------------------------------------------- 1 | // Copyright 2017 The Rustw Project Developers. 2 | // 3 | // Licensed under the Apache License, Version 2.0 or the MIT license 5 | // , at your 6 | // option. This file may not be copied, modified, or distributed 7 | // except according to those terms. 8 | 9 | import React from 'react'; 10 | import { Tab, Tabs, TabList, TabPanel } from 'react-tabs'; 11 | 12 | import { SearchPanel } from './searchPanel.js'; 13 | import { TreePanel } from './treePanel.js'; 14 | import { SymbolPanel } from './symbolPanel.js'; 15 | 16 | 17 | export class Sidebar extends React.Component { 18 | constructor(props) { 19 | super(props); 20 | this.state = { symbols: null, tabIndex: 0, searchTerm: "" }; 21 | } 22 | 23 | componentDidUpdate(prevProps) { 24 | if (this.props.search != prevProps.search) { 25 | let searchTerm = ""; 26 | if (this.props.search.searchTerm) { 27 | searchTerm = this.props.search.searchTerm; 28 | } 29 | this.setState({ tabIndex: 0, searchTerm }); 30 | } 31 | } 32 | 33 | render() { 34 | // We must keep the search box controller state here so that we preserve 35 | // the text in the box during tab switches. 36 | const enterKeyCode = 13; 37 | const searchController = { 38 | searchTerm: this.state.searchTerm, 39 | onKeyPress: (e) => { 40 | if (e.which === enterKeyCode) { 41 | this.props.app.getSearch(e.currentTarget.value); 42 | } 43 | }, 44 | onChange: (e) => { 45 | this.setState({searchTerm: e.target.value}); 46 | }, 47 | }; 48 | 49 | const onSelect = tabIndex => this.setState({ tabIndex }); 50 | 51 | return
    52 | 53 | 54 | search 55 | files 56 | symbols 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 |
    ; 70 | } 71 | } 72 | 73 | class StatusBar extends React.Component { 74 | render() { 75 | let status = ""; 76 | if (this.props.status) { 77 | status = this.props.status; 78 | } 79 | return
    80 | Status: {status} 81 |
    ; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /static/srcView.js: -------------------------------------------------------------------------------- 1 | // Copyright 2017 The Rustw Project Developers. 2 | // 3 | // Licensed under the Apache License, Version 2.0 or the MIT license 5 | // , at your 6 | // option. This file may not be copied, modified, or distributed 7 | // except according to those terms. 8 | 9 | import React from 'react'; 10 | 11 | import * as utils from './utils'; 12 | import { BreadCrumbs } from './breadCrumbs'; 13 | import { MenuHost, Menu } from './menus'; 14 | import SanitizedHTML from 'react-sanitized-html'; 15 | 16 | // Menus, highlighting on mouseover. 17 | function add_ref_functionality(self) { 18 | for (const el of $("#div_src_view").find(".class_id")) { 19 | const $element = $(el); 20 | const classes = el.className.split(' '); 21 | // FIXME[ES6]: use classes.find() and then execute following code 22 | let c = classes.find((c) => c.startsWith('class_id_')); 23 | if(c === undefined) { 24 | return; 25 | } 26 | $element.hover(function() { 27 | $("." + c).css("background-color", "#d5f3b5"); 28 | }, function() { 29 | $("." + c).css("background-color", ""); 30 | }); 31 | 32 | const id = c.slice('class_id_'.length); 33 | const showRefMenu = (ev) => { 34 | self.setState({ refMenu: { "top": ev.pageY, "left": ev.pageX, target: ev.target, id }}); 35 | ev.preventDefault(); 36 | ev.stopPropagation(); 37 | }; 38 | $element.off("contextmenu"); 39 | $element.on("contextmenu", showRefMenu); 40 | $element.addClass("hand_cursor"); 41 | } 42 | } 43 | 44 | // props: location, onClose, target 45 | // location: { "top": event.pageY, "left": event.pageX } 46 | function LineNumberMenu(props) { 47 | let items = []; 48 | if (CONFIG.edit_command) { 49 | items.push({ id: "line_number_menu_edit", label: "edit", fn: edit, unstable: true }); 50 | } 51 | if (CONFIG.vcs_link) { 52 | items.push({ id: "line_number_vcs", label: "view in VCS", fn: view_in_vcs }); 53 | } 54 | return
    ; 55 | } 56 | 57 | // props: location, onClose, target, id 58 | // location: { "top": event.pageY, "left": event.pageX } 59 | function RefMenu(props) { 60 | let items = []; 61 | 62 | const file_loc = props.target.dataset.link.split(':'); 63 | const file = file_loc[0]; 64 | 65 | if (file != "search") { 66 | let data = utils.parseLink(file_loc); 67 | items.push({ id: "ref_menu_goto_def", label: "goto def", fn: () => props.app.loadSource(file, data) }); 68 | } 69 | 70 | const docUrl = props.target.dataset.docLink; 71 | if (docUrl) { 72 | items.push({ id: "ref_menu_view_docs", label: "view docs", fn: () => window.open(docUrl, '_blank') }); 73 | } 74 | const srcUrl = props.target.dataset.srcLink; 75 | if (srcUrl) { 76 | items.push({ id: "ref_menu_view_source", label: "view source", fn: window.open(srcUrl, '_blank') }); 77 | } 78 | 79 | items.push({ id: "ref_menu_find_uses", label: "find all uses", fn: () => props.app.getUses(props.id) }); 80 | 81 | let impls = props.target.dataset.impls; 82 | // XXX non strict comparison 83 | if (impls && impls != "0") { 84 | items.push({ id: "ref_menu_find_impls", label: "find impls (" + impls + ")", fn: () => props.app.getImpls(props.id) }); 85 | } 86 | 87 | return ; 88 | } 89 | 90 | function view_in_vcs(target) { 91 | const link = target.dataset.link; 92 | const colon = link.lastIndexOf(':'); 93 | const file_name = link.substring(CONFIG.workspace_root.length + 2, colon); 94 | const line_number = link.substring(colon + 1); 95 | window.open(CONFIG.vcs_link.replace("$file", file_name).replace("$line", line_number), '_blank'); 96 | } 97 | 98 | function edit(target) { 99 | utils.request( 100 | 'edit?file=' + target.dataset.link, 101 | function() { 102 | console.log("edit - success"); 103 | }, 104 | "Error with search edit", 105 | null 106 | ); 107 | } 108 | 109 | // See https://github.com/Microsoft/TypeScript/issues/18134 110 | /** @augments {React.Component} */ 111 | export class SourceView extends React.Component { 112 | constructor(props) { 113 | super(props); 114 | this.state = { refMenu: null }; 115 | } 116 | 117 | componentDidMount() { 118 | this.componentDidUpdate(); 119 | } 120 | 121 | componentDidUpdate() { 122 | if (this.props.highlight) { 123 | utils.highlight_spans(this.props.highlight, "src_line_number_", "src_line_", "selected", this.node); 124 | } else { 125 | utils.unHighlight("selected", this.node) 126 | } 127 | 128 | // Make source links active. 129 | var $linkables = $("#div_src_view").find(".src_link"); 130 | $linkables.off("click"); 131 | $linkables.click((e) => { 132 | // The data for what to do on-click is encoded in the data-link attribute. 133 | // We need to process it here. 134 | e.preventDefault(); 135 | e.stopPropagation(); 136 | 137 | var docUrl = e.target.dataset.docLink; 138 | if (docUrl) { 139 | window.open(docUrl, '_blank'); 140 | return; 141 | } 142 | 143 | var file_loc = e.target.dataset.link.split(':'); 144 | var file = file_loc[0]; 145 | 146 | if (file === "search") { 147 | this.props.app.getUses(file_loc[1]); 148 | return; 149 | } 150 | 151 | let data = utils.parseLink(file_loc); 152 | this.props.app.loadSource(file, data); 153 | }); 154 | 155 | add_ref_functionality(this); 156 | 157 | if (this.props.highlight) { 158 | jumpToLine(this.props.highlight.line_start); 159 | } 160 | } 161 | 162 | render() { 163 | const path = this.props.path.join('/'); 164 | let count = 0, 165 | numbers = [], 166 | content = this.props.content, 167 | lines = this.props.lines && this.props.lines.map((l) => { 168 | count += 1; 169 | numbers.push(); 170 | return (); 171 | }); 172 | 173 | let refMenu = null; 174 | if (this.state.refMenu) { 175 | const onClose = () => { 176 | return this.setState({ refMenu: null }); 177 | }; 178 | 179 | refMenu = ; 180 | } 181 | 182 | const allowedTags = [ 183 | 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'p', 'a', 'ul', 'ol', 'nl', 'li', 'b', 'i', 184 | 'img', 'strong', 'em', 'strike', 'code', 'hr', 'br', 'div', 'table', 'thead', 'caption', 'tbody', 185 | 'tr', 'th', 'td', 'pre', 186 | ]; 187 | 188 | const View = { 189 | RENDERED: 'content', 190 | SOURCE: 'source', 191 | }; 192 | 193 | let viewSelector; 194 | let currentView = this.state.currentView; 195 | 196 | const setView = (to) => { 197 | this.setState({ currentView: to }) 198 | }; 199 | 200 | if (content && lines) { 201 | currentView = currentView || View.RENDERED; 202 | viewSelector = ; 214 | } else if (content) { 215 | currentView = View.RENDERED; 216 | } else { 217 | currentView = View.SOURCE; 218 | } 219 | 220 | return
    this.node = node}> 221 | 222 | { viewSelector } 223 |
    224 | { 225 | currentView == View.RENDERED 226 | ? 231 | :
    232 | 233 | {numbers} 234 | 235 | 236 | {lines} 237 | 238 |
    239 | } 240 | 241 | {refMenu} 242 |
    243 |
    ; 244 | } 245 | } 246 | 247 | function jumpToLine(line) { 248 | // Jump to the start line. 100 is a fudge so that the start line is not 249 | // right at the top of the window, which makes it easier to see. 250 | var y = line * $("#src_line_number_1").outerHeight() - 100; 251 | let div = document.getElementById("src"); 252 | div.scroll(0, y); 253 | } 254 | 255 | class LineNumber extends MenuHost { 256 | constructor(props) { 257 | super(props); 258 | this.menuFn = LineNumberMenu; 259 | } 260 | 261 | renderInner() { 262 | const numId = "src_line_number_" + this.props.count; 263 | const link = this.props.path + ":" + this.props.count; 264 | return
    265 | {this.props.count} 266 |
    ; 267 | } 268 | } 269 | 270 | function Line(props) { 271 | const line = !props.line ? ' ' : props.line; 272 | const lineId = "src_line_" + props.count; 273 | return
    ; 274 | } 275 | -------------------------------------------------------------------------------- /static/symbolPanel.js: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Rustw Project Developers. 2 | // 3 | // Licensed under the Apache License, Version 2.0 or the MIT license 5 | // , at your 6 | // option. This file may not be copied, modified, or distributed 7 | // except according to those terms. 8 | 9 | import React from 'react'; 10 | import { Treebeard } from 'react-treebeard'; 11 | import { request } from './utils'; 12 | 13 | // FIXME share code with treePanel 14 | 15 | export class SymbolPanel extends React.Component { 16 | constructor(props) { 17 | super(props); 18 | this.onToggle = this.onToggle.bind(this); 19 | this.state = {}; 20 | } 21 | 22 | componentDidMount() { 23 | this.props.app.refreshStatus(); 24 | } 25 | componentDidUpdate() { 26 | this.props.app.refreshStatus(); 27 | } 28 | 29 | onToggle(node, toggled) { 30 | const {cursor} = this.state; 31 | const self = this; 32 | 33 | // Tree handling overhead 34 | if (cursor) { 35 | cursor.active = false; 36 | } 37 | node.active = true; 38 | node.toggled = toggled; 39 | this.setState({ cursor: node }); 40 | 41 | // Jump to the line in the source code. 42 | if (node.file_name) { 43 | this.props.app.loadSource(node.file_name, { line_start: node.line_start, line_end: node.line_start }); 44 | } 45 | 46 | // Get any children from the server and add them to the tree. 47 | if (node.symId && (!node.children || node.children.length == 0)) { 48 | request( 49 | 'symbol_children?id=' + node.symId, 50 | function(json) { 51 | // FIXME? We are mutating the state of app here, I'm pretty sure this is bad practice. 52 | node.children = json.map(makeTreeNode); 53 | self.props.app.setState({}); 54 | }, 55 | "Error with symbol_children request", 56 | null 57 | ); 58 | } 59 | } 60 | 61 | render() { 62 | if (!this.props.symbols) { 63 | return
    loading...
    ; 64 | } 65 | 66 | return ( 67 | 68 | ); 69 | } 70 | } 71 | 72 | export function makeTreeData(rootData) { 73 | return { 74 | name: 'symbols', 75 | toggled: true, 76 | children: rootData.map(makeTreeNode), 77 | }; 78 | } 79 | 80 | function makeTreeNode(symData) { 81 | return { 82 | name: symData.name, 83 | toggled: false, 84 | children: [], 85 | symId: symData.id, 86 | file_name: symData.file_name, 87 | line_start: symData.line_start, 88 | }; 89 | } 90 | 91 | const style = { 92 | tree: { 93 | base: { 94 | listStyle: 'none', 95 | //backgroundColor: '#21252B', 96 | margin: 0, 97 | padding: 0, 98 | //color: '#9DA5AB', 99 | //fontFamily: 'lucida grande ,tahoma,verdana,arial,sans-serif', 100 | //fontSize: '14px' 101 | }, 102 | node: { 103 | container: { 104 | link: { 105 | cursor: 'pointer', position: 'relative', padding: '0px 5px', display: 'block' 106 | }, 107 | activeLink: { 108 | background: '#31363F' 109 | } 110 | }, 111 | base: { 112 | position: 'relative' 113 | }, 114 | link: { 115 | cursor: 'pointer', 116 | position: 'relative', 117 | padding: '0px 5px', 118 | display: 'block' 119 | }, 120 | activeLink: { 121 | background: '#B1B6BF' 122 | }, 123 | toggle: { 124 | base: { 125 | position: 'relative', 126 | display: 'inline-block', 127 | verticalAlign: 'top', 128 | marginLeft: '-5px', 129 | height: '24px', 130 | width: '24px' 131 | }, 132 | wrapper: { 133 | position: 'absolute', 134 | top: '50%', 135 | left: '50%', 136 | margin: '-7px 0 0 -7px', 137 | height: '14px' 138 | }, 139 | height: 14, 140 | width: 14, 141 | arrow: { 142 | //fill: '#9DA5AB', 143 | strokeWidth: 0 144 | } 145 | }, 146 | header: { 147 | base: { 148 | display: 'inline-block', 149 | verticalAlign: 'top', 150 | //color: '#9DA5AB' 151 | }, 152 | connector: { 153 | width: '2px', 154 | height: '12px', 155 | borderLeft: 'solid 2px black', 156 | borderBottom: 'solid 2px black', 157 | position: 'absolute', 158 | top: '0px', 159 | left: '-21px' 160 | }, 161 | title: { 162 | lineHeight: '24px', 163 | verticalAlign: 'middle' 164 | } 165 | }, 166 | subtree: { 167 | listStyle: 'none', 168 | paddingLeft: '19px' 169 | }, 170 | loading: { 171 | color: '#E2C089' 172 | } 173 | } 174 | } 175 | }; 176 | -------------------------------------------------------------------------------- /static/treePanel.js: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Rustw Project Developers. 2 | // 3 | // Licensed under the Apache License, Version 2.0 or the MIT license 5 | // , at your 6 | // option. This file may not be copied, modified, or distributed 7 | // except according to those terms. 8 | 9 | import React from 'react'; 10 | import { Treebeard } from 'react-treebeard'; 11 | 12 | export class TreePanel extends React.Component { 13 | constructor(props) { 14 | super(props); 15 | this.state = { data: makeTreeData(this.props.tree.Directory) }; 16 | this.onToggle = this.onToggle.bind(this); 17 | } 18 | 19 | onToggle(node, toggled){ 20 | const {cursor} = this.state; 21 | 22 | if (cursor) { 23 | cursor.active = false; 24 | } 25 | 26 | node.active = true; 27 | if (node.children) { 28 | node.toggled = toggled; 29 | } else { 30 | this.props.app.loadSource(node.path); 31 | } 32 | this.setState({ cursor: node }); 33 | } 34 | 35 | render() { 36 | return ( 37 | 38 | ); 39 | } 40 | } 41 | 42 | function makeTreeData(dirData) { 43 | return { 44 | name: dirData.path[dirData.path.length - 1], 45 | toggled: true, 46 | children: dirData.files.map(node), 47 | path: dirData.path.join('/'), 48 | }; 49 | } 50 | 51 | function node(fileData) { 52 | let children = null; 53 | if (fileData.kind.DirectoryTree) { 54 | children = fileData.kind.DirectoryTree.map(node); 55 | } 56 | 57 | return { 58 | name: fileData.name, 59 | toggled: false, 60 | children, 61 | path: fileData.path, 62 | }; 63 | } 64 | 65 | const style = { 66 | tree: { 67 | base: { 68 | listStyle: 'none', 69 | //backgroundColor: '#21252B', 70 | margin: 0, 71 | padding: 0, 72 | //color: '#9DA5AB', 73 | //fontFamily: 'lucida grande ,tahoma,verdana,arial,sans-serif', 74 | //fontSize: '14px' 75 | }, 76 | node: { 77 | container: { 78 | link: { 79 | cursor: 'pointer', position: 'relative', padding: '0px 5px', display: 'block' 80 | }, 81 | activeLink: { 82 | background: '#31363F' 83 | } 84 | }, 85 | base: { 86 | position: 'relative' 87 | }, 88 | link: { 89 | cursor: 'pointer', 90 | position: 'relative', 91 | padding: '0px 5px', 92 | display: 'block' 93 | }, 94 | activeLink: { 95 | background: '#B1B6BF' 96 | }, 97 | toggle: { 98 | base: { 99 | position: 'relative', 100 | display: 'inline-block', 101 | verticalAlign: 'top', 102 | marginLeft: '-5px', 103 | height: '24px', 104 | width: '24px' 105 | }, 106 | wrapper: { 107 | position: 'absolute', 108 | top: '50%', 109 | left: '50%', 110 | margin: '-7px 0 0 -7px', 111 | height: '14px' 112 | }, 113 | height: 14, 114 | width: 14, 115 | arrow: { 116 | //fill: '#9DA5AB', 117 | strokeWidth: 0 118 | } 119 | }, 120 | header: { 121 | base: { 122 | display: 'inline-block', 123 | verticalAlign: 'top', 124 | //color: '#9DA5AB' 125 | }, 126 | connector: { 127 | width: '2px', 128 | height: '12px', 129 | borderLeft: 'solid 2px black', 130 | borderBottom: 'solid 2px black', 131 | position: 'absolute', 132 | top: '0px', 133 | left: '-21px' 134 | }, 135 | title: { 136 | lineHeight: '24px', 137 | verticalAlign: 'middle' 138 | } 139 | }, 140 | subtree: { 141 | listStyle: 'none', 142 | paddingLeft: '19px' 143 | }, 144 | loading: { 145 | color: '#E2C089' 146 | } 147 | } 148 | } 149 | }; 150 | -------------------------------------------------------------------------------- /static/utils.js: -------------------------------------------------------------------------------- 1 | // Copyright 2017 The Rustw Project Developers. 2 | // 3 | // Licensed under the Apache License, Version 2.0 or the MIT license 5 | // , at your 6 | // option. This file may not be copied, modified, or distributed 7 | // except according to those terms. 8 | 9 | export function make_url(suffix) { 10 | return '/' + CONFIG.demo_mode_root_path + suffix; 11 | } 12 | 13 | export function unHighlight(css_class, element) { 14 | if (element) { 15 | let $highlighted = $(element).find('.' + css_class); 16 | $highlighted.removeClass(css_class); 17 | let $floating = $(element).find('.' + css_class + '.floating_highlight'); 18 | $floating.remove(); 19 | } 20 | 21 | } 22 | 23 | export function highlight_spans(highlight, line_number_prefix, src_line_prefix, css_class, element) { 24 | // Remove any previous highlighting. 25 | unHighlight(css_class, element) 26 | 27 | if (!highlight.line_start || !highlight.line_end) { 28 | return; 29 | } 30 | 31 | if (line_number_prefix) { 32 | for (let i = highlight.line_start; i <= highlight.line_end; ++i) { 33 | $("#" + line_number_prefix + i).addClass(css_class); 34 | } 35 | } 36 | 37 | if (!highlight.column_start || !highlight.column_end || !src_line_prefix) { 38 | return; 39 | } 40 | 41 | // Highlight all of the middle lines. 42 | for (let i = highlight.line_start + 1; i <= highlight.line_end - 1; ++i) { 43 | $("#" + src_line_prefix + i).addClass(css_class); 44 | } 45 | 46 | // If we don't have columns (at least a start), then highlight all the lines. 47 | // If we do, then highlight between columns. 48 | if (highlight.column_start <= 0) { 49 | $("#" + src_line_prefix + highlight.line_start).addClass(css_class); 50 | $("#" + src_line_prefix + highlight.line_end).addClass(css_class); 51 | } else { 52 | // First line 53 | const lhs = (highlight.column_start - 1); 54 | let rhs = 0; 55 | if (highlight.line_end === highlight.line_start && highlight.column_end > 0) { 56 | // If we're only highlighting one line, then the highlight must stop 57 | // before the end of the line. 58 | rhs = (highlight.column_end - 1); 59 | } 60 | make_highlight(src_line_prefix, highlight.line_start, lhs, rhs, css_class); 61 | 62 | // Last line 63 | if (highlight.line_end > highlight.line_start) { 64 | rhs = 0; 65 | if (highlight.column_end > 0) { 66 | rhs = (highlight.column_end - 1); 67 | } 68 | make_highlight(src_line_prefix, highlight.line_end, 0, rhs, css_class); 69 | } 70 | } 71 | } 72 | 73 | // Only supply the `app` argument if you want to show loading/error messages 74 | // in the main content panel. 75 | export function request(urlStr, success, errStr, app) { 76 | $.ajax({ 77 | url: make_url(urlStr), 78 | type: 'POST', 79 | dataType: 'JSON', 80 | cache: false 81 | }) 82 | .done(success) 83 | .fail(function (xhr, status, errorThrown) { 84 | console.log(errStr); 85 | console.log("error: " + errorThrown + "; status: " + status); 86 | 87 | if (app) { 88 | app.showError(); 89 | } 90 | }); 91 | 92 | if (app) { 93 | app.showLoading(); 94 | } 95 | } 96 | 97 | export function parseLink(file_loc) { 98 | let line_start = parseInt(file_loc[1], 10); 99 | let column_start = parseInt(file_loc[2], 10); 100 | let line_end = parseInt(file_loc[3], 10); 101 | let column_end = parseInt(file_loc[4], 10); 102 | 103 | if (line_start === 0 || isNaN(line_start)) { 104 | line_start = 0; 105 | line_end = 0; 106 | } else if (line_end === 0 || isNaN(line_end)) { 107 | line_end = line_start; 108 | } 109 | 110 | if (isNaN(column_start) || isNaN(column_end)) { 111 | column_start = 0; 112 | column_end = 0; 113 | } 114 | 115 | // FIXME the displayed span doesn't include column start and end, should it? 116 | // var display = ""; 117 | // if (line_start > 0) { 118 | // display += ":" + line_start; 119 | // if (!(line_end == 0 || line_end == line_start)) { 120 | // display += ":" + line_end; 121 | // } 122 | // } 123 | 124 | var data = { 125 | "line_start": line_start, 126 | "line_end": line_end, 127 | "column_start": column_start, 128 | "column_end": column_end 129 | }; 130 | 131 | return data; 132 | } 133 | 134 | // Left is the number of chars from the left margin to where the highlight 135 | // should start. right is the number of chars to where the highlight should end. 136 | // If right == 0, we take it as the last char in the line. 137 | // 1234 | text highlight text 138 | // ^ ^-------^ 139 | // |origin 140 | // |----| left 141 | // |------------| right 142 | function make_highlight(src_line_prefix, line_number, left, right, css_class) { 143 | var $line_div = $("#" + src_line_prefix + line_number); 144 | var $highlight = $("
     
    "); 145 | $highlight.addClass(css_class + " floating_highlight"); 146 | 147 | const adjust = $line_div.data('adjust'); 148 | if (adjust) { 149 | left -= adjust; 150 | right -= adjust; 151 | } 152 | 153 | left *= CHAR_WIDTH; 154 | right *= CHAR_WIDTH; 155 | if (right === 0) { 156 | right = $line_div.width(); 157 | } 158 | 159 | var width = right - left; 160 | var padding = parseInt($line_div.css("padding-left")); 161 | if (left > 0) { 162 | left += padding; 163 | } else { 164 | width += padding; 165 | } 166 | 167 | var offset = $line_div.offset(); 168 | if (offset) { 169 | $line_div.after($highlight); 170 | offset.left += left; 171 | $highlight.offset(offset); 172 | $highlight.width(width); 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "allowSyntheticDefaultImports": true, 5 | "jsx": "react", 6 | "module": "commonjs", 7 | "noImplicitAny": true, 8 | "outDir": "./dist/", 9 | "sourceMap": true, 10 | "target": "es2016", 11 | }, 12 | "include": [ 13 | "./static/**/*" 14 | ], 15 | "exclude": [ 16 | "./node_modules/**/*" 17 | ], 18 | 19 | } -------------------------------------------------------------------------------- /type-on-hover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rust-dev-tools/cargo-src/328f783ac40245c298ee6c1e9654f4d4e8d46e7b/type-on-hover.png -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | entry: "./static/rustw.ts", 5 | output: { 6 | filename: "./static/rustw.out.js", 7 | path: path.resolve(__dirname), 8 | libraryTarget: 'var', 9 | library: 'Rustw' 10 | }, 11 | resolve: { 12 | extensions: [".js", ".ts", ".tsx"] 13 | }, 14 | module: { 15 | rules: [ 16 | { 17 | test: /\.js$/, 18 | exclude: /node_modules/, 19 | loader: 'babel-loader' 20 | }, 21 | { 22 | test: /\.tsx?$/, 23 | exclude: /node_modules/, 24 | loader: 'ts-loader' 25 | }] 26 | }, 27 | devtool: 'source-map' 28 | } 29 | --------------------------------------------------------------------------------