feat(lsp): Implement textDocument/rename (#8910)

This commit is contained in:
hrsh7th 2020-12-30 09:58:20 +09:00 committed by GitHub
parent d5f3a749eb
commit 57b0562957
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 275 additions and 2 deletions

View file

@ -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,

View file

@ -802,6 +802,78 @@ impl lspower::LanguageServer for LanguageServer {
}
}
async fn rename(
&self,
params: RenameParams,
) -> LSPResult<Option<WorkspaceEdit>> {
if !self.enabled() {
return Ok(None);
}
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,
|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),
}
}
async fn request_else(
&self,
method: &str,
@ -1143,4 +1215,57 @@ mod tests {
]);
harness.run().await;
}
#[tokio::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;
}
}

View file

@ -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;
@ -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")]
@ -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 {
@ -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
})
}
}
}
}

View file

@ -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);"
}
}
}

View file

@ -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"
}
}

View file

@ -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

12
cli/tsc/compiler.d.ts vendored
View file

@ -50,7 +50,8 @@ declare global {
| GetDocumentHighlightsRequest
| GetReferencesRequest
| GetDefinitionRequest
| GetCompletionsRequest;
| GetCompletionsRequest
| FindRenameLocationsRequest;
interface BaseLanguageServerRequest {
id: number;
@ -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;
}
}