mirror of
https://github.com/denoland/deno.git
synced 2024-12-27 17:49:08 -05:00
e0429e2ad6
1. Fixes a cosmetic issue in the repl where it would display lsp warning messages. 2. Lazily loads dependencies from the package.json on use. 3. Supports using bare specifiers from package.json in the REPL. Closes #17929 Closes #18494
315 lines
9.5 KiB
Rust
315 lines
9.5 KiB
Rust
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
|
|
|
|
use std::collections::HashMap;
|
|
|
|
use deno_ast::LineAndColumnIndex;
|
|
use deno_ast::ModuleSpecifier;
|
|
use deno_ast::SourceTextInfo;
|
|
use deno_core::anyhow::anyhow;
|
|
use deno_core::error::AnyError;
|
|
use deno_core::serde_json;
|
|
use tower_lsp::lsp_types::ClientCapabilities;
|
|
use tower_lsp::lsp_types::ClientInfo;
|
|
use tower_lsp::lsp_types::CompletionContext;
|
|
use tower_lsp::lsp_types::CompletionParams;
|
|
use tower_lsp::lsp_types::CompletionResponse;
|
|
use tower_lsp::lsp_types::CompletionTextEdit;
|
|
use tower_lsp::lsp_types::CompletionTriggerKind;
|
|
use tower_lsp::lsp_types::DidChangeTextDocumentParams;
|
|
use tower_lsp::lsp_types::DidCloseTextDocumentParams;
|
|
use tower_lsp::lsp_types::DidOpenTextDocumentParams;
|
|
use tower_lsp::lsp_types::InitializeParams;
|
|
use tower_lsp::lsp_types::InitializedParams;
|
|
use tower_lsp::lsp_types::PartialResultParams;
|
|
use tower_lsp::lsp_types::Position;
|
|
use tower_lsp::lsp_types::Range;
|
|
use tower_lsp::lsp_types::TextDocumentContentChangeEvent;
|
|
use tower_lsp::lsp_types::TextDocumentIdentifier;
|
|
use tower_lsp::lsp_types::TextDocumentItem;
|
|
use tower_lsp::lsp_types::TextDocumentPositionParams;
|
|
use tower_lsp::lsp_types::VersionedTextDocumentIdentifier;
|
|
use tower_lsp::lsp_types::WorkDoneProgressParams;
|
|
use tower_lsp::LanguageServer;
|
|
|
|
use super::client::Client;
|
|
use super::config::CompletionSettings;
|
|
use super::config::ImportCompletionSettings;
|
|
use super::config::TestingSettings;
|
|
use super::config::WorkspaceSettings;
|
|
|
|
#[derive(Debug)]
|
|
pub struct ReplCompletionItem {
|
|
pub new_text: String,
|
|
pub range: std::ops::Range<usize>,
|
|
}
|
|
|
|
pub struct ReplLanguageServer {
|
|
language_server: super::language_server::LanguageServer,
|
|
document_version: i32,
|
|
document_text: String,
|
|
pending_text: String,
|
|
cwd_uri: ModuleSpecifier,
|
|
}
|
|
|
|
impl ReplLanguageServer {
|
|
pub async fn new_initialized() -> Result<ReplLanguageServer, AnyError> {
|
|
// downgrade info and warn lsp logging to debug
|
|
super::logging::set_lsp_log_level(log::Level::Debug);
|
|
super::logging::set_lsp_warn_level(log::Level::Debug);
|
|
|
|
let language_server =
|
|
super::language_server::LanguageServer::new(Client::new_for_repl());
|
|
|
|
let cwd_uri = get_cwd_uri()?;
|
|
|
|
#[allow(deprecated)]
|
|
language_server
|
|
.initialize(InitializeParams {
|
|
process_id: None,
|
|
root_path: None,
|
|
root_uri: Some(cwd_uri.clone()),
|
|
initialization_options: Some(
|
|
serde_json::to_value(get_repl_workspace_settings()).unwrap(),
|
|
),
|
|
capabilities: ClientCapabilities {
|
|
workspace: None,
|
|
text_document: None,
|
|
window: None,
|
|
general: None,
|
|
experimental: None,
|
|
offset_encoding: None,
|
|
},
|
|
trace: None,
|
|
workspace_folders: None,
|
|
client_info: Some(ClientInfo {
|
|
name: "Deno REPL".to_string(),
|
|
version: None,
|
|
}),
|
|
locale: None,
|
|
})
|
|
.await?;
|
|
|
|
language_server.initialized(InitializedParams {}).await;
|
|
|
|
let server = ReplLanguageServer {
|
|
language_server,
|
|
document_version: 0,
|
|
document_text: String::new(),
|
|
pending_text: String::new(),
|
|
cwd_uri,
|
|
};
|
|
server.open_current_document().await;
|
|
|
|
Ok(server)
|
|
}
|
|
|
|
pub async fn commit_text(&mut self, line_text: &str) {
|
|
self.did_change(line_text).await;
|
|
self.document_text.push_str(&self.pending_text);
|
|
self.pending_text = String::new();
|
|
}
|
|
|
|
pub async fn completions(
|
|
&mut self,
|
|
line_text: &str,
|
|
position: usize,
|
|
) -> Vec<ReplCompletionItem> {
|
|
self.did_change(line_text).await;
|
|
let text_info = deno_ast::SourceTextInfo::from_string(format!(
|
|
"{}{}",
|
|
self.document_text, self.pending_text
|
|
));
|
|
let before_line_len = self.document_text.len();
|
|
let position = text_info.range().start + before_line_len + position;
|
|
let line_and_column = text_info.line_and_column_index(position);
|
|
let response = self
|
|
.language_server
|
|
.completion(CompletionParams {
|
|
text_document_position: TextDocumentPositionParams {
|
|
text_document: TextDocumentIdentifier {
|
|
uri: self.get_document_specifier(),
|
|
},
|
|
position: Position {
|
|
line: line_and_column.line_index as u32,
|
|
character: line_and_column.column_index as u32,
|
|
},
|
|
},
|
|
work_done_progress_params: WorkDoneProgressParams {
|
|
work_done_token: None,
|
|
},
|
|
partial_result_params: PartialResultParams {
|
|
partial_result_token: None,
|
|
},
|
|
context: Some(CompletionContext {
|
|
trigger_kind: CompletionTriggerKind::INVOKED,
|
|
trigger_character: None,
|
|
}),
|
|
})
|
|
.await
|
|
.ok()
|
|
.unwrap_or_default();
|
|
|
|
let mut items = match response {
|
|
Some(CompletionResponse::Array(items)) => items,
|
|
Some(CompletionResponse::List(list)) => list.items,
|
|
None => Vec::new(),
|
|
};
|
|
items.sort_by_key(|item| {
|
|
if let Some(sort_text) = &item.sort_text {
|
|
sort_text.clone()
|
|
} else {
|
|
item.label.clone()
|
|
}
|
|
});
|
|
items
|
|
.into_iter()
|
|
.filter_map(|item| {
|
|
item.text_edit.and_then(|edit| match edit {
|
|
CompletionTextEdit::Edit(edit) => Some(ReplCompletionItem {
|
|
new_text: edit.new_text,
|
|
range: lsp_range_to_std_range(&text_info, &edit.range),
|
|
}),
|
|
CompletionTextEdit::InsertAndReplace(_) => None,
|
|
})
|
|
})
|
|
.filter(|item| {
|
|
// filter the results to only exact matches
|
|
let text = &text_info.text_str()[item.range.clone()];
|
|
item.new_text.starts_with(text)
|
|
})
|
|
.map(|mut item| {
|
|
// convert back to a line position
|
|
item.range.start -= before_line_len;
|
|
item.range.end -= before_line_len;
|
|
item
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
async fn did_change(&mut self, new_text: &str) {
|
|
self.check_cwd_change().await;
|
|
let new_text = if new_text.ends_with('\n') {
|
|
new_text.to_string()
|
|
} else {
|
|
format!("{new_text}\n")
|
|
};
|
|
self.document_version += 1;
|
|
let current_line_count =
|
|
self.document_text.chars().filter(|c| *c == '\n').count() as u32;
|
|
let pending_line_count =
|
|
self.pending_text.chars().filter(|c| *c == '\n').count() as u32;
|
|
self
|
|
.language_server
|
|
.did_change(DidChangeTextDocumentParams {
|
|
text_document: VersionedTextDocumentIdentifier {
|
|
uri: self.get_document_specifier(),
|
|
version: self.document_version,
|
|
},
|
|
content_changes: vec![TextDocumentContentChangeEvent {
|
|
range: Some(Range {
|
|
start: Position::new(current_line_count, 0),
|
|
end: Position::new(current_line_count + pending_line_count, 0),
|
|
}),
|
|
range_length: None,
|
|
text: new_text.to_string(),
|
|
}],
|
|
})
|
|
.await;
|
|
self.pending_text = new_text;
|
|
}
|
|
|
|
async fn check_cwd_change(&mut self) {
|
|
// handle if the cwd changes, if the cwd is deleted in the case of
|
|
// get_cwd_uri() erroring, then keep using it as the base
|
|
let cwd_uri = get_cwd_uri().unwrap_or_else(|_| self.cwd_uri.clone());
|
|
if self.cwd_uri != cwd_uri {
|
|
self
|
|
.language_server
|
|
.did_close(DidCloseTextDocumentParams {
|
|
text_document: TextDocumentIdentifier {
|
|
uri: self.get_document_specifier(),
|
|
},
|
|
})
|
|
.await;
|
|
self.cwd_uri = cwd_uri;
|
|
self.document_version = 0;
|
|
self.open_current_document().await;
|
|
}
|
|
}
|
|
|
|
async fn open_current_document(&self) {
|
|
self
|
|
.language_server
|
|
.did_open(DidOpenTextDocumentParams {
|
|
text_document: TextDocumentItem {
|
|
uri: self.get_document_specifier(),
|
|
language_id: "typescript".to_string(),
|
|
version: self.document_version,
|
|
text: format!("{}{}", self.document_text, self.pending_text),
|
|
},
|
|
})
|
|
.await;
|
|
}
|
|
|
|
fn get_document_specifier(&self) -> ModuleSpecifier {
|
|
self.cwd_uri.join("$deno$repl.ts").unwrap()
|
|
}
|
|
}
|
|
|
|
fn lsp_range_to_std_range(
|
|
text_info: &SourceTextInfo,
|
|
range: &Range,
|
|
) -> std::ops::Range<usize> {
|
|
let start_index = text_info
|
|
.loc_to_source_pos(LineAndColumnIndex {
|
|
line_index: range.start.line as usize,
|
|
column_index: range.start.character as usize,
|
|
})
|
|
.as_byte_index(text_info.range().start);
|
|
let end_index = text_info
|
|
.loc_to_source_pos(LineAndColumnIndex {
|
|
line_index: range.end.line as usize,
|
|
column_index: range.end.character as usize,
|
|
})
|
|
.as_byte_index(text_info.range().start);
|
|
|
|
start_index..end_index
|
|
}
|
|
|
|
fn get_cwd_uri() -> Result<ModuleSpecifier, AnyError> {
|
|
let cwd = std::env::current_dir()?;
|
|
ModuleSpecifier::from_directory_path(&cwd)
|
|
.map_err(|_| anyhow!("Could not get URI from {}", cwd.display()))
|
|
}
|
|
|
|
pub fn get_repl_workspace_settings() -> WorkspaceSettings {
|
|
WorkspaceSettings {
|
|
enable: true,
|
|
enable_paths: Vec::new(),
|
|
config: None,
|
|
certificate_stores: None,
|
|
cache: None,
|
|
import_map: None,
|
|
code_lens: Default::default(),
|
|
inlay_hints: Default::default(),
|
|
internal_debug: false,
|
|
lint: false,
|
|
tls_certificate: None,
|
|
unsafely_ignore_certificate_errors: None,
|
|
unstable: false,
|
|
suggest: CompletionSettings {
|
|
complete_function_calls: false,
|
|
names: false,
|
|
paths: false,
|
|
auto_imports: false,
|
|
imports: ImportCompletionSettings {
|
|
auto_discover: false,
|
|
hosts: HashMap::from([("https://deno.land".to_string(), true)]),
|
|
},
|
|
},
|
|
testing: TestingSettings {
|
|
args: vec![],
|
|
enable: false,
|
|
},
|
|
}
|
|
}
|