Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(lsp): registry auto discovery #10813

Merged
merged 2 commits into from
Jun 1, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
125 changes: 75 additions & 50 deletions cli/lsp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,69 @@ 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

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
Expand Down Expand Up @@ -62,55 +115,27 @@ with Deno:
}
```

## Settings

There are several settings that the language server supports for a workspace:

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

There are settings that are support on a per resource basis by the language
server:

- `deno.enable`
## Custom notifications

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
}
```
There is currently one custom notification that is send from the server to the
client:

Would enable Deno with the unstable APIs for this instance of the language
server.
- `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.

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

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.
The params for the notification are:

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;
}
```
120 changes: 93 additions & 27 deletions cli/lsp/completions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@

use super::analysis;
use super::language_server;
use super::lsp_custom;
use super::tsc;

use crate::fs_util::is_supported_ext;
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;
Expand All @@ -34,13 +36,72 @@ 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.
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);
Expand All @@ -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 {
Expand Down Expand Up @@ -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(
&[
Expand All @@ -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()
}]
);
}
}
Loading