mirror of
https://github.com/denoland/deno.git
synced 2025-03-03 09:31:22 -05:00
feat(lsp): Implement textDocument/rename (#8910)
This commit is contained in:
parent
d5f3a749eb
commit
57b0562957
7 changed files with 275 additions and 2 deletions
|
@ -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,
|
||||
|
|
|
@ -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),
|
||||
¶ms.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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
12
cli/tests/lsp/rename_did_open_notification.json
Normal file
12
cli/tests/lsp/rename_did_open_notification.json
Normal 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);"
|
||||
}
|
||||
}
|
||||
}
|
15
cli/tests/lsp/rename_request.json
Normal file
15
cli/tests/lsp/rename_request.json
Normal 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"
|
||||
}
|
||||
}
|
|
@ -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
12
cli/tsc/compiler.d.ts
vendored
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue