1
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2024-12-21 23:04:45 -05:00

feat: support inlay hints (#16287)

Closes: #11853
This commit is contained in:
Kitson Kelly 2022-10-16 13:39:43 +11:00 committed by GitHub
parent 6d2656fd56
commit 7d78f58187
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 628 additions and 2 deletions

View file

@ -141,6 +141,6 @@ pub fn server_capabilities(
"denoConfigTasks": true,
"testingApi":true,
})),
inlay_hint_provider: None,
inlay_hint_provider: Some(OneOf::Left(true)),
}
}

View file

@ -106,6 +106,101 @@ impl Default for CompletionSettings {
}
}
#[derive(Debug, Default, Clone, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct InlayHintsSettings {
#[serde(default)]
pub parameter_names: InlayHintsParamNamesOptions,
#[serde(default)]
pub parameter_types: InlayHintsParamTypesOptions,
#[serde(default)]
pub variable_types: InlayHintsVarTypesOptions,
#[serde(default)]
pub property_declaration_types: InlayHintsPropDeclTypesOptions,
#[serde(default)]
pub function_like_return_types: InlayHintsFuncLikeReturnTypesOptions,
#[serde(default)]
pub enum_member_values: InlayHintsEnumMemberValuesOptions,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct InlayHintsParamNamesOptions {
#[serde(default)]
pub enabled: InlayHintsParamNamesEnabled,
#[serde(default = "is_true")]
pub suppress_when_argument_matches_name: bool,
}
impl Default for InlayHintsParamNamesOptions {
fn default() -> Self {
Self {
enabled: InlayHintsParamNamesEnabled::None,
suppress_when_argument_matches_name: true,
}
}
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub enum InlayHintsParamNamesEnabled {
None,
Literals,
All,
}
impl Default for InlayHintsParamNamesEnabled {
fn default() -> Self {
Self::None
}
}
#[derive(Debug, Default, Clone, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct InlayHintsParamTypesOptions {
#[serde(default)]
pub enabled: bool,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct InlayHintsVarTypesOptions {
#[serde(default)]
pub enabled: bool,
#[serde(default = "is_true")]
pub suppress_when_argument_matches_name: bool,
}
impl Default for InlayHintsVarTypesOptions {
fn default() -> Self {
Self {
enabled: false,
suppress_when_argument_matches_name: true,
}
}
}
#[derive(Debug, Default, Clone, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct InlayHintsPropDeclTypesOptions {
#[serde(default)]
pub enabled: bool,
}
#[derive(Debug, Default, Clone, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct InlayHintsFuncLikeReturnTypesOptions {
#[serde(default)]
pub enabled: bool,
}
#[derive(Debug, Default, Clone, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct InlayHintsEnumMemberValuesOptions {
#[serde(default)]
pub enabled: bool,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct ImportCompletionSettings {
@ -202,6 +297,9 @@ pub struct WorkspaceSettings {
#[serde(default)]
pub code_lens: CodeLensSettings,
#[serde(default)]
pub inlay_hints: InlayHintsSettings,
/// A flag that indicates if internal debug logging should be made available.
#[serde(default)]
pub internal_debug: bool,
@ -238,6 +336,19 @@ impl WorkspaceSettings {
pub fn enabled_code_lens(&self) -> bool {
self.code_lens.implementations || self.code_lens.references
}
/// Determine if any inlay hints are enabled. This allows short circuiting
/// when there are no inlay hints enabled.
pub fn enabled_inlay_hints(&self) -> bool {
!matches!(
self.inlay_hints.parameter_names.enabled,
InlayHintsParamNamesEnabled::None
) || self.inlay_hints.parameter_types.enabled
|| self.inlay_hints.variable_types.enabled
|| self.inlay_hints.property_declaration_types.enabled
|| self.inlay_hints.function_like_return_types.enabled
|| self.inlay_hints.enum_member_values.enabled
}
}
#[derive(Debug, Clone, Default)]
@ -566,6 +677,26 @@ mod tests {
references_all_functions: false,
test: true,
},
inlay_hints: InlayHintsSettings {
parameter_names: InlayHintsParamNamesOptions {
enabled: InlayHintsParamNamesEnabled::None,
suppress_when_argument_matches_name: true
},
parameter_types: InlayHintsParamTypesOptions { enabled: false },
variable_types: InlayHintsVarTypesOptions {
enabled: false,
suppress_when_argument_matches_name: true
},
property_declaration_types: InlayHintsPropDeclTypesOptions {
enabled: false
},
function_like_return_types: InlayHintsFuncLikeReturnTypesOptions {
enabled: false
},
enum_member_values: InlayHintsEnumMemberValuesOptions {
enabled: false
},
},
internal_debug: false,
lint: true,
suggest: CompletionSettings {

View file

@ -196,6 +196,13 @@ impl LanguageServer {
}
}
pub async fn inlay_hint(
&self,
params: InlayHintParams,
) -> LspResult<Option<Vec<InlayHint>>> {
self.0.lock().await.inlay_hint(params).await
}
pub async fn virtual_text_document(
&self,
params: Option<Value>,
@ -2896,6 +2903,50 @@ impl Inner {
)
}
async fn inlay_hint(
&self,
params: InlayHintParams,
) -> LspResult<Option<Vec<InlayHint>>> {
let specifier = self.url_map.normalize_url(&params.text_document.uri);
let workspace_settings = self.config.get_workspace_settings();
if !self.is_diagnosable(&specifier)
|| !self.config.specifier_enabled(&specifier)
|| !workspace_settings.enabled_inlay_hints()
{
return Ok(None);
}
let mark = self.performance.mark("inlay_hint", Some(&params));
let asset_or_doc = self.get_asset_or_document(&specifier)?;
let line_index = asset_or_doc.line_index();
let range = tsc::TextSpan::from_range(&params.range, line_index.clone())
.map_err(|err| {
error!("Failed to convert range to text_span: {}", err);
LspError::internal_error()
})?;
let req = tsc::RequestMethod::ProvideInlayHints((
specifier.clone(),
range,
(&workspace_settings).into(),
));
let maybe_inlay_hints: Option<Vec<tsc::InlayHint>> = self
.ts_server
.request(self.snapshot(), req)
.await
.map_err(|err| {
error!("Unable to get inlay hints: {}", err);
LspError::internal_error()
})?;
let maybe_inlay_hints = maybe_inlay_hints.map(|hints| {
hints
.iter()
.map(|hint| hint.to_lsp(line_index.clone()))
.collect()
});
self.performance.measure(mark);
Ok(maybe_inlay_hints)
}
async fn reload_import_registries(&mut self) -> LspResult<Option<Value>> {
fs_util::remove_dir_all_if_exists(&self.module_registries_location)
.await

View file

@ -11,6 +11,9 @@ pub const RELOAD_IMPORT_REGISTRIES_REQUEST: &str =
"deno/reloadImportRegistries";
pub const VIRTUAL_TEXT_DOCUMENT: &str = "deno/virtualTextDocument";
// While lsp_types supports inlay hints currently, tower_lsp does not.
pub const INLAY_HINT: &str = "textDocument/inlayHint";
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CacheParams {

View file

@ -58,6 +58,7 @@ pub async fn start() -> Result<(), AnyError> {
lsp_custom::VIRTUAL_TEXT_DOCUMENT,
LanguageServer::virtual_text_document,
)
.custom_method(lsp_custom::INLAY_HINT, LanguageServer::inlay_hint)
.finish();
Server::new(stdin, stdout, socket).serve(service).await;

View file

@ -288,6 +288,7 @@ pub fn get_repl_workspace_settings() -> WorkspaceSettings {
cache: None,
import_map: None,
code_lens: Default::default(),
inlay_hints: Default::default(),
internal_debug: false,
lint: false,
tls_certificate: None,

View file

@ -618,6 +618,15 @@ pub struct TextSpan {
}
impl TextSpan {
pub fn from_range(
range: &lsp::Range,
line_index: Arc<LineIndex>,
) -> Result<Self, AnyError> {
let start = line_index.offset_tsc(range.start)?;
let length = line_index.offset_tsc(range.end)? - start;
Ok(Self { start, length })
}
pub fn to_range(&self, line_index: Arc<LineIndex>) -> lsp::Range {
lsp::Range {
start: line_index.position_tsc(self.start.into()),
@ -932,6 +941,48 @@ impl NavigateToItem {
}
}
#[derive(Debug, Clone, Deserialize)]
pub enum InlayHintKind {
Type,
Parameter,
Enum,
}
impl InlayHintKind {
pub fn to_lsp(&self) -> Option<lsp::InlayHintKind> {
match self {
Self::Enum => None,
Self::Parameter => Some(lsp::InlayHintKind::PARAMETER),
Self::Type => Some(lsp::InlayHintKind::TYPE),
}
}
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct InlayHint {
pub text: String,
pub position: u32,
pub kind: InlayHintKind,
pub whitespace_before: Option<bool>,
pub whitespace_after: Option<bool>,
}
impl InlayHint {
pub fn to_lsp(&self, line_index: Arc<LineIndex>) -> lsp::InlayHint {
lsp::InlayHint {
position: line_index.position_tsc(self.position.into()),
label: lsp::InlayHintLabel::String(self.text.clone()),
kind: self.kind.to_lsp(),
padding_left: self.whitespace_before,
padding_right: self.whitespace_after,
text_edits: None,
tooltip: None,
data: None,
}
}
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct NavigationTree {
@ -2830,6 +2881,18 @@ pub enum IncludeInlayParameterNameHints {
All,
}
impl From<&config::InlayHintsParamNamesEnabled>
for IncludeInlayParameterNameHints
{
fn from(setting: &config::InlayHintsParamNamesEnabled) -> Self {
match setting {
config::InlayHintsParamNamesEnabled::All => Self::All,
config::InlayHintsParamNamesEnabled::Literals => Self::Literals,
config::InlayHintsParamNamesEnabled::None => Self::None,
}
}
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "kebab-case")]
#[allow(dead_code)]
@ -2910,6 +2973,8 @@ pub struct UserPreferences {
#[serde(skip_serializing_if = "Option::is_none")]
pub include_inlay_variable_type_hints: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub include_inlay_variable_type_hints_when_type_matches_name: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub include_inlay_property_declaration_type_hints: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub include_inlay_function_like_return_type_hints: Option<bool>,
@ -2921,6 +2986,43 @@ pub struct UserPreferences {
pub auto_import_file_exclude_patterns: Option<Vec<String>>,
}
impl From<&config::WorkspaceSettings> for UserPreferences {
fn from(workspace_settings: &config::WorkspaceSettings) -> Self {
let inlay_hints = &workspace_settings.inlay_hints;
Self {
include_inlay_parameter_name_hints: Some(
(&inlay_hints.parameter_names.enabled).into(),
),
include_inlay_parameter_name_hints_when_argument_matches_name: Some(
inlay_hints
.parameter_names
.suppress_when_argument_matches_name,
),
include_inlay_function_parameter_type_hints: Some(
inlay_hints.parameter_types.enabled,
),
include_inlay_variable_type_hints: Some(
inlay_hints.variable_types.enabled,
),
include_inlay_variable_type_hints_when_type_matches_name: Some(
inlay_hints
.variable_types
.suppress_when_argument_matches_name,
),
include_inlay_property_declaration_type_hints: Some(
inlay_hints.property_declaration_types.enabled,
),
include_inlay_function_like_return_type_hints: Some(
inlay_hints.function_like_return_types.enabled,
),
include_inlay_enum_member_value_hints: Some(
inlay_hints.enum_member_values.enabled,
),
..Default::default()
}
}
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct SignatureHelpItemsOptions {
@ -3053,6 +3155,8 @@ pub enum RequestMethod {
ProvideCallHierarchyIncomingCalls((ModuleSpecifier, u32)),
/// Resolve outgoing call hierarchy items for a specific position.
ProvideCallHierarchyOutgoingCalls((ModuleSpecifier, u32)),
/// Resolve inlay hints for a specific text span
ProvideInlayHints((ModuleSpecifier, TextSpan, UserPreferences)),
// Special request, used only internally by the LSP
Restart,
@ -3269,6 +3373,15 @@ impl RequestMethod {
"position": position
})
}
RequestMethod::ProvideInlayHints((specifier, span, preferences)) => {
json!({
"id": id,
"method": "provideInlayHints",
"specifier": state.denormalize_specifier(specifier),
"span": span,
"preferences": preferences,
})
}
RequestMethod::Restart => json!({
"id": id,
"method": "restart",

View file

@ -1071,6 +1071,213 @@ fn lsp_hover_disabled() {
shutdown(&mut client);
}
#[test]
fn lsp_inlay_hints() {
let mut client = init("initialize_params_hints.json");
did_open(
&mut client,
json!({
"textDocument": {
"uri": "file:///a/file.ts",
"languageId": "typescript",
"version": 1,
"text": r#"function a(b: string) {
return b;
}
a("foo");
enum C {
A,
}
parseInt("123", 8);
const d = Date.now();
class E {
f = Date.now();
}
["a"].map((v) => v + v);
"#
}
}),
);
let (maybe_res, maybe_err) = client
.write_request::<_, _, Value>(
"textDocument/inlayHint",
json!({
"textDocument": {
"uri": "file:///a/file.ts",
},
"range": {
"start": {
"line": 0,
"character": 0
},
"end": {
"line": 19,
"character": 0,
}
}
}),
)
.unwrap();
assert!(maybe_err.is_none());
assert_eq!(
json!(maybe_res),
json!([
{
"position": {
"line": 0,
"character": 21
},
"label": ": string",
"kind": 1,
"paddingLeft": true
},
{
"position": {
"line": 4,
"character": 10
},
"label": "b:",
"kind": 2,
"paddingRight": true
},
{
"position": {
"line": 7,
"character": 11
},
"label": "= 0",
"paddingLeft": true
},
{
"position": {
"line": 10,
"character": 17
},
"label": "string:",
"kind": 2,
"paddingRight": true
},
{
"position": {
"line": 10,
"character": 24
},
"label": "radix:",
"kind": 2,
"paddingRight": true
},
{
"position": {
"line": 12,
"character": 15
},
"label": ": number",
"kind": 1,
"paddingLeft": true
},
{
"position": {
"line": 15,
"character": 11
},
"label": ": number",
"kind": 1,
"paddingLeft": true
},
{
"position": {
"line": 18,
"character": 18
},
"label": "callbackfn:",
"kind": 2,
"paddingRight": true
},
{
"position": {
"line": 18,
"character": 20
},
"label": ": string",
"kind": 1,
"paddingLeft": true
},
{
"position": {
"line": 18,
"character": 21
},
"label": ": string",
"kind": 1,
"paddingLeft": true
}
])
);
}
#[test]
fn lsp_inlay_hints_not_enabled() {
let mut client = init("initialize_params.json");
did_open(
&mut client,
json!({
"textDocument": {
"uri": "file:///a/file.ts",
"languageId": "typescript",
"version": 1,
"text": r#"function a(b: string) {
return b;
}
a("foo");
enum C {
A,
}
parseInt("123", 8);
const d = Date.now();
class E {
f = Date.now();
}
["a"].map((v) => v + v);
"#
}
}),
);
let (maybe_res, maybe_err) = client
.write_request::<_, _, Value>(
"textDocument/inlayHint",
json!({
"textDocument": {
"uri": "file:///a/file.ts",
},
"range": {
"start": {
"line": 0,
"character": 0
},
"end": {
"line": 19,
"character": 0,
}
}
}),
)
.unwrap();
assert!(maybe_err.is_none());
assert_eq!(json!(maybe_res), json!(null));
}
#[test]
fn lsp_workspace_enable_paths() {
let mut params: lsp::InitializeParams = serde_json::from_value(load_fixture(

View file

@ -0,0 +1,102 @@
{
"processId": 0,
"clientInfo": {
"name": "test-harness",
"version": "1.0.0"
},
"rootUri": null,
"initializationOptions": {
"enable": true,
"cache": null,
"certificateStores": null,
"codeLens": {
"implementations": true,
"references": true,
"test": true
},
"config": null,
"importMap": null,
"inlayHints": {
"parameterNames": {
"enabled": "all"
},
"parameterTypes": {
"enabled": true
},
"variableTypes": {
"enabled": true
},
"propertyDeclarationTypes": {
"enabled": true
},
"functionLikeReturnTypes": {
"enabled": true
},
"enumMemberValues": {
"enabled": true
}
},
"lint": true,
"suggest": {
"autoImports": true,
"completeFunctionCalls": false,
"names": true,
"paths": true,
"imports": {
"hosts": {}
}
},
"testing": {
"args": [
"--allow-all"
],
"enable": true
},
"tlsCertificate": null,
"unsafelyIgnoreCertificateErrors": null,
"unstable": false
},
"capabilities": {
"textDocument": {
"codeAction": {
"codeActionLiteralSupport": {
"codeActionKind": {
"valueSet": [
"quickfix",
"refactor"
]
}
},
"isPreferredSupport": true,
"dataSupport": true,
"disabledSupport": true,
"resolveSupport": {
"properties": [
"edit"
]
}
},
"completion": {
"completionItem": {
"snippetSupport": true
}
},
"foldingRange": {
"lineFoldingOnly": true
},
"synchronization": {
"dynamicRegistration": true,
"willSave": true,
"willSaveWaitUntil": true,
"didSave": true
}
},
"workspace": {
"configuration": true,
"workspaceFolders": true
},
"experimental": {
"testingApi": true
}
}
}

View file

@ -898,6 +898,15 @@ delete Object.prototype.__proto__;
),
);
}
case "provideInlayHints":
return respond(
id,
languageService.provideInlayHints(
request.specifier,
request.span,
request.preferences,
),
);
default:
throw new TypeError(
// @ts-ignore exhausted case statement sets type to never

10
cli/tsc/compiler.d.ts vendored
View file

@ -77,7 +77,8 @@ declare global {
| GetTypeDefinitionRequest
| PrepareCallHierarchy
| ProvideCallHierarchyIncomingCalls
| ProvideCallHierarchyOutgoingCalls;
| ProvideCallHierarchyOutgoingCalls
| ProvideInlayHints;
interface BaseLanguageServerRequest {
id: number;
@ -255,6 +256,13 @@ declare global {
position: number;
}
interface ProvideInlayHints extends BaseLanguageServerRequest {
method: "provideInlayHints";
specifier: string;
span: ts.TextSpan;
preferences?: ts.UserPreferences;
}
interface Restart extends BaseLanguageServerRequest {
method: "restart";
}