1
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2024-12-22 07:14:47 -05:00

feat(lsp): registry auto discovery (#10813)

Closes: #10194
Fixes: #10468
This commit is contained in:
Kitson Kelly 2021-06-01 21:53:08 +10:00 committed by GitHub
parent 9abb899f5f
commit bb5bf91067
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 355 additions and 155 deletions

View file

@ -5,9 +5,8 @@ The Deno Language Server provides a server implementation of the
which is specifically tailored to provide a _Deno_ view of code. It is
integrated into the command line and can be started via the `lsp` sub-command.
> :warning: The Language Server is highly experimental and far from feature
> complete. This document gives an overview of the structure of the language
> server.
> :warning: The Language Server is experimental and not feature complete. This
> document gives an overview of the structure of the language server.
## Structure
@ -15,6 +14,60 @@ When the language server is started, a `LanguageServer` instance is created
which holds all of the state of the language server. It also defines all of the
methods that the client calls via the Language Server RPC protocol.
## Settings
There are several settings that the language server supports for a workspace:
- `deno.enable`
- `deno.config`
- `deno.importMap`
- `deno.codeLens.implementations`
- `deno.codeLens.references`
- `deno.codeLens.referencesAllFunctions`
- `deno.suggest.completeFunctionCalls`
- `deno.suggest.names`
- `deno.suggest.paths`
- `deno.suggest.autoImports`
- `deno.suggest.imports.autoDiscover`
- `deno.suggest.imports.hosts`
- `deno.lint`
- `deno.unstable`
There are settings that are support on a per resource basis by the language
server:
- `deno.enable`
There are several points in the process where Deno analyzes these settings.
First, when the `initialize` request from the client, the
`initializationOptions` will be assumed to be an object that represents the
`deno` namespace of options. For example, the following value:
```json
{
"enable": true,
"unstable": true
}
```
Would enable Deno with the unstable APIs for this instance of the language
server.
When the language server receives a `workspace/didChangeConfiguration`
notification, it will assess if the client has indicated if it has a
`workspaceConfiguration` capability. If it does, it will send a
`workspace/configuration` request which will include a request for the workspace
configuration as well as the configuration of all URIs that the language server
is currently tracking.
If the client has the `workspaceConfiguration` capability, the language server
will send a configuration request for the URI when it received the
`textDocument/didOpen` notification in order to get the resources specific
settings.
If the client does not have the `workspaceConfiguration` capability, the
language server will assume the workspace setting applies to all resources.
## Custom requests
The LSP currently supports the following custom requests. A client should
@ -62,55 +115,27 @@ with Deno:
}
```
## Settings
## Custom notifications
There are several settings that the language server supports for a workspace:
There is currently one custom notification that is send from the server to the
client:
- `deno.enable`
- `deno.config`
- `deno.import_map`
- `deno.code_lens.implementations`
- `deno.code_lens.references`
- `deno.code_lens.references_all_functions`
- `deno.suggest.complete_function_calls`
- `deno.suggest.names`
- `deno.suggest.paths`
- `deno.suggest.auto_imports`
- `deno.imports.hosts`
- `deno.lint`
- `deno.unstable`
- `deno/registryStatus` - when `deno.suggest.imports.autoDiscover` is `true` and
an origin for an import being added to a document is not explicitly set in
`deno.suggest.imports.hosts`, the origin will be checked and the notification
will be sent to the client of the status.
There are settings that are support on a per resource basis by the language
server:
When receiving the notification, if the param `suggestion` is `true`, the
client should offer the user the choice to enable the origin and add it to the
configuration for `deno.suggest.imports.hosts`. If `suggestion` is `false` the
client should add it to the configuration of as `false` to stop the language
server from attempting to detect if suggestions are supported.
- `deno.enable`
The params for the notification are:
There are several points in the process where Deno analyzes these settings.
First, when the `initialize` request from the client, the
`initializationOptions` will be assumed to be an object that represents the
`deno` namespace of options. For example, the following value:
```json
{
"enable": true,
"unstable": true
}
```
Would enable Deno with the unstable APIs for this instance of the language
server.
When the language server receives a `workspace/didChangeConfiguration`
notification, it will assess if the client has indicated if it has a
`workspaceConfiguration` capability. If it does, it will send a
`workspace/configuration` request which will include a request for the workspace
configuration as well as the configuration of all URIs that the language server
is currently tracking.
If the client has the `workspaceConfiguration` capability, the language server
will send a configuration request for the URI when it received the
`textDocument/didOpen` notification in order to get the resources specific
settings.
If the client does not have the `workspaceConfiguration` capability, the
language server will assume the workspace setting applies to all resources.
```ts
interface RegistryStatusNotificationParams {
origin: string;
suggestions: boolean;
}
```

View file

@ -2,6 +2,7 @@
use super::analysis;
use super::language_server;
use super::lsp_custom;
use super::tsc;
use crate::fs_util::is_supported_ext;
@ -9,6 +10,7 @@ use crate::media_type::MediaType;
use deno_core::normalize_path;
use deno_core::resolve_path;
use deno_core::resolve_url;
use deno_core::serde::Deserialize;
use deno_core::serde::Serialize;
use deno_core::url::Position;
@ -34,6 +36,64 @@ pub struct CompletionItemData {
pub tsc: Option<tsc::CompletionItemData>,
}
/// Check if the origin can be auto-configured for completions, and if so, send
/// a notification to the client.
async fn check_auto_config_registry(
url_str: &str,
snapshot: &language_server::StateSnapshot,
client: lspower::Client,
) {
// check to see if auto discovery is enabled
if snapshot
.config
.settings
.workspace
.suggest
.imports
.auto_discover
{
if let Ok(specifier) = resolve_url(url_str) {
let scheme = specifier.scheme();
let path = &specifier[Position::BeforePath..];
if scheme.starts_with("http")
&& !path.is_empty()
&& url_str.ends_with(path)
{
// check to see if this origin is already explicitly set
let in_config = snapshot
.config
.settings
.workspace
.suggest
.imports
.hosts
.iter()
.any(|(h, _)| {
resolve_url(h).map(|u| u.origin()) == Ok(specifier.origin())
});
// if it isn't in the configuration, we will check to see if it supports
// suggestions and send a notification to the client.
if !in_config {
let origin = specifier.origin().ascii_serialization();
let suggestions = snapshot
.module_registries
.fetch_config(&origin)
.await
.is_ok();
client
.send_custom_notification::<lsp_custom::RegistryStateNotification>(
lsp_custom::RegistryStateNotificationParams {
origin,
suggestions,
},
)
.await;
}
}
}
}
}
/// Given a specifier, a position, and a snapshot, optionally return a
/// completion response, which will be valid import completions for the specific
/// context.
@ -41,6 +101,7 @@ pub async fn get_import_completions(
specifier: &ModuleSpecifier,
position: &lsp::Position,
state_snapshot: &language_server::StateSnapshot,
client: lspower::Client,
) -> Option<lsp::CompletionResponse> {
if let Ok(Some(source)) = state_snapshot.documents.content(specifier) {
let media_type = MediaType::from(specifier);
@ -58,6 +119,8 @@ pub async fn get_import_completions(
}
// completion of modules from a module registry or cache
if !current_specifier.is_empty() {
check_auto_config_registry(&current_specifier, state_snapshot, client)
.await;
let offset = if position.character > range.start.character {
(position.character - range.start.character) as usize
} else {
@ -808,11 +871,17 @@ mod tests {
}
#[tokio::test]
async fn test_get_import_completions() {
async fn test_get_workspace_completions() {
let specifier = resolve_url("file:///a/b/c.ts").unwrap();
let position = lsp::Position {
line: 0,
character: 21,
let range = lsp::Range {
start: lsp::Position {
line: 0,
character: 20,
},
end: lsp::Position {
line: 0,
character: 21,
},
};
let state_snapshot = setup(
&[
@ -822,32 +891,29 @@ mod tests {
&[("https://deno.land/x/a/b/c.ts", "console.log(1);\n")],
);
let actual =
get_import_completions(&specifier, &position, &state_snapshot).await;
get_workspace_completions(&specifier, "h", &range, &state_snapshot);
assert_eq!(
actual,
Some(lsp::CompletionResponse::List(lsp::CompletionList {
is_incomplete: false,
items: vec![lsp::CompletionItem {
label: "https://deno.land/x/a/b/c.ts".to_string(),
kind: Some(lsp::CompletionItemKind::File),
detail: Some("(remote)".to_string()),
sort_text: Some("1".to_string()),
text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
range: lsp::Range {
start: lsp::Position {
line: 0,
character: 20
},
end: lsp::Position {
line: 0,
character: 21,
}
vec![lsp::CompletionItem {
label: "https://deno.land/x/a/b/c.ts".to_string(),
kind: Some(lsp::CompletionItemKind::File),
detail: Some("(remote)".to_string()),
sort_text: Some("1".to_string()),
text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
range: lsp::Range {
start: lsp::Position {
line: 0,
character: 20
},
new_text: "https://deno.land/x/a/b/c.ts".to_string(),
})),
..Default::default()
}]
}))
end: lsp::Position {
line: 0,
character: 21,
}
},
new_text: "https://deno.land/x/a/b/c.ts".to_string(),
})),
..Default::default()
}]
);
}
}

View file

@ -28,7 +28,7 @@ pub struct ClientCapabilities {
pub line_folding_only: bool,
}
#[derive(Debug, Clone, Deserialize)]
#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct CodeLensSettings {
/// Flag for providing implementation code lenses.
@ -53,16 +53,20 @@ impl Default for CodeLensSettings {
}
}
#[derive(Debug, Clone, Deserialize)]
fn is_true() -> bool {
true
}
#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct CompletionSettings {
#[serde(default)]
pub complete_function_calls: bool,
#[serde(default)]
#[serde(default = "is_true")]
pub names: bool,
#[serde(default)]
#[serde(default = "is_true")]
pub paths: bool,
#[serde(default)]
#[serde(default = "is_true")]
pub auto_imports: bool,
#[serde(default)]
pub imports: ImportCompletionSettings,
@ -80,9 +84,15 @@ impl Default for CompletionSettings {
}
}
#[derive(Debug, Clone, Deserialize)]
#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct ImportCompletionSettings {
/// A flag that indicates if non-explicitly set origins should be checked for
/// supporting import suggestions.
#[serde(default = "is_true")]
pub auto_discover: bool,
/// A map of origins which have had explicitly set if import suggestions are
/// enabled.
#[serde(default)]
pub hosts: HashMap<String, bool>,
}
@ -90,6 +100,7 @@ pub struct ImportCompletionSettings {
impl Default for ImportCompletionSettings {
fn default() -> Self {
Self {
auto_discover: true,
hosts: HashMap::default(),
}
}
@ -104,10 +115,11 @@ pub struct SpecifierSettings {
}
/// Deno language server specific settings that are applied to a workspace.
#[derive(Debug, Default, Clone, Deserialize)]
#[derive(Debug, Default, Clone, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct WorkspaceSettings {
/// A flag that indicates if Deno is enabled for the workspace.
#[serde(default)]
pub enable: bool,
/// An option that points to a path string of the config file to apply to
@ -420,4 +432,38 @@ mod tests {
.expect("could not update");
assert!(config.specifier_enabled(&specifier));
}
#[test]
fn test_set_workspace_settings_defaults() {
let config = setup();
config
.set_workspace_settings(json!({}))
.expect("could not update");
assert_eq!(
config.get_workspace_settings(),
WorkspaceSettings {
enable: false,
config: None,
import_map: None,
code_lens: CodeLensSettings {
implementations: false,
references: false,
references_all_functions: false,
},
internal_debug: false,
lint: false,
suggest: CompletionSettings {
complete_function_calls: false,
names: true,
paths: true,
auto_imports: true,
imports: ImportCompletionSettings {
auto_discover: true,
hosts: HashMap::new(),
}
},
unstable: false,
}
);
}
}

View file

@ -3,8 +3,6 @@
use deno_core::error::anyhow;
use deno_core::error::AnyError;
use deno_core::resolve_url;
use deno_core::serde::Deserialize;
use deno_core::serde::Serialize;
use deno_core::serde_json;
use deno_core::serde_json::json;
use deno_core::serde_json::Value;
@ -44,6 +42,7 @@ use super::config::SETTINGS_SECTION;
use super::diagnostics;
use super::diagnostics::DiagnosticSource;
use super::documents::DocumentCache;
use super::lsp_custom;
use super::performance::Performance;
use super::registries;
use super::sources;
@ -385,10 +384,9 @@ impl Inner {
.iter()
{
if *enabled {
info!("Enabling auto complete registry for: {}", registry);
info!("Enabling import suggestions for: {}", registry);
self.module_registries.enable(registry).await?;
} else {
info!("Disabling auto complete registry for: {}", registry);
self.module_registries.disable(registry).await?;
}
}
@ -552,17 +550,11 @@ impl Inner {
async fn initialized(&mut self, _: InitializedParams) {
// Check to see if we need to setup the import map
if let Err(err) = self.update_import_map().await {
self
.client
.show_message(MessageType::Warning, err.to_string())
.await;
self.client.show_message(MessageType::Warning, err).await;
}
// Check to see if we need to setup any module registries
if let Err(err) = self.update_registries().await {
self
.client
.show_message(MessageType::Warning, err.to_string())
.await;
self.client.show_message(MessageType::Warning, err).await;
}
if self
@ -713,22 +705,13 @@ impl Inner {
self.update_debug_flag();
if let Err(err) = self.update_import_map().await {
self
.client
.show_message(MessageType::Warning, err.to_string())
.await;
self.client.show_message(MessageType::Warning, err).await;
}
if let Err(err) = self.update_registries().await {
self
.client
.show_message(MessageType::Warning, err.to_string())
.await;
self.client.show_message(MessageType::Warning, err).await;
}
if let Err(err) = self.update_tsconfig().await {
self
.client
.show_message(MessageType::Warning, err.to_string())
.await;
self.client.show_message(MessageType::Warning, err).await;
}
if let Err(err) = self.diagnostics_server.update() {
error!("{}", err);
@ -748,10 +731,7 @@ impl Inner {
if let Some(import_map_uri) = &self.maybe_import_map_uri {
if params.changes.iter().any(|fe| *import_map_uri == fe.uri) {
if let Err(err) = self.update_import_map().await {
self
.client
.show_message(MessageType::Warning, err.to_string())
.await;
self.client.show_message(MessageType::Warning, err).await;
}
}
}
@ -759,10 +739,7 @@ impl Inner {
if let Some(config_uri) = &self.maybe_config_uri {
if params.changes.iter().any(|fe| *config_uri == fe.uri) {
if let Err(err) = self.update_tsconfig().await {
self
.client
.show_message(MessageType::Warning, err.to_string())
.await;
self.client.show_message(MessageType::Warning, err).await;
}
}
}
@ -1549,6 +1526,7 @@ impl Inner {
&specifier,
&params.text_document_position.position,
&self.snapshot()?,
self.client.clone(),
)
.await
{
@ -2004,27 +1982,31 @@ impl Inner {
params: Option<Value>,
) -> LspResult<Option<Value>> {
match method {
"deno/cache" => match params.map(serde_json::from_value) {
lsp_custom::CACHE_REQUEST => match params.map(serde_json::from_value) {
Some(Ok(params)) => self.cache(params).await,
Some(Err(err)) => Err(LspError::invalid_params(err.to_string())),
None => Err(LspError::invalid_params("Missing parameters")),
},
"deno/performance" => Ok(Some(self.get_performance())),
"deno/reloadImportRegistries" => self.reload_import_registries().await,
"deno/virtualTextDocument" => match params.map(serde_json::from_value) {
Some(Ok(params)) => Ok(Some(
serde_json::to_value(self.virtual_text_document(params).await?)
.map_err(|err| {
error!(
"Failed to serialize virtual_text_document response: {}",
err
);
LspError::internal_error()
})?,
)),
Some(Err(err)) => Err(LspError::invalid_params(err.to_string())),
None => Err(LspError::invalid_params("Missing parameters")),
},
lsp_custom::PERFORMANCE_REQUEST => Ok(Some(self.get_performance())),
lsp_custom::RELOAD_IMPORT_REGISTRIES_REQUEST => {
self.reload_import_registries().await
}
lsp_custom::VIRTUAL_TEXT_DOCUMENT => {
match params.map(serde_json::from_value) {
Some(Ok(params)) => Ok(Some(
serde_json::to_value(self.virtual_text_document(params).await?)
.map_err(|err| {
error!(
"Failed to serialize virtual_text_document response: {}",
err
);
LspError::internal_error()
})?,
)),
Some(Err(err)) => Err(LspError::invalid_params(err.to_string())),
None => Err(LspError::invalid_params("Missing parameters")),
}
}
_ => {
error!("Got a {} request, but no handler is defined", method);
Err(LspError::method_not_found())
@ -2437,28 +2419,14 @@ impl lspower::LanguageServer for LanguageServer {
}
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
struct CacheParams {
/// The document currently open in the editor. If there are no `uris`
/// supplied, the referrer will be cached.
referrer: TextDocumentIdentifier,
/// Any documents that have been specifically asked to be cached via the
/// command.
uris: Vec<TextDocumentIdentifier>,
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
struct VirtualTextDocumentParams {
text_document: TextDocumentIdentifier,
}
// These are implementations of custom commands supported by the LSP
impl Inner {
/// Similar to `deno cache` on the command line, where modules will be cached
/// in the Deno cache, including any of their dependencies.
async fn cache(&mut self, params: CacheParams) -> LspResult<Option<Value>> {
async fn cache(
&mut self,
params: lsp_custom::CacheParams,
) -> LspResult<Option<Value>> {
let mark = self.performance.mark("cache", Some(&params));
let referrer = self.url_map.normalize_url(&params.referrer.uri);
if !params.uris.is_empty() {
@ -2519,7 +2487,7 @@ impl Inner {
async fn virtual_text_document(
&mut self,
params: VirtualTextDocumentParams,
params: lsp_custom::VirtualTextDocumentParams,
) -> LspResult<Option<String>> {
let mark = self
.performance

42
cli/lsp/lsp_custom.rs Normal file
View file

@ -0,0 +1,42 @@
// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license.
use deno_core::serde::Deserialize;
use deno_core::serde::Serialize;
use lspower::lsp;
pub const CACHE_REQUEST: &str = "deno/cache";
pub const PERFORMANCE_REQUEST: &str = "deno/performance";
pub const RELOAD_IMPORT_REGISTRIES_REQUEST: &str =
"deno/reloadImportRegistries";
pub const VIRTUAL_TEXT_DOCUMENT: &str = "deno/virtualTextDocument";
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CacheParams {
/// The document currently open in the editor. If there are no `uris`
/// supplied, the referrer will be cached.
pub referrer: lsp::TextDocumentIdentifier,
/// Any documents that have been specifically asked to be cached via the
/// command.
pub uris: Vec<lsp::TextDocumentIdentifier>,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct RegistryStateNotificationParams {
pub origin: String,
pub suggestions: bool,
}
pub enum RegistryStateNotification {}
impl lsp::notification::Notification for RegistryStateNotification {
type Params = RegistryStateNotificationParams;
const METHOD: &'static str = "deno/registryState";
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct VirtualTextDocumentParams {
pub text_document: lsp::TextDocumentIdentifier,
}

View file

@ -11,6 +11,7 @@ mod config;
mod diagnostics;
mod documents;
pub(crate) mod language_server;
mod lsp_custom;
mod path_to_regex;
mod performance;
mod registries;

View file

@ -219,7 +219,7 @@ fn validate_config(config: &RegistryConfigurationJson) -> Result<(), AnyError> {
}
#[derive(Debug, Clone, Deserialize)]
struct RegistryConfigurationVariable {
pub(crate) struct RegistryConfigurationVariable {
/// The name of the variable.
key: String,
/// The URL with variable substitutions of the endpoint that will provide
@ -228,7 +228,7 @@ struct RegistryConfigurationVariable {
}
#[derive(Debug, Clone, Deserialize)]
struct RegistryConfiguration {
pub(crate) struct RegistryConfiguration {
/// A Express-like path which describes how URLs are composed for a registry.
schema: String,
/// The variables denoted in the `schema` should have a variable entry.
@ -339,7 +339,7 @@ impl ModuleRegistry {
}
/// Attempt to fetch the configuration for a specific origin.
async fn fetch_config(
pub(crate) async fn fetch_config(
&self,
origin: &str,
) -> Result<Vec<RegistryConfiguration>, AnyError> {
@ -443,6 +443,11 @@ impl ModuleRegistry {
.await
{
let end = if p.is_some() { i + 1 } else { i };
let end = if end > tokens.len() {
tokens.len()
} else {
end
};
let compiler = Compiler::new(&tokens[..end], None);
for (idx, item) in items.into_iter().enumerate() {
let label = if let Some(p) = &p {

View file

@ -1839,6 +1839,53 @@ fn lsp_completions_registry_empty() {
shutdown(&mut client);
}
#[test]
fn lsp_auto_discover_registry() {
let _g = http_server();
let mut client = init("initialize_params.json");
did_open(
&mut client,
json!({
"textDocument": {
"uri": "file:///a/file.ts",
"languageId": "typescript",
"version": 1,
"text": "import * as a from \"http://localhost:4545/x/a@\""
}
}),
);
let (maybe_res, maybe_err) = client
.write_request::<_, _, Value>(
"textDocument/completion",
json!({
"textDocument": {
"uri": "file:///a/file.ts"
},
"position": {
"line": 0,
"character": 46
},
"context": {
"triggerKind": 2,
"triggerCharacter": "@"
}
}),
)
.unwrap();
assert!(maybe_err.is_none());
assert!(maybe_res.is_some());
let (method, maybe_res) = client.read_notification().unwrap();
assert_eq!(method, "deno/registryState");
assert_eq!(
maybe_res,
Some(json!({
"origin": "http://localhost:4545",
"suggestions": true,
}))
);
shutdown(&mut client);
}
#[test]
fn lsp_diagnostics_warn() {
let _g = http_server();