@@ -1,5 +1,6 @@
1use std::collections::HashMap;
1use std::collections::HashMap;
2use std::sync::Arc;
2use std::sync::Arc;
3+use std::sync::atomic::{AtomicBool, Ordering};
3use std::time::Duration;
4use std::time::Duration;
5use serde_json::json;
6use serde_json::json;
@@ -36,6 +37,9 @@ struct Backend {
36 /// Per-URI debounce handles. The value is a join handle for an in-flight
37 /// Per-URI debounce handles. The value is a join handle for an in-flight
37 /// diagnostic publish; replacing the entry cancels the prior handle.
38 /// diagnostic publish; replacing the entry cancels the prior handle.
38 pending_diags: Arc<Mutex<HashMap<Url, JoinHandle<()>>>>,
39 pending_diags: Arc<Mutex<HashMap<Url, JoinHandle<()>>>>,
40+ /// Whether the client accepted the `utf-8` position encoding. When false,
41+ /// positions are UTF-16 code units (the LSP default).
42+ utf8_positions: AtomicBool,
41const DIAG_DEBOUNCE: Duration = Duration::from_millis(150);
45const DIAG_DEBOUNCE: Duration = Duration::from_millis(150);
@@ -55,7 +59,11 @@ impl Backend {
57 .insert(uri.clone(), entry.clone());
61 .insert(uri.clone(), entry.clone());
62+ self.doc_updated(uri, entry).await;
65+ /// Follow-up work after a document entry has been stored.
66+ async fn doc_updated(&self, uri: Url, entry: Arc<DocumentEntry>) {
59 // Refresh the workspace symbol index for this file synchronously —
67 // Refresh the workspace symbol index for this file synchronously —
60 // symbol extraction is cheap (single pass over text) and keeps
68 // symbol extraction is cheap (single pass over text) and keeps
61 // workspace-wide queries consistent without waiting on the debounce.
69 // workspace-wide queries consistent without waiting on the debounce.
@@ -152,9 +160,22 @@ impl LanguageServer for Backend {
152 self.index.write().await.set_root(root);
160 self.index.write().await.set_root(root);
163+ // Positions are byte offsets throughout this server, so prefer the
164+ // `utf-8` encoding — but only if the client offers it. Otherwise we
165+ // must stay on the UTF-16 default (VS Code refuses to start a server
166+ // that answers with an encoding it didn't offer).
167+ let client_supports_utf8 = params
171+ .and_then(|g| g.position_encodings.as_ref())
172+ .is_some_and(|encs| encs.contains(&PositionEncodingKind::UTF8));
174+ .store(client_supports_utf8, Ordering::Relaxed);
155 Ok(InitializeResult {
176 Ok(InitializeResult {
156 capabilities: ServerCapabilities {
177 capabilities: ServerCapabilities {
157− position_encoding: Some(PositionEncodingKind::UTF8),
178+ position_encoding: client_supports_utf8.then_some(PositionEncodingKind::UTF8),
158 text_document_sync: Some(TextDocumentSyncCapability::Kind(
179 text_document_sync: Some(TextDocumentSyncCapability::Kind(
159 TextDocumentSyncKind::INCREMENTAL,
180 TextDocumentSyncKind::INCREMENTAL,
@@ -279,15 +300,22 @@ impl LanguageServer for Backend {
279 let uri = params.text_document.uri.clone();
300 let uri = params.text_document.uri.clone();
280 let version = params.text_document.version;
301 let version = params.text_document.version;
282− // Start from the current text and apply each change in order.
303+ let utf8 = self.utf8_positions.load(Ordering::Relaxed);
283− let mut text = match self.doc(&uri).await {
304+
284− Some(d) => d.text.clone(),
305+ // Hold the write lock across read-modify-write: tower-lsp may run
285− None => String::new(),
306+ // notification handlers concurrently, and two interleaved incremental
307+ // edits applied to the same base text would lose one of them.
309+ let mut docs = self.documents.write().await;
310+ let mut text = docs.get(&uri).map(|d| d.text.clone()).unwrap_or_default();
311+ for change in ¶ms.content_changes {
312+ apply_change(&mut text, change, utf8);
314+ let entry = Arc::new(DocumentEntry::new(text, version));
315+ docs.insert(uri.clone(), entry.clone());
287− for change in ¶ms.content_changes {
288− apply_change(&mut text, change);
290− self.set_doc(uri, text, version).await;
318+ self.doc_updated(uri, entry).await;
293 async fn did_close(&self, params: DidCloseTextDocumentParams) {
321 async fn did_close(&self, params: DidCloseTextDocumentParams) {
@@ -303,6 +331,16 @@ impl LanguageServer for Backend {
304 handle.abort();
332 handle.abort();
334+ // The index may hold symbols from unsaved buffer edits; resync it
335+ // with what's actually on disk.
336+ if let Ok(path) = params.text_document.uri.to_file_path() {
337+ let mut idx = self.index.write().await;
339+ idx.update_from_disk(&path);
306 self.client
344 self.client
307 .publish_diagnostics(params.text_document.uri, vec![], None)
345 .publish_diagnostics(params.text_document.uri, vec![], None)
@@ -613,8 +651,12 @@ impl LanguageServer for Backend {
613 if formatted == doc.text {
651 if formatted == doc.text {
614 return Ok(None);
652 return Ok(None);
654+ // End the edit at the true end of the document. `str::lines` drops a
655+ // trailing newline, which would leave it outside the replaced range
656+ // and append an extra blank line after the (newline-terminated)
616− let last_line = doc.text.lines().count().saturating_sub(1) as u32;
658+ let last_line = doc.text.matches('\n').count() as u32;
617− let last_col = doc.text.lines().last().map_or(0, |l| l.len()) as u32;
659+ let last_col = doc.text.rsplit('\n').next().map_or(0, |l| l.len()) as u32;
618 Ok(Some(vec![TextEdit {
660 Ok(Some(vec![TextEdit {
619 range: Range::new(Position::new(0, 0), Position::new(last_line, last_col)),
661 range: Range::new(Position::new(0, 0), Position::new(last_line, last_col)),
620 new_text: formatted,
662 new_text: formatted,
@@ -738,6 +780,7 @@ impl LanguageServer for Backend {
738 #[derive(Clone)]
780 #[derive(Clone)]
739 struct Def {
781 struct Def {
740 line: u32,
782 line: u32,
741 name: String,
784 name: String,
742 kind: &'static str,
785 kind: &'static str,
@@ -746,6 +789,9 @@ impl LanguageServer for Backend {
746 for (i, line) in lines.iter().enumerate() {
789 for (i, line) in lines.iter().enumerate() {
747 let trimmed = line.trim_start();
790 let trimmed = line.trim_start();
748 if let Some(rest) = trimmed.strip_prefix("@let ") {
791 if let Some(rest) = trimmed.strip_prefix("@let ") {
792+ // `rest` is a suffix of `line`, so the name's column is
793+ // however much of the line precedes it.
794+ let col = (line.len() - rest.trim_start().len()) as u32;
749 let rest = rest.trim();
795 let rest = rest.trim();
750 if let Some(name) = rest.split_whitespace().next() {
796 if let Some(name) = rest.split_whitespace().next() {
751 let has_body = lines
797 let has_body = lines
@@ -758,18 +804,21 @@ impl LanguageServer for Backend {
759 defs.push(Def {
805 defs.push(Def {
760 line: i as u32,
806 line: i as u32,
761 name: name.to_string(),
808 name: name.to_string(),
762 kind: "fn",
809 kind: "fn",
764 } else if value_after_name.starts_with('[') {
811 } else if value_after_name.starts_with('[') {
765 defs.push(Def {
812 defs.push(Def {
766 line: i as u32,
813 line: i as u32,
767 name: name.to_string(),
815 name: name.to_string(),
768 kind: "define",
816 kind: "define",
771 defs.push(Def {
819 defs.push(Def {
772 line: i as u32,
820 line: i as u32,
773 name: name.to_string(),
822 name: name.to_string(),
774 kind: "let",
823 kind: "let",
@@ -780,7 +829,7 @@ impl LanguageServer for Backend {
781 let mut lenses = Vec::with_capacity(defs.len());
830 let mut lenses = Vec::with_capacity(defs.len());
782 for def in &defs {
831 for def in &defs {
783− let mut count: usize = 0;
832+ let mut locations: Vec<Location> = Vec::new();
784 let needle = match def.kind {
833 let needle = match def.kind {
785 "fn" => format!("@{}", def.name),
834 "fn" => format!("@{}", def.name),
786 _ => format!("${}", def.name),
835 _ => format!("${}", def.name),
@@ -798,27 +847,36 @@ impl LanguageServer for Backend {
798 Some(c) => !(c.is_ascii_alphanumeric() || c == b'_' || c == b'-'),
847 Some(c) => !(c.is_ascii_alphanumeric() || c == b'_' || c == b'-'),
801− count += 1;
850+ locations.push(Location {
853+ Position::new(i as u32, pos as u32),
854+ Position::new(i as u32, (pos + needle.len()) as u32),
803 from = pos + needle.len();
858 from = pos + needle.len();
807− let title = if count == 1 {
862+ let title = if locations.len() == 1 {
808 "1 reference".to_string()
863 "1 reference".to_string()
810− format!("{} references", count)
865+ format!("{} references", locations.len())
812 // A clickable lens that opens VS Code's references panel at this
867 // A clickable lens that opens VS Code's references panel at this
813− // definition. The command name matches what VS Code's built-in
868+ // definition. The arguments mirror what VS Code's built-in
814 // `editor.action.showReferences` accepts: (uri, position, locations).
869 // `editor.action.showReferences` accepts: (uri, position, locations).
870+ // The locations are sent precomputed because a reference query at
871+ // the definition site would resolve the `@let` keyword instead.
815 let line_pos = Position::new(def.line, 0);
872 let line_pos = Position::new(def.line, 0);
873+ let name_pos = Position::new(def.line, def.col);
816 lenses.push(CodeLens {
874 lenses.push(CodeLens {
817 range: Range::new(line_pos, line_pos),
875 range: Range::new(line_pos, line_pos),
818 command: Some(Command {
876 command: Some(Command {
820 command: "htmlang.showReferences".into(),
878 command: "htmlang.showReferences".into(),
821− arguments: Some(vec![json!(uri), json!(line_pos)]),
879+ arguments: Some(vec![json!(uri), json!(name_pos), json!(locations)]),
823 data: None,
881 data: None,
@@ -917,6 +975,7 @@ async fn main() {
917 documents: Arc::new(RwLock::new(HashMap::new())),
975 documents: Arc::new(RwLock::new(HashMap::new())),
918 index: Arc::new(RwLock::new(WorkspaceIndex::new())),
976 index: Arc::new(RwLock::new(WorkspaceIndex::new())),
919 pending_diags: Arc::new(Mutex::new(HashMap::new())),
977 pending_diags: Arc::new(Mutex::new(HashMap::new())),
978+ utf8_positions: AtomicBool::new(false),
921 Server::new(stdin, stdout, socket).serve(service).await;
980 Server::new(stdin, stdout, socket).serve(service).await;