aboutsummaryrefslogtreecommitdiff
path: root/crates/ide
diff options
context:
space:
mode:
Diffstat (limited to 'crates/ide')
-rw-r--r--crates/ide/src/completion/complete_postfix/format_like.rs2
-rw-r--r--crates/ide/src/doc_links.rs (renamed from crates/ide/src/link_rewrite.rs)285
-rw-r--r--crates/ide/src/hover.rs2
-rw-r--r--crates/ide/src/lib.rs16
-rw-r--r--crates/ide/src/references.rs25
-rw-r--r--crates/ide/src/references/rename.rs175
-rw-r--r--crates/ide/src/syntax_highlighting/test_data/highlighting.html2
7 files changed, 448 insertions, 59 deletions
diff --git a/crates/ide/src/completion/complete_postfix/format_like.rs b/crates/ide/src/completion/complete_postfix/format_like.rs
index 81c33bf3a..50d1e5c81 100644
--- a/crates/ide/src/completion/complete_postfix/format_like.rs
+++ b/crates/ide/src/completion/complete_postfix/format_like.rs
@@ -25,6 +25,7 @@ static KINDS: &[(&str, &str)] = &[
25 ("fmt", "format!"), 25 ("fmt", "format!"),
26 ("panic", "panic!"), 26 ("panic", "panic!"),
27 ("println", "println!"), 27 ("println", "println!"),
28 ("eprintln", "eprintln!"),
28 ("logd", "log::debug!"), 29 ("logd", "log::debug!"),
29 ("logt", "log::trace!"), 30 ("logt", "log::trace!"),
30 ("logi", "log::info!"), 31 ("logi", "log::info!"),
@@ -259,6 +260,7 @@ mod tests {
259 fn test_into_suggestion() { 260 fn test_into_suggestion() {
260 let test_vector = &[ 261 let test_vector = &[
261 ("println!", "{}", r#"println!("{}", $1)"#), 262 ("println!", "{}", r#"println!("{}", $1)"#),
263 ("eprintln!", "{}", r#"eprintln!("{}", $1)"#),
262 ( 264 (
263 "log::info!", 265 "log::info!",
264 "{} {expr} {} {2 + 2}", 266 "{} {expr} {} {2 + 2}",
diff --git a/crates/ide/src/link_rewrite.rs b/crates/ide/src/doc_links.rs
index c317a2379..06af36b73 100644
--- a/crates/ide/src/link_rewrite.rs
+++ b/crates/ide/src/doc_links.rs
@@ -1,13 +1,27 @@
1//! Resolves and rewrites links in markdown documentation. 1//! Resolves and rewrites links in markdown documentation.
2//!
3//! Most of the implementation can be found in [`hir::doc_links`].
4 2
5use hir::{Adt, Crate, HasAttrs, ModuleDef}; 3use std::iter::once;
6use ide_db::{defs::Definition, RootDatabase}; 4
5use itertools::Itertools;
7use pulldown_cmark::{CowStr, Event, LinkType, Options, Parser, Tag}; 6use pulldown_cmark::{CowStr, Event, LinkType, Options, Parser, Tag};
8use pulldown_cmark_to_cmark::{cmark_with_options, Options as CmarkOptions}; 7use pulldown_cmark_to_cmark::{cmark_with_options, Options as CmarkOptions};
9use url::Url; 8use url::Url;
10 9
10use hir::{
11 db::{DefDatabase, HirDatabase},
12 Adt, AsAssocItem, AsName, AssocItem, AssocItemContainer, Crate, Field, HasAttrs, ItemInNs,
13 ModuleDef,
14};
15use ide_db::{
16 defs::{classify_name, classify_name_ref, Definition},
17 RootDatabase,
18};
19use syntax::{ast, match_ast, AstNode, SyntaxKind::*, SyntaxToken, TokenAtOffset, T};
20
21use crate::{FilePosition, Semantics};
22
23pub type DocumentationLink = String;
24
11/// Rewrite documentation links in markdown to point to an online host (e.g. docs.rs) 25/// Rewrite documentation links in markdown to point to an online host (e.g. docs.rs)
12pub fn rewrite_links(db: &RootDatabase, markdown: &str, definition: &Definition) -> String { 26pub fn rewrite_links(db: &RootDatabase, markdown: &str, definition: &Definition) -> String {
13 let doc = Parser::new_with_broken_link_callback( 27 let doc = Parser::new_with_broken_link_callback(
@@ -80,6 +94,70 @@ pub fn remove_links(markdown: &str) -> String {
80 out 94 out
81} 95}
82 96
97// FIXME:
98// BUG: For Option::Some
99// Returns https://doc.rust-lang.org/nightly/core/prelude/v1/enum.Option.html#variant.Some
100// Instead of https://doc.rust-lang.org/nightly/core/option/enum.Option.html
101//
102// This should cease to be a problem if RFC2988 (Stable Rustdoc URLs) is implemented
103// https://github.com/rust-lang/rfcs/pull/2988
104fn get_doc_link(db: &RootDatabase, definition: Definition) -> Option<String> {
105 // Get the outermost definition for the moduledef. This is used to resolve the public path to the type,
106 // then we can join the method, field, etc onto it if required.
107 let target_def: ModuleDef = match definition {
108 Definition::ModuleDef(moddef) => match moddef {
109 ModuleDef::Function(f) => f
110 .as_assoc_item(db)
111 .and_then(|assoc| match assoc.container(db) {
112 AssocItemContainer::Trait(t) => Some(t.into()),
113 AssocItemContainer::ImplDef(impld) => {
114 impld.target_ty(db).as_adt().map(|adt| adt.into())
115 }
116 })
117 .unwrap_or_else(|| f.clone().into()),
118 moddef => moddef,
119 },
120 Definition::Field(f) => f.parent_def(db).into(),
121 // FIXME: Handle macros
122 _ => return None,
123 };
124
125 let ns = ItemInNs::from(target_def.clone());
126
127 let module = definition.module(db)?;
128 let krate = module.krate();
129 let import_map = db.import_map(krate.into());
130 let base = once(krate.declaration_name(db)?.to_string())
131 .chain(import_map.path_of(ns)?.segments.iter().map(|name| name.to_string()))
132 .join("/");
133
134 let filename = get_symbol_filename(db, &target_def);
135 let fragment = match definition {
136 Definition::ModuleDef(moddef) => match moddef {
137 ModuleDef::Function(f) => {
138 get_symbol_fragment(db, &FieldOrAssocItem::AssocItem(AssocItem::Function(f)))
139 }
140 ModuleDef::Const(c) => {
141 get_symbol_fragment(db, &FieldOrAssocItem::AssocItem(AssocItem::Const(c)))
142 }
143 ModuleDef::TypeAlias(ty) => {
144 get_symbol_fragment(db, &FieldOrAssocItem::AssocItem(AssocItem::TypeAlias(ty)))
145 }
146 _ => None,
147 },
148 Definition::Field(field) => get_symbol_fragment(db, &FieldOrAssocItem::Field(field)),
149 _ => None,
150 };
151
152 get_doc_url(db, &krate)
153 .and_then(|url| url.join(&base).ok())
154 .and_then(|url| filename.as_deref().and_then(|f| url.join(f).ok()))
155 .and_then(
156 |url| if let Some(fragment) = fragment { url.join(&fragment).ok() } else { Some(url) },
157 )
158 .map(|url| url.into_string())
159}
160
83fn rewrite_intra_doc_link( 161fn rewrite_intra_doc_link(
84 db: &RootDatabase, 162 db: &RootDatabase,
85 def: Definition, 163 def: Definition,
@@ -138,7 +216,29 @@ fn rewrite_url_link(db: &RootDatabase, def: ModuleDef, target: &str) -> Option<S
138 .map(|url| url.into_string()) 216 .map(|url| url.into_string())
139} 217}
140 218
141// Rewrites a markdown document, resolving links using `callback` and additionally striping prefixes/suffixes on link titles. 219/// Retrieve a link to documentation for the given symbol.
220pub(crate) fn external_docs(
221 db: &RootDatabase,
222 position: &FilePosition,
223) -> Option<DocumentationLink> {
224 let sema = Semantics::new(db);
225 let file = sema.parse(position.file_id).syntax().clone();
226 let token = pick_best(file.token_at_offset(position.offset))?;
227 let token = sema.descend_into_macros(token);
228
229 let node = token.parent();
230 let definition = match_ast! {
231 match node {
232 ast::NameRef(name_ref) => classify_name_ref(&sema, &name_ref).map(|d| d.definition(sema.db)),
233 ast::Name(name) => classify_name(&sema, &name).map(|d| d.definition(sema.db)),
234 _ => None,
235 }
236 };
237
238 get_doc_link(db, definition?)
239}
240
241/// Rewrites a markdown document, applying 'callback' to each link.
142fn map_links<'e>( 242fn map_links<'e>(
143 events: impl Iterator<Item = Event<'e>>, 243 events: impl Iterator<Item = Event<'e>>,
144 callback: impl Fn(&str, &str) -> (String, String), 244 callback: impl Fn(&str, &str) -> (String, String),
@@ -239,6 +339,12 @@ fn ns_from_intra_spec(s: &str) -> Option<hir::Namespace> {
239 .next() 339 .next()
240} 340}
241 341
342/// Get the root URL for the documentation of a crate.
343///
344/// ```
345/// https://doc.rust-lang.org/std/iter/trait.Iterator.html#tymethod.next
346/// ^^^^^^^^^^^^^^^^^^^^^^^^^^
347/// ```
242fn get_doc_url(db: &RootDatabase, krate: &Crate) -> Option<Url> { 348fn get_doc_url(db: &RootDatabase, krate: &Crate) -> Option<Url> {
243 krate 349 krate
244 .get_html_root_url(db) 350 .get_html_root_url(db)
@@ -255,8 +361,11 @@ fn get_doc_url(db: &RootDatabase, krate: &Crate) -> Option<Url> {
255 361
256/// Get the filename and extension generated for a symbol by rustdoc. 362/// Get the filename and extension generated for a symbol by rustdoc.
257/// 363///
258/// Example: `struct.Shard.html` 364/// ```
259fn get_symbol_filename(db: &RootDatabase, definition: &ModuleDef) -> Option<String> { 365/// https://doc.rust-lang.org/std/iter/trait.Iterator.html#tymethod.next
366/// ^^^^^^^^^^^^^^^^^^^
367/// ```
368fn get_symbol_filename(db: &dyn HirDatabase, definition: &ModuleDef) -> Option<String> {
260 Some(match definition { 369 Some(match definition {
261 ModuleDef::Adt(adt) => match adt { 370 ModuleDef::Adt(adt) => match adt {
262 Adt::Struct(s) => format!("struct.{}.html", s.name(db)), 371 Adt::Struct(s) => format!("struct.{}.html", s.name(db)),
@@ -266,7 +375,7 @@ fn get_symbol_filename(db: &RootDatabase, definition: &ModuleDef) -> Option<Stri
266 ModuleDef::Module(_) => "index.html".to_string(), 375 ModuleDef::Module(_) => "index.html".to_string(),
267 ModuleDef::Trait(t) => format!("trait.{}.html", t.name(db)), 376 ModuleDef::Trait(t) => format!("trait.{}.html", t.name(db)),
268 ModuleDef::TypeAlias(t) => format!("type.{}.html", t.name(db)), 377 ModuleDef::TypeAlias(t) => format!("type.{}.html", t.name(db)),
269 ModuleDef::BuiltinType(t) => format!("primitive.{}.html", t), 378 ModuleDef::BuiltinType(t) => format!("primitive.{}.html", t.as_name()),
270 ModuleDef::Function(f) => format!("fn.{}.html", f.name(db)), 379 ModuleDef::Function(f) => format!("fn.{}.html", f.name(db)),
271 ModuleDef::EnumVariant(ev) => { 380 ModuleDef::EnumVariant(ev) => {
272 format!("enum.{}.html#variant.{}", ev.parent_enum(db).name(db), ev.name(db)) 381 format!("enum.{}.html#variant.{}", ev.parent_enum(db).name(db), ev.name(db))
@@ -275,3 +384,163 @@ fn get_symbol_filename(db: &RootDatabase, definition: &ModuleDef) -> Option<Stri
275 ModuleDef::Static(s) => format!("static.{}.html", s.name(db)?), 384 ModuleDef::Static(s) => format!("static.{}.html", s.name(db)?),
276 }) 385 })
277} 386}
387
388enum FieldOrAssocItem {
389 Field(Field),
390 AssocItem(AssocItem),
391}
392
393/// Get the fragment required to link to a specific field, method, associated type, or associated constant.
394///
395/// ```
396/// https://doc.rust-lang.org/std/iter/trait.Iterator.html#tymethod.next
397/// ^^^^^^^^^^^^^^
398/// ```
399fn get_symbol_fragment(db: &dyn HirDatabase, field_or_assoc: &FieldOrAssocItem) -> Option<String> {
400 Some(match field_or_assoc {
401 FieldOrAssocItem::Field(field) => format!("#structfield.{}", field.name(db)),
402 FieldOrAssocItem::AssocItem(assoc) => match assoc {
403 AssocItem::Function(function) => {
404 let is_trait_method = matches!(
405 function.as_assoc_item(db).map(|assoc| assoc.container(db)),
406 Some(AssocItemContainer::Trait(..))
407 );
408 // This distinction may get more complicated when specialisation is available.
409 // Rustdoc makes this decision based on whether a method 'has defaultness'.
410 // Currently this is only the case for provided trait methods.
411 if is_trait_method && !function.has_body(db) {
412 format!("#tymethod.{}", function.name(db))
413 } else {
414 format!("#method.{}", function.name(db))
415 }
416 }
417 AssocItem::Const(constant) => format!("#associatedconstant.{}", constant.name(db)?),
418 AssocItem::TypeAlias(ty) => format!("#associatedtype.{}", ty.name(db)),
419 },
420 })
421}
422
423fn pick_best(tokens: TokenAtOffset<SyntaxToken>) -> Option<SyntaxToken> {
424 return tokens.max_by_key(priority);
425 fn priority(n: &SyntaxToken) -> usize {
426 match n.kind() {
427 IDENT | INT_NUMBER => 3,
428 T!['('] | T![')'] => 2,
429 kind if kind.is_trivia() => 0,
430 _ => 1,
431 }
432 }
433}
434
435#[cfg(test)]
436mod tests {
437 use expect_test::{expect, Expect};
438
439 use crate::fixture;
440
441 fn check(ra_fixture: &str, expect: Expect) {
442 let (analysis, position) = fixture::position(ra_fixture);
443 let url = analysis.external_docs(position).unwrap().expect("could not find url for symbol");
444
445 expect.assert_eq(&url)
446 }
447
448 #[test]
449 fn test_doc_url_struct() {
450 check(
451 r#"
452pub struct Fo<|>o;
453"#,
454 expect![[r#"https://docs.rs/test/*/test/struct.Foo.html"#]],
455 );
456 }
457
458 #[test]
459 fn test_doc_url_fn() {
460 check(
461 r#"
462pub fn fo<|>o() {}
463"#,
464 expect![[r##"https://docs.rs/test/*/test/fn.foo.html#method.foo"##]],
465 );
466 }
467
468 #[test]
469 fn test_doc_url_inherent_method() {
470 check(
471 r#"
472pub struct Foo;
473
474impl Foo {
475 pub fn met<|>hod() {}
476}
477
478"#,
479 expect![[r##"https://docs.rs/test/*/test/struct.Foo.html#method.method"##]],
480 );
481 }
482
483 #[test]
484 fn test_doc_url_trait_provided_method() {
485 check(
486 r#"
487pub trait Bar {
488 fn met<|>hod() {}
489}
490
491"#,
492 expect![[r##"https://docs.rs/test/*/test/trait.Bar.html#method.method"##]],
493 );
494 }
495
496 #[test]
497 fn test_doc_url_trait_required_method() {
498 check(
499 r#"
500pub trait Foo {
501 fn met<|>hod();
502}
503
504"#,
505 expect![[r##"https://docs.rs/test/*/test/trait.Foo.html#tymethod.method"##]],
506 );
507 }
508
509 #[test]
510 fn test_doc_url_field() {
511 check(
512 r#"
513pub struct Foo {
514 pub fie<|>ld: ()
515}
516
517"#,
518 expect![[r##"https://docs.rs/test/*/test/struct.Foo.html#structfield.field"##]],
519 );
520 }
521
522 // FIXME: ImportMap will return re-export paths instead of public module
523 // paths. The correct path to documentation will never be a re-export.
524 // This problem stops us from resolving stdlib items included in the prelude
525 // such as `Option::Some` correctly.
526 #[ignore = "ImportMap may return re-exports"]
527 #[test]
528 fn test_reexport_order() {
529 check(
530 r#"
531pub mod wrapper {
532 pub use module::Item;
533
534 pub mod module {
535 pub struct Item;
536 }
537}
538
539fn foo() {
540 let bar: wrapper::It<|>em;
541}
542 "#,
543 expect![[r#"https://docs.rs/test/*/test/wrapper/module/struct.Item.html"#]],
544 )
545 }
546}
diff --git a/crates/ide/src/hover.rs b/crates/ide/src/hover.rs
index 53265488e..6290b35bd 100644
--- a/crates/ide/src/hover.rs
+++ b/crates/ide/src/hover.rs
@@ -14,7 +14,7 @@ use test_utils::mark;
14 14
15use crate::{ 15use crate::{
16 display::{macro_label, ShortLabel, ToNav, TryToNav}, 16 display::{macro_label, ShortLabel, ToNav, TryToNav},
17 link_rewrite::{remove_links, rewrite_links}, 17 doc_links::{remove_links, rewrite_links},
18 markdown_remove::remove_markdown, 18 markdown_remove::remove_markdown,
19 markup::Markup, 19 markup::Markup,
20 runnables::runnable, 20 runnables::runnable,
diff --git a/crates/ide/src/lib.rs b/crates/ide/src/lib.rs
index 57f3581b6..686cee3a1 100644
--- a/crates/ide/src/lib.rs
+++ b/crates/ide/src/lib.rs
@@ -45,8 +45,8 @@ mod status;
45mod syntax_highlighting; 45mod syntax_highlighting;
46mod syntax_tree; 46mod syntax_tree;
47mod typing; 47mod typing;
48mod link_rewrite;
49mod markdown_remove; 48mod markdown_remove;
49mod doc_links;
50 50
51use std::sync::Arc; 51use std::sync::Arc;
52 52
@@ -77,7 +77,9 @@ pub use crate::{
77 hover::{HoverAction, HoverConfig, HoverGotoTypeData, HoverResult}, 77 hover::{HoverAction, HoverConfig, HoverGotoTypeData, HoverResult},
78 inlay_hints::{InlayHint, InlayHintsConfig, InlayKind}, 78 inlay_hints::{InlayHint, InlayHintsConfig, InlayKind},
79 markup::Markup, 79 markup::Markup,
80 references::{Declaration, Reference, ReferenceAccess, ReferenceKind, ReferenceSearchResult}, 80 references::{
81 Declaration, Reference, ReferenceAccess, ReferenceKind, ReferenceSearchResult, RenameError,
82 },
81 runnables::{Runnable, RunnableKind, TestId}, 83 runnables::{Runnable, RunnableKind, TestId},
82 syntax_highlighting::{ 84 syntax_highlighting::{
83 Highlight, HighlightModifier, HighlightModifiers, HighlightTag, HighlightedRange, 85 Highlight, HighlightModifier, HighlightModifiers, HighlightTag, HighlightedRange,
@@ -382,6 +384,14 @@ impl Analysis {
382 self.with_db(|db| hover::hover(db, position, links_in_hover, markdown)) 384 self.with_db(|db| hover::hover(db, position, links_in_hover, markdown))
383 } 385 }
384 386
387 /// Return URL(s) for the documentation of the symbol under the cursor.
388 pub fn external_docs(
389 &self,
390 position: FilePosition,
391 ) -> Cancelable<Option<doc_links::DocumentationLink>> {
392 self.with_db(|db| doc_links::external_docs(db, &position))
393 }
394
385 /// Computes parameter information for the given call expression. 395 /// Computes parameter information for the given call expression.
386 pub fn call_info(&self, position: FilePosition) -> Cancelable<Option<CallInfo>> { 396 pub fn call_info(&self, position: FilePosition) -> Cancelable<Option<CallInfo>> {
387 self.with_db(|db| call_info::call_info(db, position)) 397 self.with_db(|db| call_info::call_info(db, position))
@@ -490,7 +500,7 @@ impl Analysis {
490 &self, 500 &self,
491 position: FilePosition, 501 position: FilePosition,
492 new_name: &str, 502 new_name: &str,
493 ) -> Cancelable<Option<RangeInfo<SourceChange>>> { 503 ) -> Cancelable<Result<RangeInfo<SourceChange>, RenameError>> {
494 self.with_db(|db| references::rename(db, position, new_name)) 504 self.with_db(|db| references::rename(db, position, new_name))
495 } 505 }
496 506
diff --git a/crates/ide/src/references.rs b/crates/ide/src/references.rs
index 571dd5452..f65a05ea3 100644
--- a/crates/ide/src/references.rs
+++ b/crates/ide/src/references.rs
@@ -26,6 +26,7 @@ use syntax::{
26use crate::{display::TryToNav, FilePosition, FileRange, NavigationTarget, RangeInfo}; 26use crate::{display::TryToNav, FilePosition, FileRange, NavigationTarget, RangeInfo};
27 27
28pub(crate) use self::rename::rename; 28pub(crate) use self::rename::rename;
29pub use self::rename::RenameError;
29 30
30pub use ide_db::search::{Reference, ReferenceAccess, ReferenceKind}; 31pub use ide_db::search::{Reference, ReferenceAccess, ReferenceKind};
31 32
@@ -732,6 +733,30 @@ fn f(e: En) {
732 ); 733 );
733 } 734 }
734 735
736 #[test]
737 fn test_find_all_refs_enum_var_privacy() {
738 check(
739 r#"
740mod m {
741 pub enum En {
742 Variant {
743 field<|>: u8,
744 }
745 }
746}
747
748fn f() -> m::En {
749 m::En::Variant { field: 0 }
750}
751"#,
752 expect![[r#"
753 field RECORD_FIELD FileId(0) 56..65 56..61 Other
754
755 FileId(0) 125..130 Other Read
756 "#]],
757 );
758 }
759
735 fn check(ra_fixture: &str, expect: Expect) { 760 fn check(ra_fixture: &str, expect: Expect) {
736 check_with_scope(ra_fixture, None, expect) 761 check_with_scope(ra_fixture, None, expect)
737 } 762 }
diff --git a/crates/ide/src/references/rename.rs b/crates/ide/src/references/rename.rs
index 8cbe1ae5a..f3b5cfc8c 100644
--- a/crates/ide/src/references/rename.rs
+++ b/crates/ide/src/references/rename.rs
@@ -6,11 +6,16 @@ use ide_db::{
6 defs::{classify_name, classify_name_ref, Definition, NameClass, NameRefClass}, 6 defs::{classify_name, classify_name_ref, Definition, NameClass, NameRefClass},
7 RootDatabase, 7 RootDatabase,
8}; 8};
9use std::convert::TryInto; 9
10use std::{
11 convert::TryInto,
12 error::Error,
13 fmt::{self, Display},
14};
10use syntax::{ 15use syntax::{
11 algo::find_node_at_offset, 16 algo::find_node_at_offset,
12 ast::{self, NameOwner}, 17 ast::{self, NameOwner},
13 lex_single_valid_syntax_kind, match_ast, AstNode, SyntaxKind, SyntaxNode, SyntaxToken, 18 lex_single_syntax_kind, match_ast, AstNode, SyntaxKind, SyntaxNode, SyntaxToken,
14}; 19};
15use test_utils::mark; 20use test_utils::mark;
16use text_edit::TextEdit; 21use text_edit::TextEdit;
@@ -20,17 +25,37 @@ use crate::{
20 SourceChange, SourceFileEdit, TextRange, TextSize, 25 SourceChange, SourceFileEdit, TextRange, TextSize,
21}; 26};
22 27
28#[derive(Debug)]
29pub struct RenameError(pub(crate) String);
30
31impl fmt::Display for RenameError {
32 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
33 Display::fmt(&self.0, f)
34 }
35}
36
37impl Error for RenameError {}
38
23pub(crate) fn rename( 39pub(crate) fn rename(
24 db: &RootDatabase, 40 db: &RootDatabase,
25 position: FilePosition, 41 position: FilePosition,
26 new_name: &str, 42 new_name: &str,
27) -> Option<RangeInfo<SourceChange>> { 43) -> Result<RangeInfo<SourceChange>, RenameError> {
28 let sema = Semantics::new(db); 44 let sema = Semantics::new(db);
29 45
30 match lex_single_valid_syntax_kind(new_name)? { 46 match lex_single_syntax_kind(new_name) {
31 SyntaxKind::IDENT | SyntaxKind::UNDERSCORE => (), 47 Some(res) => match res {
32 SyntaxKind::SELF_KW => return rename_to_self(&sema, position), 48 (SyntaxKind::IDENT, _) => (),
33 _ => return None, 49 (SyntaxKind::UNDERSCORE, _) => (),
50 (SyntaxKind::SELF_KW, _) => return rename_to_self(&sema, position),
51 (_, Some(syntax_error)) => {
52 return Err(RenameError(format!("Invalid name `{}`: {}", new_name, syntax_error)))
53 }
54 (_, None) => {
55 return Err(RenameError(format!("Invalid name `{}`: not an identifier", new_name)))
56 }
57 },
58 None => return Err(RenameError(format!("Invalid name `{}`: not an identifier", new_name))),
34 } 59 }
35 60
36 let source_file = sema.parse(position.file_id); 61 let source_file = sema.parse(position.file_id);
@@ -103,7 +128,7 @@ fn rename_mod(
103 position: FilePosition, 128 position: FilePosition,
104 module: Module, 129 module: Module,
105 new_name: &str, 130 new_name: &str,
106) -> Option<RangeInfo<SourceChange>> { 131) -> Result<RangeInfo<SourceChange>, RenameError> {
107 let mut source_file_edits = Vec::new(); 132 let mut source_file_edits = Vec::new();
108 let mut file_system_edits = Vec::new(); 133 let mut file_system_edits = Vec::new();
109 134
@@ -125,7 +150,7 @@ fn rename_mod(
125 150
126 if let Some(src) = module.declaration_source(sema.db) { 151 if let Some(src) = module.declaration_source(sema.db) {
127 let file_id = src.file_id.original_file(sema.db); 152 let file_id = src.file_id.original_file(sema.db);
128 let name = src.value.name()?; 153 let name = src.value.name().unwrap();
129 let edit = SourceFileEdit { 154 let edit = SourceFileEdit {
130 file_id, 155 file_id,
131 edit: TextEdit::replace(name.syntax().text_range(), new_name.into()), 156 edit: TextEdit::replace(name.syntax().text_range(), new_name.into()),
@@ -133,35 +158,40 @@ fn rename_mod(
133 source_file_edits.push(edit); 158 source_file_edits.push(edit);
134 } 159 }
135 160
136 let RangeInfo { range, info: refs } = find_all_refs(sema, position, None)?; 161 let RangeInfo { range, info: refs } = find_all_refs(sema, position, None)
162 .ok_or_else(|| RenameError("No references found at position".to_string()))?;
137 let ref_edits = refs 163 let ref_edits = refs
138 .references 164 .references
139 .into_iter() 165 .into_iter()
140 .map(|reference| source_edit_from_reference(reference, new_name)); 166 .map(|reference| source_edit_from_reference(reference, new_name));
141 source_file_edits.extend(ref_edits); 167 source_file_edits.extend(ref_edits);
142 168
143 Some(RangeInfo::new(range, SourceChange::from_edits(source_file_edits, file_system_edits))) 169 Ok(RangeInfo::new(range, SourceChange::from_edits(source_file_edits, file_system_edits)))
144} 170}
145 171
146fn rename_to_self( 172fn rename_to_self(
147 sema: &Semantics<RootDatabase>, 173 sema: &Semantics<RootDatabase>,
148 position: FilePosition, 174 position: FilePosition,
149) -> Option<RangeInfo<SourceChange>> { 175) -> Result<RangeInfo<SourceChange>, RenameError> {
150 let source_file = sema.parse(position.file_id); 176 let source_file = sema.parse(position.file_id);
151 let syn = source_file.syntax(); 177 let syn = source_file.syntax();
152 178
153 let fn_def = find_node_at_offset::<ast::Fn>(syn, position.offset)?; 179 let fn_def = find_node_at_offset::<ast::Fn>(syn, position.offset)
154 let params = fn_def.param_list()?; 180 .ok_or_else(|| RenameError("No surrounding method declaration found".to_string()))?;
181 let params =
182 fn_def.param_list().ok_or_else(|| RenameError("Method has no parameters".to_string()))?;
155 if params.self_param().is_some() { 183 if params.self_param().is_some() {
156 return None; // method already has self param 184 return Err(RenameError("Method already has a self parameter".to_string()));
157 } 185 }
158 let first_param = params.params().next()?; 186 let first_param =
187 params.params().next().ok_or_else(|| RenameError("Method has no parameters".into()))?;
159 let mutable = match first_param.ty() { 188 let mutable = match first_param.ty() {
160 Some(ast::Type::RefType(rt)) => rt.mut_token().is_some(), 189 Some(ast::Type::RefType(rt)) => rt.mut_token().is_some(),
161 _ => return None, // not renaming other types 190 _ => return Err(RenameError("Not renaming other types".to_string())),
162 }; 191 };
163 192
164 let RangeInfo { range, info: refs } = find_all_refs(sema, position, None)?; 193 let RangeInfo { range, info: refs } = find_all_refs(sema, position, None)
194 .ok_or_else(|| RenameError("No reference found at position".to_string()))?;
165 195
166 let param_range = first_param.syntax().text_range(); 196 let param_range = first_param.syntax().text_range();
167 let (param_ref, usages): (Vec<Reference>, Vec<Reference>) = refs 197 let (param_ref, usages): (Vec<Reference>, Vec<Reference>) = refs
@@ -169,7 +199,7 @@ fn rename_to_self(
169 .partition(|reference| param_range.intersect(reference.file_range.range).is_some()); 199 .partition(|reference| param_range.intersect(reference.file_range.range).is_some());
170 200
171 if param_ref.is_empty() { 201 if param_ref.is_empty() {
172 return None; 202 return Err(RenameError("Parameter to rename not found".to_string()));
173 } 203 }
174 204
175 let mut edits = usages 205 let mut edits = usages
@@ -185,7 +215,7 @@ fn rename_to_self(
185 ), 215 ),
186 }); 216 });
187 217
188 Some(RangeInfo::new(range, SourceChange::from(edits))) 218 Ok(RangeInfo::new(range, SourceChange::from(edits)))
189} 219}
190 220
191fn text_edit_from_self_param( 221fn text_edit_from_self_param(
@@ -216,12 +246,13 @@ fn rename_self_to_param(
216 position: FilePosition, 246 position: FilePosition,
217 self_token: SyntaxToken, 247 self_token: SyntaxToken,
218 new_name: &str, 248 new_name: &str,
219) -> Option<RangeInfo<SourceChange>> { 249) -> Result<RangeInfo<SourceChange>, RenameError> {
220 let source_file = sema.parse(position.file_id); 250 let source_file = sema.parse(position.file_id);
221 let syn = source_file.syntax(); 251 let syn = source_file.syntax();
222 252
223 let text = sema.db.file_text(position.file_id); 253 let text = sema.db.file_text(position.file_id);
224 let fn_def = find_node_at_offset::<ast::Fn>(syn, position.offset)?; 254 let fn_def = find_node_at_offset::<ast::Fn>(syn, position.offset)
255 .ok_or_else(|| RenameError("No surrounding method declaration found".to_string()))?;
225 let search_range = fn_def.syntax().text_range(); 256 let search_range = fn_def.syntax().text_range();
226 257
227 let mut edits: Vec<SourceFileEdit> = vec![]; 258 let mut edits: Vec<SourceFileEdit> = vec![];
@@ -235,7 +266,8 @@ fn rename_self_to_param(
235 syn.token_at_offset(offset).find(|t| t.kind() == SyntaxKind::SELF_KW) 266 syn.token_at_offset(offset).find(|t| t.kind() == SyntaxKind::SELF_KW)
236 { 267 {
237 let edit = if let Some(ref self_param) = ast::SelfParam::cast(usage.parent()) { 268 let edit = if let Some(ref self_param) = ast::SelfParam::cast(usage.parent()) {
238 text_edit_from_self_param(syn, self_param, new_name)? 269 text_edit_from_self_param(syn, self_param, new_name)
270 .ok_or_else(|| RenameError("No target type found".to_string()))?
239 } else { 271 } else {
240 TextEdit::replace(usage.text_range(), String::from(new_name)) 272 TextEdit::replace(usage.text_range(), String::from(new_name))
241 }; 273 };
@@ -246,15 +278,18 @@ fn rename_self_to_param(
246 let range = ast::SelfParam::cast(self_token.parent()) 278 let range = ast::SelfParam::cast(self_token.parent())
247 .map_or(self_token.text_range(), |p| p.syntax().text_range()); 279 .map_or(self_token.text_range(), |p| p.syntax().text_range());
248 280
249 Some(RangeInfo::new(range, SourceChange::from(edits))) 281 Ok(RangeInfo::new(range, SourceChange::from(edits)))
250} 282}
251 283
252fn rename_reference( 284fn rename_reference(
253 sema: &Semantics<RootDatabase>, 285 sema: &Semantics<RootDatabase>,
254 position: FilePosition, 286 position: FilePosition,
255 new_name: &str, 287 new_name: &str,
256) -> Option<RangeInfo<SourceChange>> { 288) -> Result<RangeInfo<SourceChange>, RenameError> {
257 let RangeInfo { range, info: refs } = find_all_refs(sema, position, None)?; 289 let RangeInfo { range, info: refs } = match find_all_refs(sema, position, None) {
290 Some(range_info) => range_info,
291 None => return Err(RenameError("No references found at position".to_string())),
292 };
258 293
259 let edit = refs 294 let edit = refs
260 .into_iter() 295 .into_iter()
@@ -262,10 +297,10 @@ fn rename_reference(
262 .collect::<Vec<_>>(); 297 .collect::<Vec<_>>();
263 298
264 if edit.is_empty() { 299 if edit.is_empty() {
265 return None; 300 return Err(RenameError("No references found at position".to_string()));
266 } 301 }
267 302
268 Some(RangeInfo::new(range, SourceChange::from(edit))) 303 Ok(RangeInfo::new(range, SourceChange::from(edit)))
269} 304}
270 305
271#[cfg(test)] 306#[cfg(test)]
@@ -280,25 +315,45 @@ mod tests {
280 fn check(new_name: &str, ra_fixture_before: &str, ra_fixture_after: &str) { 315 fn check(new_name: &str, ra_fixture_before: &str, ra_fixture_after: &str) {
281 let ra_fixture_after = &trim_indent(ra_fixture_after); 316 let ra_fixture_after = &trim_indent(ra_fixture_after);
282 let (analysis, position) = fixture::position(ra_fixture_before); 317 let (analysis, position) = fixture::position(ra_fixture_before);
283 let source_change = analysis.rename(position, new_name).unwrap(); 318 let rename_result = analysis
284 let mut text_edit_builder = TextEdit::builder(); 319 .rename(position, new_name)
285 let mut file_id: Option<FileId> = None; 320 .unwrap_or_else(|err| panic!("Rename to '{}' was cancelled: {}", new_name, err));
286 if let Some(change) = source_change { 321 match rename_result {
287 for edit in change.info.source_file_edits { 322 Ok(source_change) => {
288 file_id = Some(edit.file_id); 323 let mut text_edit_builder = TextEdit::builder();
289 for indel in edit.edit.into_iter() { 324 let mut file_id: Option<FileId> = None;
290 text_edit_builder.replace(indel.delete, indel.insert); 325 for edit in source_change.info.source_file_edits {
326 file_id = Some(edit.file_id);
327 for indel in edit.edit.into_iter() {
328 text_edit_builder.replace(indel.delete, indel.insert);
329 }
291 } 330 }
331 let mut result = analysis.file_text(file_id.unwrap()).unwrap().to_string();
332 text_edit_builder.finish().apply(&mut result);
333 assert_eq_text!(ra_fixture_after, &*result);
292 } 334 }
293 } 335 Err(err) => {
294 let mut result = analysis.file_text(file_id.unwrap()).unwrap().to_string(); 336 if ra_fixture_after.starts_with("error:") {
295 text_edit_builder.finish().apply(&mut result); 337 let error_message = ra_fixture_after
296 assert_eq_text!(ra_fixture_after, &*result); 338 .chars()
339 .into_iter()
340 .skip("error:".len())
341 .collect::<String>();
342 assert_eq!(error_message.trim(), err.to_string());
343 return;
344 } else {
345 panic!("Rename to '{}' failed unexpectedly: {}", new_name, err)
346 }
347 }
348 };
297 } 349 }
298 350
299 fn check_expect(new_name: &str, ra_fixture: &str, expect: Expect) { 351 fn check_expect(new_name: &str, ra_fixture: &str, expect: Expect) {
300 let (analysis, position) = fixture::position(ra_fixture); 352 let (analysis, position) = fixture::position(ra_fixture);
301 let source_change = analysis.rename(position, new_name).unwrap().unwrap(); 353 let source_change = analysis
354 .rename(position, new_name)
355 .unwrap()
356 .expect("Expect returned RangeInfo to be Some, but was None");
302 expect.assert_debug_eq(&source_change) 357 expect.assert_debug_eq(&source_change)
303 } 358 }
304 359
@@ -313,11 +368,30 @@ mod tests {
313 } 368 }
314 369
315 #[test] 370 #[test]
316 fn test_rename_to_invalid_identifier() { 371 fn test_rename_to_invalid_identifier1() {
317 let (analysis, position) = fixture::position(r#"fn main() { let i<|> = 1; }"#); 372 check(
318 let new_name = "invalid!"; 373 "invalid!",
319 let source_change = analysis.rename(position, new_name).unwrap(); 374 r#"fn main() { let i<|> = 1; }"#,
320 assert!(source_change.is_none()); 375 "error: Invalid name `invalid!`: not an identifier",
376 );
377 }
378
379 #[test]
380 fn test_rename_to_invalid_identifier2() {
381 check(
382 "multiple tokens",
383 r#"fn main() { let i<|> = 1; }"#,
384 "error: Invalid name `multiple tokens`: not an identifier",
385 );
386 }
387
388 #[test]
389 fn test_rename_to_invalid_identifier3() {
390 check(
391 "let",
392 r#"fn main() { let i<|> = 1; }"#,
393 "error: Invalid name `let`: not an identifier",
394 );
321 } 395 }
322 396
323 #[test] 397 #[test]
@@ -350,6 +424,15 @@ fn main() {
350 } 424 }
351 425
352 #[test] 426 #[test]
427 fn test_rename_unresolved_reference() {
428 check(
429 "new_name",
430 r#"fn main() { let _ = unresolved_ref<|>; }"#,
431 "error: No references found at position",
432 );
433 }
434
435 #[test]
353 fn test_rename_for_macro_args() { 436 fn test_rename_for_macro_args() {
354 check( 437 check(
355 "b", 438 "b",
diff --git a/crates/ide/src/syntax_highlighting/test_data/highlighting.html b/crates/ide/src/syntax_highlighting/test_data/highlighting.html
index 4f47e1be9..0bb0928e4 100644
--- a/crates/ide/src/syntax_highlighting/test_data/highlighting.html
+++ b/crates/ide/src/syntax_highlighting/test_data/highlighting.html
@@ -62,7 +62,7 @@ pre { color: #DCDCCC; background: #3F3F3F; font-size: 22px; padd
62 62
63<span class="keyword">impl</span> <span class="struct">Foo</span> <span class="punctuation">{</span> 63<span class="keyword">impl</span> <span class="struct">Foo</span> <span class="punctuation">{</span>
64 <span class="keyword">fn</span> <span class="function declaration">baz</span><span class="punctuation">(</span><span class="keyword">mut</span> <span class="self_keyword mutable">self</span><span class="punctuation">,</span> <span class="value_param declaration">f</span><span class="punctuation">:</span> <span class="struct">Foo</span><span class="punctuation">)</span> <span class="operator">-&gt;</span> <span class="builtin_type">i32</span> <span class="punctuation">{</span> 64 <span class="keyword">fn</span> <span class="function declaration">baz</span><span class="punctuation">(</span><span class="keyword">mut</span> <span class="self_keyword mutable">self</span><span class="punctuation">,</span> <span class="value_param declaration">f</span><span class="punctuation">:</span> <span class="struct">Foo</span><span class="punctuation">)</span> <span class="operator">-&gt;</span> <span class="builtin_type">i32</span> <span class="punctuation">{</span>
65 <span class="value_param">f</span><span class="punctuation">.</span><span class="function consuming">baz</span><span class="punctuation">(</span><span class="self_keyword consuming">self</span><span class="punctuation">)</span> 65 <span class="value_param">f</span><span class="punctuation">.</span><span class="function consuming">baz</span><span class="punctuation">(</span><span class="self_keyword mutable consuming">self</span><span class="punctuation">)</span>
66 <span class="punctuation">}</span> 66 <span class="punctuation">}</span>
67 67
68 <span class="keyword">fn</span> <span class="function declaration">qux</span><span class="punctuation">(</span><span class="operator">&</span><span class="keyword">mut</span> <span class="self_keyword mutable">self</span><span class="punctuation">)</span> <span class="punctuation">{</span> 68 <span class="keyword">fn</span> <span class="function declaration">qux</span><span class="punctuation">(</span><span class="operator">&</span><span class="keyword">mut</span> <span class="self_keyword mutable">self</span><span class="punctuation">)</span> <span class="punctuation">{</span>