Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(lsp): Implement textDocument/rename #8910

Merged
merged 1 commit into from
Dec 30, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cli/lsp/capabilities.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ pub fn server_capabilities(
document_on_type_formatting_provider: None,
selection_range_provider: None,
folding_range_provider: None,
rename_provider: None,
rename_provider: Some(OneOf::Left(true)),
document_link_provider: None,
color_provider: None,
execute_command_provider: None,
Expand Down
125 changes: 125 additions & 0 deletions cli/lsp/language_server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -802,6 +802,78 @@ impl lspower::LanguageServer for LanguageServer {
}
}

async fn rename(
&self,
params: RenameParams,
) -> LSPResult<Option<WorkspaceEdit>> {
if !self.enabled() {
return Ok(None);
}
lucacasonato marked this conversation as resolved.
Show resolved Hide resolved

let snapshot = self.snapshot();
let specifier =
utils::normalize_url(params.text_document_position.text_document.uri);

let line_index =
self
.get_line_index(specifier.clone())
.await
.map_err(|err| {
error!("Failed to get line_index {:#?}", err);
LSPError::internal_error()
})?;

let req = tsc::RequestMethod::FindRenameLocations((
specifier,
text::to_char_pos(&line_index, params.text_document_position.position),
true,
true,
false,
));

let res = self
.ts_server
.request(snapshot.clone(), req)
.await
.map_err(|err| {
error!("Failed to request to tsserver {:#?}", err);
LSPError::invalid_request()
})?;

let maybe_locations = serde_json::from_value::<
Option<Vec<tsc::RenameLocation>>,
>(res)
.map_err(|err| {
error!(
"Failed to deserialize tsserver response to Vec<RenameLocation> {:#?}",
err
);
LSPError::internal_error()
})?;

match maybe_locations {
Some(locations) => {
let rename_locations = tsc::RenameLocations { locations };
let workpace_edits = rename_locations
.into_workspace_edit(
snapshot,
lucacasonato marked this conversation as resolved.
Show resolved Hide resolved
|s| self.get_line_index(s),
&params.new_name,
)
.await
.map_err(|err| {
error!(
"Failed to convert tsc::RenameLocations to WorkspaceEdit {:#?}",
err
);
LSPError::internal_error()
})?;
Ok(Some(workpace_edits))
}
None => Ok(None),
lucacasonato marked this conversation as resolved.
Show resolved Hide resolved
}
}

async fn request_else(
&self,
method: &str,
Expand Down Expand Up @@ -1143,4 +1215,57 @@ mod tests {
]);
harness.run().await;
}
#[tokio::test]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great test 👍

async fn test_rename() {
let mut harness = LspTestHarness::new(vec![
("initialize_request.json", LspResponse::RequestAny),
("initialized_notification.json", LspResponse::None),
("rename_did_open_notification.json", LspResponse::None),
(
"rename_request.json",
LspResponse::Request(
2,
json!({
"documentChanges": [{
"textDocument": {
"uri": "file:///a/file.ts",
"version": 1,
},
"edits": [{
"range": {
"start": {
"line": 0,
"character": 4
},
"end": {
"line": 0,
"character": 12
}
},
"newText": "variable_modified"
}, {
"range": {
"start": {
"line": 1,
"character": 12
},
"end": {
"line": 1,
"character": 20
}
},
"newText": "variable_modified"
}]
}]
}),
),
),
(
"shutdown_request.json",
LspResponse::Request(3, json!(null)),
),
("exit_notification.json", LspResponse::None),
]);
harness.run().await;
}
}
99 changes: 99 additions & 0 deletions cli/lsp/tsc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ use deno_core::serde::Serialize;
use deno_core::serde_json;
use deno_core::serde_json::json;
use deno_core::serde_json::Value;
use deno_core::url::Url;
use deno_core::JsRuntime;
use deno_core::ModuleSpecifier;
use deno_core::OpFn;
Expand Down Expand Up @@ -411,6 +412,85 @@ impl QuickInfo {
}
}

#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RenameLocation {
// inherit from DocumentSpan
text_span: TextSpan,
file_name: String,
original_text_span: Option<TextSpan>,
original_file_name: Option<String>,
context_span: Option<TextSpan>,
original_context_span: Option<TextSpan>,
// RenameLocation props
prefix_text: Option<String>,
suffix_text: Option<String>,
}

pub struct RenameLocations {
pub locations: Vec<RenameLocation>,
}

