Compare commits

...

11 Commits

Author SHA1 Message Date
SofusA f996fbe2da
Merge 43cbbcd586 into 362e97e927 2025-06-16 11:07:01 -04:00
Michael Davis 362e97e927
Update tree-house to v0.3.0
This release contains some fixes to highlight ordering which could cause
panics in the markdown component for highlights arriving out of order.
2025-06-16 10:27:19 -04:00
Michael Davis ba54b6afe4
LSP: Short-circuit documentColors request for no servers
This fixes a deadlock when starting Helix with very many files, like
`hx runtime/queries/*/*.scm`. The tree-sitter query files don't have
an active language server on my machine and yet we were spawning a tokio
task to collect documentColors responses. We can skip that entirely.
Further debugging is needed to figure out why this lead to a deadlock
previously.
2025-06-16 09:42:48 -04:00
Tatesa Uradnik 837627dd8a
feat: allow moving nonexistent file (#13748) 2025-06-16 08:19:28 -05:00
CalebLarsen 1246549afd
Fix: update c++ highlights (#13772) 2025-06-16 08:04:22 -05:00
uncenter ada8004ea5
Highlight HTML entities (#13753) 2025-06-16 08:03:02 -05:00
Sofus Addington 43cbbcd586
Add separate event for pulling visible documents diagnostics 2025-06-11 09:06:24 +02:00
Sofus Addington ec10b7e741
Cancel ongoing pull diagnostic events 2025-06-11 09:06:23 +02:00
Sofus Addington 53738e5392
Remove related documents changes 2025-06-11 09:06:23 +02:00
Sofus Addington 86b9ef9367
Update document by path 2025-06-11 09:06:23 +02:00
Sofus Addington 00a06a4024
Pull diagnostics 2025-06-11 09:06:22 +02:00
17 changed files with 479 additions and 17 deletions

4
Cargo.lock generated
View File

@ -2810,9 +2810,9 @@ checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
[[package]]
name = "tree-house"
version = "0.2.0"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "679e3296e503901cd9f6e116be5a43a9270222215bf6c78b4b1f4af5c3dcc62d"
checksum = "d00ea55222392f171ae004dd13b62edd09d995633abf0c13406a8df3547fb999"
dependencies = [
"arc-swap",
"hashbrown 0.15.4",

View File

@ -37,7 +37,7 @@ package.helix-tui.opt-level = 2
package.helix-term.opt-level = 2
[workspace.dependencies]
tree-house = { version = "0.2.0", default-features = false }
tree-house = { version = "0.3.0", default-features = false }
nucleo = "0.5.0"
slotmap = "1.0.7"
thiserror = "2.0"

View File

@ -265,6 +265,7 @@ pub enum LanguageServerFeature {
WorkspaceSymbols,
// Symbols, use bitflags, see above?
Diagnostics,
PullDiagnostics,
RenameSymbol,
InlayHints,
DocumentColors,
@ -289,6 +290,7 @@ impl Display for LanguageServerFeature {
DocumentSymbols => "document-symbols",
WorkspaceSymbols => "workspace-symbols",
Diagnostics => "diagnostics",
PullDiagnostics => "pull-diagnostics",
RenameSymbol => "rename-symbol",
InlayHints => "inlay-hints",
DocumentColors => "document-colors",

View File

@ -62,6 +62,7 @@ pub struct Client {
initialize_notify: Arc<Notify>,
/// workspace folders added while the server is still initializing
req_timeout: u64,
ongoing_work: Mutex<Vec<(lsp::TextDocumentIdentifier, lsp::ProgressToken)>>,
}
impl Client {
@ -258,6 +259,7 @@ impl Client {
root_uri,
workspace_folders: Mutex::new(workspace_folders),
initialize_notify: initialize_notify.clone(),
ongoing_work: Mutex::new(Default::default()),
};
Ok((client, server_rx, initialize_notify))
@ -372,6 +374,7 @@ impl Client {
Some(OneOf::Left(true) | OneOf::Right(_))
),
LanguageServerFeature::Diagnostics => true, // there's no extra server capability
LanguageServerFeature::PullDiagnostics => capabilities.diagnostic_provider.is_some(),
LanguageServerFeature::RenameSymbol => matches!(
capabilities.rename_provider,
Some(OneOf::Left(true)) | Some(OneOf::Right(_))
@ -602,6 +605,9 @@ impl Client {
did_rename: Some(true),
..Default::default()
}),
diagnostic: Some(lsp::DiagnosticWorkspaceClientCapabilities {
refresh_support: Some(true),
}),
..Default::default()
}),
text_document: Some(lsp::TextDocumentClientCapabilities {
@ -679,6 +685,10 @@ impl Client {
}),
..Default::default()
}),
diagnostic: Some(lsp::DiagnosticClientCapabilities {
dynamic_registration: Some(false),
related_document_support: Some(true),
}),
publish_diagnostics: Some(lsp::PublishDiagnosticsClientCapabilities {
version_support: Some(true),
tag_support: Some(lsp::TagSupport {
@ -1229,6 +1239,75 @@ impl Client {
Some(self.call::<lsp::request::RangeFormatting>(params))
}
pub fn mark_work_as_done(&self, id: lsp::ProgressToken) {
self.ongoing_work.lock().retain(|x| x.1 != id);
}
fn cancel_ongoing_work(&self, id: lsp::ProgressToken) {
self.notify::<lsp::notification::Cancel>(lsp::CancelParams { id: id.clone() });
self.mark_work_as_done(id);
}
pub fn text_document_diagnostic(
&self,
text_document: lsp::TextDocumentIdentifier,
previous_result_id: Option<String>,
) -> Option<(
impl Future<Output = Result<lsp::DocumentDiagnosticReportResult>>,
lsp::ProgressToken,
)> {
let capabilities = self.capabilities();
let ongoing_work = {
let ongoing_work_lock = self.ongoing_work.lock();
ongoing_work_lock
.iter()
.filter(|x| x.0 == text_document)
.cloned()
.collect::<Vec<_>>()
};
if !ongoing_work.is_empty() {
for id in ongoing_work.into_iter().map(|x| x.1) {
self.cancel_ongoing_work(id.clone());
}
}
// Return early if the server does not support pull diagnostic.
let identifier = match capabilities.diagnostic_provider.as_ref()? {
lsp::DiagnosticServerCapabilities::Options(cap) => cap.identifier.clone(),
lsp::DiagnosticServerCapabilities::RegistrationOptions(cap) => {
cap.diagnostic_options.identifier.clone()
}
};
let request_id = match self.next_request_id() {
jsonrpc::Id::Null => lsp::ProgressToken::Number(1),
jsonrpc::Id::Num(num) => lsp::ProgressToken::Number(num as i32),
jsonrpc::Id::Str(str) => lsp::ProgressToken::String(str),
};
let params = lsp::DocumentDiagnosticParams {
text_document: text_document.clone(),
identifier,
previous_result_id,
work_done_progress_params: lsp::WorkDoneProgressParams {
work_done_token: Some(request_id.clone()),
},
partial_result_params: lsp::PartialResultParams::default(),
};
self.ongoing_work
.lock()
.push((text_document, request_id.clone()));
Some((
self.call::<lsp::request::DocumentDiagnosticRequest>(params),
request_id,
))
}
pub fn text_document_document_highlight(
&self,
text_document: lsp::TextDocumentIdentifier,

View File

@ -463,6 +463,7 @@ pub enum MethodCall {
RegisterCapability(lsp::RegistrationParams),
UnregisterCapability(lsp::UnregistrationParams),
ShowDocument(lsp::ShowDocumentParams),
WorkspaceDiagnosticRefresh,
}
impl MethodCall {
@ -494,6 +495,7 @@ impl MethodCall {
let params: lsp::ShowDocumentParams = params.parse()?;
Self::ShowDocument(params)
}
lsp::request::WorkspaceDiagnosticRefresh::METHOD => Self::WorkspaceDiagnosticRefresh,
_ => {
return Err(Error::Unhandled);
}

View File

@ -1009,6 +1009,21 @@ impl Application {
let result = self.handle_show_document(params, offset_encoding);
Ok(json!(result))
}
Ok(MethodCall::WorkspaceDiagnosticRefresh) => {
for document in self.editor.documents() {
let language_server = language_server!();
if language_server.supports_feature(
syntax::config::LanguageServerFeature::PullDiagnostics,
) && document.supports_language_server(language_server.id())
{
handlers::diagnostics::pull_diagnostics_for_document(
document,
language_server,
);
}
}
Ok(serde_json::Value::Null)
}
};
let language_server = language_server!();

View File

@ -1,11 +1,13 @@
use std::sync::Arc;
use arc_swap::ArcSwap;
use diagnostics::PullAllDocumentsDiagnosticHandler;
use helix_event::AsyncHook;
use crate::config::Config;
use crate::events;
use crate::handlers::auto_save::AutoSaveHandler;
use crate::handlers::diagnostics::PullDiagnosticsHandler;
use crate::handlers::signature_help::SignatureHelpHandler;
pub use helix_view::handlers::Handlers;
@ -14,7 +16,7 @@ use self::document_colors::DocumentColorsHandler;
mod auto_save;
pub mod completion;
mod diagnostics;
pub mod diagnostics;
mod document_colors;
mod signature_help;
mod snippet;
@ -26,12 +28,16 @@ pub fn setup(config: Arc<ArcSwap<Config>>) -> Handlers {
let signature_hints = SignatureHelpHandler::new().spawn();
let auto_save = AutoSaveHandler::new().spawn();
let document_colors = DocumentColorsHandler::default().spawn();
let pull_diagnostics = PullDiagnosticsHandler::new().spawn();
let pull_all_documents_diagnostics = PullAllDocumentsDiagnosticHandler::new().spawn();
let handlers = Handlers {
completions: helix_view::handlers::completion::CompletionHandler::new(event_tx),
signature_hints,
auto_save,
document_colors,
pull_diagnostics,
pull_all_documents_diagnostics,
};
helix_view::handlers::register_hooks(&handlers);

View File

@ -1,12 +1,25 @@
use std::collections::HashSet;
use std::time::Duration;
use tokio::time::Instant;
use helix_core::diagnostic::DiagnosticProvider;
use helix_core::syntax::config::LanguageServerFeature;
use helix_core::Uri;
use helix_event::{register_hook, send_blocking};
use helix_lsp::lsp;
use helix_view::document::Mode;
use helix_view::events::DiagnosticsDidChange;
use helix_view::events::{
DiagnosticsDidChange, DocumentDidChange, DocumentDidOpen, LanguageServerInitialized,
};
use helix_view::handlers::diagnostics::DiagnosticEvent;
use helix_view::handlers::lsp::{PullAllDocumentsDiagnosticsEvent, PullDiagnosticsEvent};
use helix_view::handlers::Handlers;
use helix_view::{DocumentId, Editor};
use crate::events::OnModeSwitch;
use crate::job;
pub(super) fn register_hooks(_handlers: &Handlers) {
pub(super) fn register_hooks(handlers: &Handlers) {
register_hook!(move |event: &mut DiagnosticsDidChange<'_>| {
if event.editor.mode != Mode::Insert {
for (view, _) in event.editor.tree.views_mut() {
@ -21,4 +34,249 @@ pub(super) fn register_hooks(_handlers: &Handlers) {
}
Ok(())
});
let tx = handlers.pull_diagnostics.clone();
let tx_all_documents = handlers.pull_all_documents_diagnostics.clone();
register_hook!(move |event: &mut DocumentDidChange<'_>| {
if event
.doc
.has_language_server_with_feature(LanguageServerFeature::PullDiagnostics)
{
let document_id = event.doc.id();
send_blocking(&tx, PullDiagnosticsEvent { document_id });
send_blocking(&tx_all_documents, PullAllDocumentsDiagnosticsEvent {});
}
Ok(())
});
register_hook!(move |event: &mut DocumentDidOpen<'_>| {
let doc = doc!(event.editor, &event.doc);
for language_server in
doc.language_servers_with_feature(LanguageServerFeature::PullDiagnostics)
{
pull_diagnostics_for_document(doc, language_server);
}
Ok(())
});
register_hook!(move |event: &mut LanguageServerInitialized<'_>| {
let language_server = event.editor.language_server_by_id(event.server_id).unwrap();
if language_server.supports_feature(LanguageServerFeature::PullDiagnostics) {
for doc in event
.editor
.documents()
.filter(|doc| doc.supports_language_server(event.server_id))
{
pull_diagnostics_for_document(doc, language_server);
}
}
Ok(())
});
}
#[derive(Debug)]
pub(super) struct PullDiagnosticsHandler {
document_ids: HashSet<DocumentId>,
}
impl PullDiagnosticsHandler {
pub fn new() -> Self {
PullDiagnosticsHandler {
document_ids: Default::default(),
}
}
}
impl helix_event::AsyncHook for PullDiagnosticsHandler {
type Event = PullDiagnosticsEvent;
fn handle_event(
&mut self,
event: Self::Event,
_timeout: Option<tokio::time::Instant>,
) -> Option<tokio::time::Instant> {
self.document_ids.insert(event.document_id);
Some(Instant::now() + Duration::from_millis(125))
}
fn finish_debounce(&mut self) {
let document_ids = self.document_ids.clone();
job::dispatch_blocking(move |editor, _| {
for document_id in document_ids {
let document = editor.document(document_id);
let Some(document) = document else {
return;
};
let language_servers = document
.language_servers_with_feature(LanguageServerFeature::PullDiagnostics)
.filter(|ls| ls.is_initialized());
for language_server in language_servers {
pull_diagnostics_for_document(document, language_server);
}
}
})
}
}
#[derive(Debug)]
pub(super) struct PullAllDocumentsDiagnosticHandler {}
impl PullAllDocumentsDiagnosticHandler {
pub fn new() -> Self {
PullAllDocumentsDiagnosticHandler {}
}
}
impl helix_event::AsyncHook for PullAllDocumentsDiagnosticHandler {
type Event = PullAllDocumentsDiagnosticsEvent;
fn handle_event(
&mut self,
_event: Self::Event,
_timeout: Option<tokio::time::Instant>,
) -> Option<tokio::time::Instant> {
Some(Instant::now() + Duration::from_millis(500))
}
fn finish_debounce(&mut self) {
job::dispatch_blocking(move |editor, _| {
for document in editor.documents.values() {
let language_servers = document
.language_servers_with_feature(LanguageServerFeature::PullDiagnostics)
.filter(|ls| ls.is_initialized())
.filter(|ls| {
ls.capabilities().diagnostic_provider.as_ref().is_some_and(
|diagnostic_provider| match diagnostic_provider {
lsp::DiagnosticServerCapabilities::Options(options) => {
options.inter_file_dependencies
}
lsp::DiagnosticServerCapabilities::RegistrationOptions(options) => {
options.diagnostic_options.inter_file_dependencies
}
},
)
});
for language_server in language_servers {
pull_diagnostics_for_document(document, language_server);
}
}
})
}
}
pub fn pull_diagnostics_for_document(
doc: &helix_view::Document,
language_server: &helix_lsp::Client,
) {
let Some(future) = language_server
.text_document_diagnostic(doc.identifier(), doc.previous_diagnostic_id.clone())
else {
return;
};
let Some(uri) = doc.uri() else {
return;
};
let identifier = language_server
.capabilities()
.diagnostic_provider
.as_ref()
.and_then(|diagnostic_provider| match diagnostic_provider {
lsp::DiagnosticServerCapabilities::Options(options) => options.identifier.clone(),
lsp::DiagnosticServerCapabilities::RegistrationOptions(options) => {
options.diagnostic_options.identifier.clone()
}
});
let language_server_id = language_server.id();
let provider = DiagnosticProvider::Lsp {
server_id: language_server_id,
identifier,
};
let document_id = doc.id();
tokio::spawn(async move {
match future.0.await {
Ok(result) => {
job::dispatch(move |editor, _| {
if let Some(language_server) = editor.language_server_by_id(language_server_id)
{
language_server.mark_work_as_done(future.1);
};
handle_pull_diagnostics_response(editor, result, provider, uri, document_id)
})
.await
}
Err(err) => {
let parsed_cancellation_data = if let helix_lsp::Error::Rpc(error) = err {
error.data.and_then(|data| {
serde_json::from_value::<lsp::DiagnosticServerCancellationData>(data).ok()
})
} else {
log::error!("Pull diagnostic request failed: {err}");
return;
};
if let Some(parsed_cancellation_data) = parsed_cancellation_data {
if parsed_cancellation_data.retrigger_request {
tokio::time::sleep(Duration::from_millis(500)).await;
job::dispatch(move |editor, _| {
if let (Some(doc), Some(language_server)) = (
editor.document(document_id),
editor.language_server_by_id(language_server_id),
) {
language_server.mark_work_as_done(future.1);
if doc.supports_language_server(language_server_id) {
pull_diagnostics_for_document(doc, language_server);
}
}
})
.await;
}
}
}
}
});
}
fn handle_pull_diagnostics_response(
editor: &mut Editor,
result: lsp::DocumentDiagnosticReportResult,
provider: DiagnosticProvider,
uri: Uri,
document_id: DocumentId,
) {
match result {
lsp::DocumentDiagnosticReportResult::Report(report) => {
let result_id = match report {
lsp::DocumentDiagnosticReport::Full(report) => {
editor.handle_lsp_diagnostics(
&provider,
uri,
None,
report.full_document_diagnostic_report.items,
);
report.full_document_diagnostic_report.result_id
}
lsp::DocumentDiagnosticReport::Unchanged(report) => {
Some(report.unchanged_document_diagnostic_report.result_id)
}
};
if let Some(doc) = editor.document_mut(document_id) {
doc.previous_diagnostic_id = result_id;
};
}
lsp::DocumentDiagnosticReportResult::Partial(_) => {}
};
}

View File

@ -81,6 +81,10 @@ fn request_document_colors(editor: &mut Editor, doc_id: DocumentId) {
})
.collect();
if futures.is_empty() {
return;
}
tokio::spawn(async move {
let mut all_colors = Vec::new();
loop {

View File

@ -204,6 +204,8 @@ pub struct Document {
pub readonly: bool,
pub previous_diagnostic_id: Option<String>,
/// Annotations for LSP document color swatches
pub color_swatches: Option<DocumentColorSwatches>,
// NOTE: ideally this would live on the handler for color swatches. This is blocked on a
@ -728,6 +730,7 @@ impl Document {
color_swatches: None,
color_swatch_controller: TaskController::new(),
syn_loader,
previous_diagnostic_id: None,
}
}
@ -2277,6 +2280,10 @@ impl Document {
pub fn reset_all_inlay_hints(&mut self) {
self.inlay_hints = Default::default();
}
pub fn has_language_server_with_feature(&self, feature: LanguageServerFeature) -> bool {
self.language_servers_with_feature(feature).next().is_some()
}
}
#[derive(Debug, Default)]

View File

@ -1437,7 +1437,11 @@ impl Editor {
log::error!("failed to apply workspace edit: {err:?}")
}
}
fs::rename(old_path, &new_path)?;
if old_path.exists() {
fs::rename(old_path, &new_path)?;
}
if let Some(doc) = self.document_by_path(old_path) {
self.set_doc_path(doc.id(), &new_path);
}

View File

@ -22,6 +22,8 @@ pub struct Handlers {
pub signature_hints: Sender<lsp::SignatureHelpEvent>,
pub auto_save: Sender<AutoSaveEvent>,
pub document_colors: Sender<lsp::DocumentColorsEvent>,
pub pull_diagnostics: Sender<lsp::PullDiagnosticsEvent>,
pub pull_all_documents_diagnostics: Sender<lsp::PullAllDocumentsDiagnosticsEvent>,
}
impl Handlers {

View File

@ -30,6 +30,12 @@ pub enum SignatureHelpEvent {
RequestComplete { open: bool },
}
pub struct PullDiagnosticsEvent {
pub document_id: DocumentId,
}
pub struct PullAllDocumentsDiagnosticsEvent {}
#[derive(Debug)]
pub struct ApplyEditError {
pub kind: ApplyEditErrorKind,

View File

@ -937,7 +937,7 @@ indent = { tab-width = 2, unit = " " }
[[grammar]]
name = "html"
source = { git = "https://github.com/tree-sitter/tree-sitter-html", rev = "29f53d8f4f2335e61bf6418ab8958dac3282077a" }
source = { git = "https://github.com/tree-sitter/tree-sitter-html", rev = "cbb91a0ff3621245e890d1c50cc811bffb77a26b" }
[[language]]
name = "python"

View File

@ -1,3 +1,48 @@
; inherits: html
(tag_name) @tag
(erroneous_end_tag_name) @error
(doctype) @constant
(attribute_name) @attribute
(comment) @comment
((attribute
(attribute_name) @attribute
(quoted_attribute_value (attribute_value) @markup.link.url))
(#any-of? @attribute "href" "src"))
((element
(start_tag
(tag_name) @tag)
(text) @markup.link.label)
(#eq? @tag "a"))
(attribute [(attribute_value) (quoted_attribute_value)] @string)
((element
(start_tag
(tag_name) @tag)
(text) @markup.bold)
(#any-of? @tag "strong" "b"))
((element
(start_tag
(tag_name) @tag)
(text) @markup.italic)
(#any-of? @tag "em" "i"))
((element
(start_tag
(tag_name) @tag)
(text) @markup.strikethrough)
(#any-of? @tag "s" "del"))
[
"<"
">"
"</"
"/>"
"<!"
] @punctuation.bracket
"=" @punctuation.delimiter
["---"] @punctuation.delimiter

View File

@ -12,8 +12,6 @@
(namespace_definition name: (namespace_identifier) @namespace)
(namespace_identifier) @namespace
(qualified_identifier name: (identifier) @type.enum.variant)
(auto) @type
"decltype" @type
@ -21,12 +19,29 @@
(reference_declarator ["&" "&&"] @type.builtin)
(abstract_reference_declarator ["&" "&&"] @type.builtin)
; -------
; Functions
; -------
; Support up to 4 levels of nesting of qualifiers
; i.e. a::b::c::d::func();
(call_expression
function: (qualified_identifier
name: (identifier) @function))
(call_expression
function: (qualified_identifier
name: (qualified_identifier
name: (identifier) @function)))
(call_expression
function: (qualified_identifier
name: (qualified_identifier
name: (qualified_identifier
name: (identifier) @function))))
(call_expression
function: (qualified_identifier
name: (qualified_identifier
name: (qualified_identifier
name: (qualified_identifier
name: (identifier) @function)))))
(template_function
name: (identifier) @function)
@ -34,26 +49,42 @@
(template_method
name: (field_identifier) @function)
; Support up to 3 levels of nesting of qualifiers
; i.e. a::b::c::func();
; Support up to 4 levels of nesting of qualifiers
; i.e. a::b::c::d::func();
(function_declarator
declarator: (qualified_identifier
name: (identifier) @function))
(function_declarator
declarator: (qualified_identifier
name: (qualified_identifier
name: (identifier) @function)))
(function_declarator
declarator: (qualified_identifier
name: (qualified_identifier
name: (qualified_identifier
name: (identifier) @function))))
(function_declarator
declarator: (qualified_identifier
name: (qualified_identifier
name: (qualified_identifier
name: (qualified_identifier
name: (identifier) @function)))))
(function_declarator
declarator: (field_identifier) @function)
; Constructors
(class_specifier
(type_identifier) @type
(field_declaration_list
(function_definition
(function_declarator
(identifier) @constructor)))
(#eq? @type @constructor))
(destructor_name "~" @constructor
(identifier) @constructor)
; Parameters
(parameter_declaration

View File

@ -2,6 +2,7 @@
(erroneous_end_tag_name) @error
(doctype) @constant
(attribute_name) @attribute
(entity) @string.special.symbol
(comment) @comment
((attribute