mirror of
https://github.com/denoland/deno.git
synced 2025-03-03 17:34:47 -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,
|
document_on_type_formatting_provider: None,
|
||||||
selection_range_provider: None,
|
selection_range_provider: None,
|
||||||
folding_range_provider: None,
|
folding_range_provider: None,
|
||||||
rename_provider: None,
|
rename_provider: Some(OneOf::Left(true)),
|
||||||
document_link_provider: None,
|
document_link_provider: None,
|
||||||
color_provider: None,
|
color_provider: None,
|
||||||
execute_command_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(
|
async fn request_else(
|
||||||
&self,
|
&self,
|
||||||
method: &str,
|
method: &str,
|
||||||
|
@ -1143,4 +1215,57 @@ mod tests {
|
||||||
]);
|
]);
|
||||||
harness.run().await;
|
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;
|
||||||
use deno_core::serde_json::json;
|
use deno_core::serde_json::json;
|
||||||
use deno_core::serde_json::Value;
|
use deno_core::serde_json::Value;
|
||||||
|
use deno_core::url::Url;
|
||||||
use deno_core::JsRuntime;
|
use deno_core::JsRuntime;
|
||||||
use deno_core::ModuleSpecifier;
|
use deno_core::ModuleSpecifier;
|
||||||
use deno_core::OpFn;
|
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)]
|
#[derive(Debug, Deserialize)]
|
||||||
pub enum HighlightSpanKind {
|
pub enum HighlightSpanKind {
|
||||||
#[serde(rename = "none")]
|
#[serde(rename = "none")]
|
||||||
|
@ -1059,6 +1139,8 @@ pub enum RequestMethod {
|
||||||
GetDefinition((ModuleSpecifier, u32)),
|
GetDefinition((ModuleSpecifier, u32)),
|
||||||
/// Get completion information at a given position (IntelliSense).
|
/// Get completion information at a given position (IntelliSense).
|
||||||
GetCompletions((ModuleSpecifier, u32, UserPreferences)),
|
GetCompletions((ModuleSpecifier, u32, UserPreferences)),
|
||||||
|
/// Get rename locations at a given position.
|
||||||
|
FindRenameLocations((ModuleSpecifier, u32, bool, bool, bool)),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RequestMethod {
|
impl RequestMethod {
|
||||||
|
@ -1127,6 +1209,23 @@ impl RequestMethod {
|
||||||
"preferences": preferences,
|
"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:
|
default:
|
||||||
throw new TypeError(
|
throw new TypeError(
|
||||||
// @ts-ignore exhausted case statement sets type to never
|
// @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
|
| GetDocumentHighlightsRequest
|
||||||
| GetReferencesRequest
|
| GetReferencesRequest
|
||||||
| GetDefinitionRequest
|
| GetDefinitionRequest
|
||||||
| GetCompletionsRequest;
|
| GetCompletionsRequest
|
||||||
|
| FindRenameLocationsRequest;
|
||||||
|
|
||||||
interface BaseLanguageServerRequest {
|
interface BaseLanguageServerRequest {
|
||||||
id: number;
|
id: number;
|
||||||
|
@ -114,4 +115,13 @@ declare global {
|
||||||
position: number;
|
position: number;
|
||||||
preferences: ts.UserPreferences;
|
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