├── .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 | [](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 | 
34 |
35 | ### Hover to show the type of an identifier:
36 |
37 | 
38 |
39 | ### Search for an identifier name (shows definitions, and all references to each definition):
40 |
41 | 
42 |
43 | ### Find all uses of a definition:
44 |
45 | 
46 |
47 | ### Right click an identifier to show more options
48 |
49 | 
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:/^ *\[([^\]]+)\]: *([^\s>]+)>?(?: +["(]([^\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"\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+""+type+">\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+""+type+">\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='"+text+"";return out};Renderer.prototype.image=function(href,title,text){var out='
":">";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