Compare commits

...

2 Commits

Author SHA1 Message Date
Conrad Irwin
53cf8a4e0e WIP WIPW IPW
t
2025-07-11 16:49:14 -06:00
Conrad Irwin
6a9ec10dac WIP 2025-07-11 16:02:53 -06:00
11 changed files with 283 additions and 15 deletions

1
Cargo.lock generated
View File

@@ -9134,6 +9134,7 @@ dependencies = [
"futures 0.3.31",
"gpui",
"http_client",
"indoc",
"language",
"log",
"lsp",

View File

@@ -6,7 +6,7 @@ use editor::{
hover_popover::diagnostics_markdown_style,
};
use gpui::{AppContext, Entity, Focusable, WeakEntity};
use language::{BufferId, Diagnostic, DiagnosticEntry};
use language::{BufferId, Diagnostic, DiagnosticEntry, LanguageRegistry};
use lsp::DiagnosticSeverity;
use markdown::{Markdown, MarkdownElement};
use settings::Settings;
@@ -27,6 +27,7 @@ impl DiagnosticRenderer {
diagnostic_group: Vec<DiagnosticEntry<Point>>,
buffer_id: BufferId,
diagnostics_editor: Option<WeakEntity<ProjectDiagnosticsEditor>>,
languages: Arc<LanguageRegistry>,
cx: &mut App,
) -> Vec<DiagnosticBlock> {
let Some(primary_ix) = diagnostic_group
@@ -79,7 +80,9 @@ impl DiagnosticRenderer {
initial_range: primary.range.clone(),
severity: primary.diagnostic.severity,
diagnostics_editor: diagnostics_editor.clone(),
markdown: cx.new(|cx| Markdown::new(markdown.into(), None, None, cx)),
markdown: cx.new(|cx| {
Markdown::new(markdown.into(), Some(languages.clone()), None, cx)
}),
});
} else if entry.range.start.row.abs_diff(primary.range.start.row) < 5 {
let markdown = Self::markdown(&entry.diagnostic);
@@ -88,7 +91,9 @@ impl DiagnosticRenderer {
initial_range: entry.range.clone(),
severity: entry.diagnostic.severity,
diagnostics_editor: diagnostics_editor.clone(),
markdown: cx.new(|cx| Markdown::new(markdown.into(), None, None, cx)),
markdown: cx.new(|cx| {
Markdown::new(markdown.into(), Some(languages.clone()), None, cx)
}),
});
} else {
let mut markdown = Self::markdown(&entry.diagnostic);
@@ -100,7 +105,9 @@ impl DiagnosticRenderer {
initial_range: entry.range.clone(),
severity: entry.diagnostic.severity,
diagnostics_editor: diagnostics_editor.clone(),
markdown: cx.new(|cx| Markdown::new(markdown.into(), None, None, cx)),
markdown: cx.new(|cx| {
Markdown::new(markdown.into(), Some(languages.clone()), None, cx)
}),
});
}
}
@@ -127,9 +134,11 @@ impl editor::DiagnosticRenderer for DiagnosticRenderer {
buffer_id: BufferId,
snapshot: EditorSnapshot,
editor: WeakEntity<Editor>,
languages: Arc<LanguageRegistry>,
cx: &mut App,
) -> Vec<BlockProperties<Anchor>> {
let blocks = Self::diagnostic_blocks_for_group(diagnostic_group, buffer_id, None, cx);
let blocks =
Self::diagnostic_blocks_for_group(diagnostic_group, buffer_id, None, languages, cx);
blocks
.into_iter()
.map(|block| {
@@ -155,9 +164,11 @@ impl editor::DiagnosticRenderer for DiagnosticRenderer {
diagnostic_group: Vec<DiagnosticEntry<Point>>,
range: Range<Point>,
buffer_id: BufferId,
languages: Arc<LanguageRegistry>,
cx: &mut App,
) -> Option<Entity<Markdown>> {
let blocks = Self::diagnostic_blocks_for_group(diagnostic_group, buffer_id, None, cx);
let blocks =
Self::diagnostic_blocks_for_group(diagnostic_group, buffer_id, None, languages, cx);
blocks.into_iter().find_map(|block| {
if block.initial_range == range {
Some(block.markdown)

View File

@@ -508,6 +508,15 @@ impl ProjectDiagnosticsEditor {
window: &mut Window,
cx: &mut Context<Self>,
) -> Task<Result<()>> {
let languages = self
.editor
.read(cx)
.project
.as_ref()
.unwrap()
.read(cx)
.languages()
.clone();
let was_empty = self.multibuffer.read(cx).is_empty();
let buffer_snapshot = buffer.read(cx).snapshot();
let buffer_id = buffer_snapshot.remote_id();
@@ -559,6 +568,7 @@ impl ProjectDiagnosticsEditor {
group,
buffer_snapshot.remote_id(),
Some(this.clone()),
languages.clone(),
cx,
)
})?;

View File

@@ -111,8 +111,9 @@ use itertools::Itertools;
use language::{
AutoindentMode, BracketMatch, BracketPair, Buffer, Capability, CharKind, CodeLabel,
CursorShape, DiagnosticEntry, DiffOptions, DocumentationConfig, EditPredictionsMode,
EditPreview, HighlightedText, IndentKind, IndentSize, Language, OffsetRangeExt, Point,
Selection, SelectionGoal, TextObject, TransactionId, TreeSitterOptions, WordsQuery,
EditPreview, HighlightedText, IndentKind, IndentSize, Language, LanguageRegistry,
OffsetRangeExt, Point, Selection, SelectionGoal, TextObject, TransactionId, TreeSitterOptions,
WordsQuery,
language_settings::{
self, InlayHintSettings, LspInsertMode, RewrapBehavior, WordsCompletionMode,
all_language_settings, language_settings,
@@ -402,6 +403,7 @@ pub trait DiagnosticRenderer {
buffer_id: BufferId,
snapshot: EditorSnapshot,
editor: WeakEntity<Editor>,
languages: Arc<LanguageRegistry>,
cx: &mut App,
) -> Vec<BlockProperties<Anchor>>;
@@ -410,6 +412,7 @@ pub trait DiagnosticRenderer {
diagnostic_group: Vec<DiagnosticEntry<Point>>,
range: Range<Point>,
buffer_id: BufferId,
languages: Arc<LanguageRegistry>,
cx: &mut App,
) -> Option<Entity<markdown::Markdown>>;
@@ -16574,13 +16577,20 @@ impl Editor {
let Some(renderer) = GlobalDiagnosticRenderer::global(cx) else {
return;
};
let languages = self.project.as_ref().unwrap().read(cx).languages().clone();
let diagnostic_group = buffer
.diagnostic_group(buffer_id, diagnostic.diagnostic.group_id)
.collect::<Vec<_>>();
let blocks =
renderer.render_group(diagnostic_group, buffer_id, snapshot, cx.weak_entity(), cx);
let blocks = renderer.render_group(
diagnostic_group,
buffer_id,
snapshot,
cx.weak_entity(),
languages,
cx,
);
let blocks = self.display_map.update(cx, |display_map, cx| {
display_map.insert_blocks(blocks, cx).into_iter().collect()

View File

@@ -275,6 +275,13 @@ fn show_hover(
return None;
}
}
let languages = editor
.project
.as_ref()
.unwrap()
.read(cx)
.languages()
.clone();
let hover_popover_delay = EditorSettings::get_global(cx).hover_popover_delay;
let all_diagnostics_active = editor.active_diagnostics == ActiveDiagnostic::All;
@@ -340,7 +347,7 @@ fn show_hover(
renderer
.as_ref()
.and_then(|renderer| {
renderer.render_hover(group, point_range, buffer_id, cx)
renderer.render_hover(group, point_range, buffer_id, languages, cx)
})
.context("no rendered diagnostic")
})??;

View File

@@ -44,6 +44,7 @@ dap.workspace = true
futures.workspace = true
gpui.workspace = true
http_client.workspace = true
indoc.workspace = true
language.workspace = true
log.workspace = true
lsp.workspace = true

View File

@@ -262,6 +262,7 @@ impl LspAdapter for RustLspAdapter {
_: LanguageServerId,
_: Option<&'_ Buffer>,
) {
// https://zed.dev/cla
static REGEX: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"(?m)`([^`]+)\n`$").expect("Failed to create REGEX"));

View File

@@ -8,9 +8,10 @@ use futures::future::join_all;
use gpui::{App, AppContext, AsyncApp, Task};
use http_client::github::{AssetKind, GitHubLspBinaryVersion, build_asset_url};
use language::{
ContextLocation, ContextProvider, File, LanguageToolchainStore, LspAdapter, LspAdapterDelegate,
Buffer, ContextLocation, ContextProvider, File, LanguageToolchainStore, LspAdapter,
LspAdapterDelegate,
};
use lsp::{CodeActionKind, LanguageServerBinary, LanguageServerName};
use lsp::{CodeActionKind, LanguageServerBinary, LanguageServerId, LanguageServerName};
use node_runtime::NodeRuntime;
use project::{Fs, lsp_store::language_server_settings};
use serde_json::{Value, json};
@@ -605,6 +606,7 @@ impl LspAdapter for TypeScriptLspAdapter {
}
}
// >>> https://zed.dev/cla <<<
async fn fetch_server_binary(
&self,
latest_version: Box<dyn 'static + Send + Any>,
@@ -748,6 +750,15 @@ impl LspAdapter for TypeScriptLspAdapter {
("TSX".into(), "typescriptreact".into()),
])
}
fn process_diagnostics(
&self,
d: &mut lsp::PublishDiagnosticsParams,
_: LanguageServerId,
_: Option<&'_ Buffer>,
) {
dbg!("called with ", d);
}
}
async fn get_cached_ts_server_binary(

View File

@@ -280,6 +280,185 @@ impl LspAdapter for VtslsLspAdapter {
("TSX".into(), "typescriptreact".into()),
])
}
fn diagnostic_message_to_markdown(&self, message: &str) -> Option<String> {
use regex::{Captures, Regex};
dbg!(&message);
// Helper functions for formatting
let format_type_block = |prefix: &str, content: &str| -> String {
if prefix.is_empty() {
if content.len() > 50 || content.contains('\n') || content.contains('`') {
format!("\n```typescript\ntype a ={}\n```\n", dbg!(content))
} else {
format!("`{}`", dbg!(content))
}
} else {
if content.len() > 50 || content.contains('\n') || content.contains('`') {
format!(
"{}\n```typescript\ntype a ={}\n```\n",
prefix,
dbg!(content)
)
} else {
format!("{} `{}`", prefix, dbg!(content))
}
}
};
let format_typescript_block =
|content: &str| -> String { format!("\n\n```typescript\n{}\n```\n", dbg!(content)) };
let format_simple_type_block = |content: &str| -> String { format!("`{}`", dbg!(content)) };
let unstyle_code_block = |content: &str| -> String { format!("`{}`", dbg!(content)) };
let mut result = message.to_string();
// Format 'key' with "value"
let re = Regex::new(r#"(\w+)(\s+)'(.+?)'(\s+)with(\s+)"(.+?)""#).unwrap();
result = re
.replace_all(&result, |caps: &Captures| {
format!(
"{}{}`{}`{} with `\"{}\"`",
&caps[1], &caps[2], &caps[3], &caps[4], &caps[6]
)
})
.to_string();
// Format "key"
let re = Regex::new(r#"(\s)'"(.*?)"'(\s|:|.|$)"#).unwrap();
result = re
.replace_all(&result, |caps: &Captures| {
format!("{}`\"{}\"`{}", &caps[1], &caps[2], &caps[3])
})
.to_string();
// Format declare module snippet
let re = Regex::new(r#"['"](declare module )['"](.*)['""];['"']"#).unwrap();
result = re
.replace_all(&result, |caps: &Captures| {
format_typescript_block(&format!("{} \"{}\"", &caps[1], &caps[2]))
})
.to_string();
// Format missing props error
let re = Regex::new(r#"(is missing the following properties from type\s?)'(.*)': ([^:]+)"#)
.unwrap();
result = re
.replace_all(&result, |caps: &Captures| {
let props: Vec<&str> = caps[3].split(", ").filter(|s| !s.is_empty()).collect();
let props_html = props
.iter()
.map(|prop| format!("<li>{}</li>", prop))
.collect::<Vec<_>>()
.join("");
format!("{}`{}`: <ul>{}</ul>", &caps[1], &caps[2], props_html)
})
.to_string();
// Format type pairs
let re = Regex::new(r#"(?i)(types) ['"](.*?)['"] and ['"](.*?)['"][.]?"#).unwrap();
result = re
.replace_all(&result, |caps: &Captures| {
format!("{} `{}` and `{}`", &caps[1], &caps[2], &caps[3])
})
.to_string();
// Format type annotation options
let re = Regex::new(r#"(?i)type annotation must be ['"](.*?)['"] or ['"](.*?)['"][.]?"#)
.unwrap();
result = re
.replace_all(&result, |caps: &Captures| {
format!("type annotation must be `{}` or `{}`", &caps[1], &caps[2])
})
.to_string();
// Format overload
let re = Regex::new(r#"(?i)(Overload \d of \d), ['"](.*?)['"], "#).unwrap();
result = re
.replace_all(&result, |caps: &Captures| {
format!("{}, `{}`, ", &caps[1], &caps[2])
})
.to_string();
// Format simple strings
let re = Regex::new(r#"^['"]"[^"]*"['"]$"#).unwrap();
result = re
.replace_all(&result, |caps: &Captures| format_typescript_block(&caps[0]))
.to_string();
// Replace module 'x' by module "x" for ts error #2307
let re = Regex::new(r#"(?i)(module )'([^"]*?)'"#).unwrap();
result = re
.replace_all(&result, |caps: &Captures| {
format!("{}\"{}\"", &caps[1], &caps[2])
})
.to_string();
// Format string types
let re = Regex::new(r#"(?i)(module|file|file name|imported via) ['""](.*?)['""]"#).unwrap();
result = re
.replace_all(&result, |caps: &Captures| {
format_type_block(&caps[1], &format!("\"{}\"", &caps[2]))
})
.to_string();
// Format types
dbg!(&result);
let re = Regex::new(r#"(?i)(type|type alias|interface|module|file|file name|class|method's|subtype of constraint) ['"](.*?)['"]"#).unwrap();
result = re
.replace_all(&result, |caps: &Captures| {
dbg!(&caps);
format_type_block(&caps[1], &caps[2])
})
.to_string();
// Format reversed types
let re = Regex::new(r#"(?i)(.*)['"]([^>]*)['"] (type|interface|return type|file|module|is (not )?assignable)"#).unwrap();
result = re
.replace_all(&result, |caps: &Captures| {
format!("{}`{}` {}", &caps[1], &caps[2], &caps[3])
})
.to_string();
// Format simple types that didn't captured before
let re = Regex::new(
r#"['"]((void|null|undefined|any|boolean|string|number|bigint|symbol)(\[\])?)['"']"#,
)
.unwrap();
result = re
.replace_all(&result, |caps: &Captures| {
format_simple_type_block(&caps[1])
})
.to_string();
// Format some typescript keywords
let re = Regex::new(r#"['"](import|export|require|in|continue|break|let|false|true|const|new|throw|await|for await|[0-9]+)( ?.*?)['"]"#).unwrap();
result = re
.replace_all(&result, |caps: &Captures| {
format_typescript_block(&format!("{}{}", &caps[1], &caps[2]))
})
.to_string();
// Format return values
let re = Regex::new(r#"(?i)(return|operator) ['"](.*?)['"']"#).unwrap();
result = re
.replace_all(&result, |caps: &Captures| {
format!("{} {}", &caps[1], format_typescript_block(&caps[2]))
})
.to_string();
// Format regular code blocks
let re = Regex::new(r#"(\W|^)'([^'"]*?)'(\W|$)"#).unwrap();
result = re
.replace_all(&result, |caps: &Captures| {
format!("{}{}{}", &caps[1], unstyle_code_block(&caps[2]), &caps[3])
})
.to_string();
Some(result)
}
}
async fn get_cached_ts_server_binary(
@@ -301,3 +480,25 @@ async fn get_cached_ts_server_binary(
.await
.log_err()
}
#[cfg(test)]
mod test {
use super::*;
use indoc::indoc;
#[test]
fn test_diagnostic_message_to_markdown() {
let message = "Property 'user' is missing in type '{ person: { username: string; email: string; }; }' but required in type '{ user: { name: string; email: `${string}@${string}.${string}`; age: number; }; }'.";
let expected = indoc! { "
Property `user` is missing in type `{ person: { username: string; email: string; }; }` but required in type
```typescript
{ user: { name: string; email: `${string}@${string}.${string}`; age: number; }; }
```
"};
let result = VtslsLspAdapter::new(NodeRuntime::unavailable())
.diagnostic_message_to_markdown(message)
.unwrap();
pretty_assertions::assert_eq!(result, expected.to_string());
}
}

View File

@@ -1534,12 +1534,26 @@ impl MarkdownElementBuilder {
rendered_index: self.pending_line.text.len(),
source_index: source_range.start,
});
self.pending_line.text.push_str(text);
if text.starts_with("type a =") {
self.pending_line.text.push_str(&text["type a =".len()..]);
} else {
self.pending_line.text.push_str(text);
}
self.current_source_index = source_range.end;
if let Some(Some(language)) = self.code_block_stack.last() {
dbg!(&language);
let mut offset = 0;
for (range, highlight_id) in language.highlight_text(&Rope::from(text), 0..text.len()) {
for (mut range, highlight_id) in
language.highlight_text(&Rope::from(text), 0..text.len())
{
if text.starts_with("type a =") {
if range.start < "type a =".len() || range.end < "type a =".len() {
continue;
}
range.start -= "type a =".len();
range.end -= "type a =".len();
};
if range.start > offset {
self.pending_line
.runs

View File

@@ -54,6 +54,7 @@ impl sqlez::bindable::Bind for SerializedAxis {
}
}
// > https://zed.dev/cla
impl sqlez::bindable::Column for SerializedAxis {
fn column(
statement: &mut sqlez::statement::Statement,