Implement pretty TypeScript errors (#42494)
Closes #7844 This change uses tree-sitter highlights as a method of showing typescript errors prettily, keeping regex as simple as possible: <img width="832" height="446" alt="Screenshot 2025-11-11 at 3 40 24 PM" src="https://github.com/user-attachments/assets/0b3b6cf1-4d4d-4398-b89b-ef5ec0df87ec" /> It covers three main areas: 1. Diagnostics Diagnostics are now rendered with language-aware typescript, by providing the project's language registry. 2. Vtsls The LSP provider for typescript now implements the `diagnostic_message_to_markdown` function in the `LspAdapter` trait, so as to provide Diagnostics with \`\`\`typescript...\`\`\`-style code blocks for any selection of typescript longer than one word. In the single-word case, it simply wraps with \`\` 3. Typescript's `highlights.scm` `vtsls` doesn't provide strictly valid typescript in much of its messaging. Rather, it returns a message with snippets of typescript values which are invalid. Tree-sitter was not properly highlighting these snippets because it was expecting key-value formats. For instance: ``` type foo = { foo: string; bar: string; baz: number[] } ``` is valid, whereas simply ``` { foo: string; bar: string; baz: number[] } ``` is not. Therefore, highlights.scm needed to be adjusted in order to pattern-match on literal values that might be returned from the vtsls diagnostics messages. This was done by a) identifying arrow functions on their own, and b) augmenting the `statment_block` pattern matching in order to match on values which were clearly object literals. This approach may not be exhaustive - I'm happy to work on any additional cases we might identify from `vtsls` here - but hopefully demonstrates an extensible approach to making these messages look nice, without taking on the technical burden of extensive regex. Release Notes: - Show pretty TypeScript errors with language-aware Markdown.
This commit is contained in:
@@ -9,6 +9,36 @@
|
||||
(type_identifier) @type
|
||||
(predefined_type) @type.builtin
|
||||
|
||||
;; Highlights object literals by hijacking the statement_block pattern, but only if
|
||||
;; the statement block follows an object literal pattern
|
||||
((statement_block
|
||||
(labeled_statement
|
||||
;; highlight the label like a property name
|
||||
label: (statement_identifier) @property.name
|
||||
body: [
|
||||
;; match a terminating expression statement
|
||||
(expression_statement
|
||||
;; single identifier - treat as a type name
|
||||
[(identifier) @type.name
|
||||
;; object - treat as a property - type pair
|
||||
(object
|
||||
(pair
|
||||
key: (_) @property.name
|
||||
value: (_) @type.name))
|
||||
;; subscript_expression - treat as an array declaration
|
||||
(subscript_expression
|
||||
object: (_) @type.name
|
||||
index: (_)
|
||||
)
|
||||
;; templated string - treat each identifier contained as a type name
|
||||
(template_string
|
||||
(template_substitution
|
||||
(identifier) @type.name))
|
||||
])
|
||||
;; match a nested statement block
|
||||
(statement_block) @nested
|
||||
])))
|
||||
|
||||
(import_specifier
|
||||
"type"
|
||||
name: (identifier) @type
|
||||
@@ -79,6 +109,8 @@
|
||||
left: (identifier) @function
|
||||
right: [(function_expression) (arrow_function)])
|
||||
|
||||
(arrow_function) @function
|
||||
|
||||
; Literals
|
||||
|
||||
(this) @variable.special
|
||||
|
||||
@@ -6,11 +6,12 @@ use language::{LanguageName, LspAdapter, LspAdapterDelegate, LspInstaller, Toolc
|
||||
use lsp::{CodeActionKind, LanguageServerBinary, LanguageServerName};
|
||||
use node_runtime::{NodeRuntime, VersionStrategy};
|
||||
use project::{Fs, lsp_store::language_server_settings};
|
||||
use regex::Regex;
|
||||
use serde_json::Value;
|
||||
use std::{
|
||||
ffi::OsString,
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
sync::{Arc, LazyLock},
|
||||
};
|
||||
use util::{ResultExt, maybe, merge_json_value_into};
|
||||
|
||||
@@ -56,6 +57,20 @@ impl VtslsLspAdapter {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn enhance_diagnostic_message(message: &str) -> Option<String> {
|
||||
static SINGLE_WORD_REGEX: LazyLock<Regex> =
|
||||
LazyLock::new(|| Regex::new(r"'([^\s']*)'").expect("Failed to create REGEX"));
|
||||
|
||||
static MULTI_WORD_REGEX: LazyLock<Regex> =
|
||||
LazyLock::new(|| Regex::new(r"'([^']+\s+[^']*)'").expect("Failed to create REGEX"));
|
||||
|
||||
let first = SINGLE_WORD_REGEX.replace_all(message, "`$1`").to_string();
|
||||
let second = MULTI_WORD_REGEX
|
||||
.replace_all(&first, "\n```typescript\n$1\n```\n")
|
||||
.to_string();
|
||||
Some(second)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TypeScriptVersions {
|
||||
@@ -274,6 +289,10 @@ impl LspAdapter for VtslsLspAdapter {
|
||||
Ok(default_workspace_configuration)
|
||||
}
|
||||
|
||||
fn diagnostic_message_to_markdown(&self, message: &str) -> Option<String> {
|
||||
VtslsLspAdapter::enhance_diagnostic_message(message)
|
||||
}
|
||||
|
||||
fn language_ids(&self) -> HashMap<LanguageName, String> {
|
||||
HashMap::from_iter([
|
||||
(LanguageName::new("TypeScript"), "typescript".into()),
|
||||
@@ -302,3 +321,41 @@ async fn get_cached_ts_server_binary(
|
||||
.await
|
||||
.log_err()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::vtsls::VtslsLspAdapter;
|
||||
|
||||
#[test]
|
||||
fn test_diagnostic_message_to_markdown() {
|
||||
// Leaves simple messages unchanged
|
||||
let message = "The expected type comes from the return type of this signature.";
|
||||
|
||||
let expected = "The expected type comes from the return type of this signature.";
|
||||
|
||||
assert_eq!(
|
||||
VtslsLspAdapter::enhance_diagnostic_message(message).expect("Should be some"),
|
||||
expected
|
||||
);
|
||||
|
||||
// Parses both multi-word and single-word correctly
|
||||
let message = "Property 'baz' is missing in type '{ foo: string; bar: string; }' but required in type 'User'.";
|
||||
|
||||
let expected = "Property `baz` is missing in type \n```typescript\n{ foo: string; bar: string; }\n```\n but required in type `User`.";
|
||||
|
||||
assert_eq!(
|
||||
VtslsLspAdapter::enhance_diagnostic_message(message).expect("Should be some"),
|
||||
expected
|
||||
);
|
||||
|
||||
// Parses multi-and-single word in any order, and ignores existing newlines
|
||||
let message = "Type '() => { foo: string; bar: string; }' is not assignable to type 'GetUserFunction'.\n Property 'baz' is missing in type '{ foo: string; bar: string; }' but required in type 'User'.";
|
||||
|
||||
let expected = "Type \n```typescript\n() => { foo: string; bar: string; }\n```\n is not assignable to type `GetUserFunction`.\n Property `baz` is missing in type \n```typescript\n{ foo: string; bar: string; }\n```\n but required in type `User`.";
|
||||
|
||||
assert_eq!(
|
||||
VtslsLspAdapter::enhance_diagnostic_message(message).expect("Should be some"),
|
||||
expected
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user