impl RenameLocations {
pub async fn into_workspace_edit<F, Fut>(
self,
snapshot: StateSnapshot,
index_provider: F,
new_name: &str,
) -> Result<lsp_types::WorkspaceEdit, AnyError>
where
F: Fn(ModuleSpecifier) -> Fut,
Fut: Future<Output = Result<Vec<u32>, AnyError>>,
{
let mut text_document_edit_map: HashMap<Url, lsp_types::TextDocumentEdit> =
HashMap::new();
for location in self.locations.iter() {
let uri = utils::normalize_file_name(&location.file_name)?;
let specifier = ModuleSpecifier::resolve_url(&location.file_name)?;

// ensure TextDocumentEdit for `location.file_name`.
if text_document_edit_map.get(&uri).is_none() {
text_document_edit_map.insert(
uri.clone(),
lsp_types::TextDocumentEdit {
text_document: lsp_types::OptionalVersionedTextDocumentIdentifier {
uri: uri.clone(),
version: snapshot
.doc_data
.get(&specifier)
.map_or_else(|| None, |data| data.version),
},
edits: Vec::<
lsp_types::OneOf<
lsp_types::TextEdit,
lsp_types::AnnotatedTextEdit,
>,
>::new(),
},
);
}

// push TextEdit for ensured `TextDocumentEdit.edits`.
let document_edit = text_document_edit_map.get_mut(&uri).unwrap();
document_edit
.edits
.push(lsp_types::OneOf::Left(lsp_types::TextEdit {
range: location
.text_span
.to_range(&index_provider(specifier.clone()).await?),
new_text: new_name.to_string(),
}));
}

Ok(lsp_types::WorkspaceEdit {
changes: None,
document_changes: Some(lsp_types::DocumentChanges::Edits(
text_document_edit_map.values().cloned().collect(),
)),
})
}
}

#[derive(Debug, Deserialize)]
pub enum HighlightSpanKind {
#[serde(rename = "none")]
Expand Down Expand Up @@ -1059,6 +1139,8 @@ pub enum RequestMethod {
GetDefinition((ModuleSpecifier, u32)),
/// Get completion information at a given position (IntelliSense).
GetCompletions((ModuleSpecifier, u32, UserPreferences)),
/// Get rename locations at a given position.
FindRenameLocations((ModuleSpecifier, u32, bool, bool, bool)),
}

impl RequestMethod {
Expand Down Expand Up @@ -1127,6 +1209,23 @@ impl RequestMethod {
"preferences": preferences,
})
}
RequestMethod::FindRenameLocations((
specifier,
position,
find_in_strings,
find_in_comments,
provide_prefix_and_suffix_text_for_rename,
)) => {
json!({
"id": id,
"method": "findRenameLocations",
"specifier": specifier,
"position": position,
"findInStrings": find_in_strings,
"findInComments": find_in_comments,
"providePrefixAndSuffixTextForRename": provide_prefix_and_suffix_text_for_rename
})
}
}
}
}
Expand Down
12 changes: 12 additions & 0 deletions cli/tests/lsp/rename_did_open_notification.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"jsonrpc": "2.0",
"method": "textDocument/didOpen",
"params": {
"textDocument": {
"uri": "file:///a/file.ts",
"languageId": "typescript",
"version": 1,
"text": "let variable = 'a';\nconsole.log(variable);"
}
}
}
15 changes: 15 additions & 0 deletions cli/tests/lsp/rename_request.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"jsonrpc": "2.0",
"id": 2,
"method": "textDocument/rename",
"params": {
"textDocument": {
"uri": "file:///a/file.ts"
},
"position": {
"line": 5,
"character": 19
},
"newName": "variable_modified"
}
}
12 changes: 12 additions & 0 deletions cli/tsc/99_main_compiler.js
Original file line number Diff line number Diff line change
Expand Up @@ -562,6 +562,18 @@ delete Object.prototype.__proto__;
),
);
}
case "findRenameLocations": {
return respond(
id,
languageService.findRenameLocations(
request.specifier,
request.position,
request.findInStrings,
request.findInComments,
request.providePrefixAndSuffixTextForRename,
),
);
}
default:
throw new TypeError(
// @ts-ignore exhausted case statement sets type to never
Expand Down
12 changes: 11 additions & 1 deletion cli/tsc/compiler.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,8 @@ declare global {
| GetDocumentHighlightsRequest
| GetReferencesRequest
| GetDefinitionRequest
| GetCompletionsRequest;
| GetCompletionsRequest
| FindRenameLocationsRequest;

interface BaseLanguageServerRequest {
id: number;
Expand Down Expand Up @@ -114,4 +115,13 @@ declare global {
position: number;
preferences: ts.UserPreferences;
}

interface FindRenameLocationsRequest extends BaseLanguageServerRequest {
method: "findRenameLocations";
specifier: string;
position: number;
findInStrings: boolean;
findInComments: boolean;
providePrefixAndSuffixTextForRename: boolean;
}
}