diff --git a/crates/djls-server/src/server.rs b/crates/djls-server/src/server.rs index 2e1da39d..f92504c8 100644 --- a/crates/djls-server/src/server.rs +++ b/crates/djls-server/src/server.rs @@ -157,7 +157,7 @@ impl LanguageServer for DjangoLanguageServer { save: Some(lsp_types::SaveOptions::default().into()), }, )), - position_encoding: Some(lsp_types::PositionEncodingKind::from(encoding)), + position_encoding: Some(djls_workspace::position_encoding_to_lsp(encoding)), diagnostic_provider: Some(lsp_types::DiagnosticServerCapabilities::Options( lsp_types::DiagnosticOptions { identifier: None, diff --git a/crates/djls-server/src/session.rs b/crates/djls-server/src/session.rs index f3cf3025..ff0096f0 100644 --- a/crates/djls-server/src/session.rs +++ b/crates/djls-server/src/session.rs @@ -77,7 +77,7 @@ impl Session { settings, workspace, client_capabilities: params.capabilities.clone(), - position_encoding: PositionEncoding::negotiate(params), + position_encoding: djls_workspace::negotiate_position_encoding(params), db, } } diff --git a/crates/djls-source/src/lib.rs b/crates/djls-source/src/lib.rs index b2227aef..3f290be3 100644 --- a/crates/djls-source/src/lib.rs +++ b/crates/djls-source/src/lib.rs @@ -1,6 +1,7 @@ mod db; mod file; mod position; +mod protocol; pub use db::Db; pub use file::File; @@ -9,3 +10,4 @@ pub use position::ByteOffset; pub use position::LineCol; pub use position::LineIndex; pub use position::Span; +pub use protocol::PositionEncoding; diff --git a/crates/djls-source/src/protocol.rs b/crates/djls-source/src/protocol.rs new file mode 100644 index 00000000..9667d490 --- /dev/null +++ b/crates/djls-source/src/protocol.rs @@ -0,0 +1,192 @@ +use std::fmt; + +use crate::position::ByteOffset; +use crate::position::LineCol; +use crate::position::LineIndex; + +/// Specifies how column positions are counted in text. +/// +/// While motivated by LSP (Language Server Protocol) requirements, this enum +/// represents a fundamental choice about text position measurement that any +/// text processing system must make. Different systems count "column" positions +/// differently: +/// +/// - Some count bytes (fast but breaks on multi-byte characters) +/// - Some count UTF-16 code units (common in JavaScript/Windows ecosystems) +/// - Some count Unicode codepoints (intuitive but slower) +/// +/// This crate provides encoding-aware position conversion to support different +/// client expectations without coupling to specific protocol implementations. +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] +pub enum PositionEncoding { + /// Column positions count UTF-8 code units (bytes from line start) + Utf8, + /// Column positions count UTF-16 code units (common in VS Code and Windows editors) + #[default] + Utf16, + /// Column positions count Unicode scalar values (codepoints) + Utf32, +} + +impl fmt::Display for PositionEncoding { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Utf8 => write!(f, "utf-8"), + Self::Utf16 => write!(f, "utf-16"), + Self::Utf32 => write!(f, "utf-32"), + } + } +} + +impl PositionEncoding { + /// Convert a line/column position to a byte offset with encoding awareness. + /// + /// The encoding specifies how the column value should be interpreted: + /// - `PositionEncoding::Utf8`: column is a byte offset from line start + /// - `PositionEncoding::Utf16`: column counts UTF-16 code units + /// - `PositionEncoding::Utf32`: column counts Unicode codepoints + /// + /// This method is primarily used to convert protocol-specific positions + /// (which may use different column counting methods) into byte offsets + /// that can be used to index into the actual UTF-8 text. + /// + /// # Examples + /// + /// ``` + /// # use djls_source::{LineIndex, LineCol, ByteOffset, PositionEncoding}; + /// let text = "Hello 🌍 world"; + /// let index = LineIndex::from_text(text); + /// + /// // UTF-16: "Hello " (6) + "🌍" (2 UTF-16 units) = position 8 + /// let offset = PositionEncoding::Utf16.line_col_to_offset( + /// &index, + /// LineCol((0, 8)), + /// text + /// ); + /// assert_eq!(offset, Some(ByteOffset(10))); // "Hello 🌍" is 10 bytes + /// ``` + #[must_use] + pub fn line_col_to_offset( + &self, + index: &LineIndex, + line_col: LineCol, + text: &str, + ) -> Option { + let line = line_col.line(); + let character = line_col.column(); + + // Handle line bounds - if line > line_count, return document length + let line_start_utf8 = match index.lines().get(line as usize) { + Some(start) => *start, + None => return Some(ByteOffset(u32::try_from(text.len()).unwrap_or(u32::MAX))), + }; + + if character == 0 { + return Some(ByteOffset(line_start_utf8)); + } + + let next_line_start = index + .lines() + .get(line as usize + 1) + .copied() + .unwrap_or_else(|| u32::try_from(text.len()).unwrap_or(u32::MAX)); + + let line_text = text.get(line_start_utf8 as usize..next_line_start as usize)?; + + // Fast path optimization for ASCII text, all encodings are equivalent to byte offsets + if line_text.is_ascii() { + let char_offset = character.min(u32::try_from(line_text.len()).unwrap_or(u32::MAX)); + return Some(ByteOffset(line_start_utf8 + char_offset)); + } + + match self { + PositionEncoding::Utf8 => { + // UTF-8: character positions are already byte offsets + let char_offset = character.min(u32::try_from(line_text.len()).unwrap_or(u32::MAX)); + Some(ByteOffset(line_start_utf8 + char_offset)) + } + PositionEncoding::Utf16 => { + // UTF-16: count UTF-16 code units + let mut utf16_pos = 0; + let mut utf8_pos = 0; + + for c in line_text.chars() { + if utf16_pos >= character { + break; + } + utf16_pos += u32::try_from(c.len_utf16()).unwrap_or(0); + utf8_pos += u32::try_from(c.len_utf8()).unwrap_or(0); + } + + // If character position exceeds line length, clamp to line end + Some(ByteOffset(line_start_utf8 + utf8_pos)) + } + PositionEncoding::Utf32 => { + // UTF-32: count Unicode code points (characters) + let mut utf8_pos = 0; + + for (char_count, c) in line_text.chars().enumerate() { + if char_count >= character as usize { + break; + } + utf8_pos += u32::try_from(c.len_utf8()).unwrap_or(0); + } + + // If character position exceeds line length, clamp to line end + Some(ByteOffset(line_start_utf8 + utf8_pos)) + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_position_encoding_display() { + assert_eq!(PositionEncoding::Utf8.to_string(), "utf-8"); + assert_eq!(PositionEncoding::Utf16.to_string(), "utf-16"); + assert_eq!(PositionEncoding::Utf32.to_string(), "utf-32"); + } + + #[test] + fn test_line_col_to_offset_utf16() { + let text = "Hello 🌍 world"; + let index = LineIndex::from_text(text); + + // "Hello " = 6 UTF-16 units, "🌍" = 2 UTF-16 units + // So position (0, 8) in UTF-16 should be after the emoji + let offset = PositionEncoding::Utf16 + .line_col_to_offset(&index, LineCol((0, 8)), text) + .expect("Should get offset"); + assert_eq!(offset, ByteOffset(10)); // "Hello 🌍" is 10 bytes + + // In UTF-8, character 10 would be at the 'r' in 'world' + let offset_utf8 = PositionEncoding::Utf8 + .line_col_to_offset(&index, LineCol((0, 10)), text) + .expect("Should get offset"); + assert_eq!(offset_utf8, ByteOffset(10)); + } + + #[test] + fn test_line_col_to_offset_ascii_fast_path() { + let text = "Hello world"; + let index = LineIndex::from_text(text); + + // For ASCII text, all encodings should give the same result + let offset_utf8 = PositionEncoding::Utf8 + .line_col_to_offset(&index, LineCol((0, 5)), text) + .expect("Should get offset"); + let offset_utf16 = PositionEncoding::Utf16 + .line_col_to_offset(&index, LineCol((0, 5)), text) + .expect("Should get offset"); + let offset_utf32 = PositionEncoding::Utf32 + .line_col_to_offset(&index, LineCol((0, 5)), text) + .expect("Should get offset"); + + assert_eq!(offset_utf8, ByteOffset(5)); + assert_eq!(offset_utf16, ByteOffset(5)); + assert_eq!(offset_utf32, ByteOffset(5)); + } +} diff --git a/crates/djls-workspace/src/document.rs b/crates/djls-workspace/src/document.rs index 2d538a26..e14f1c4c 100644 --- a/crates/djls-workspace/src/document.rs +++ b/crates/djls-workspace/src/document.rs @@ -6,10 +6,10 @@ //! and diagnostics. use djls_source::LineIndex; +use djls_source::PositionEncoding; use tower_lsp_server::lsp_types::Position; use tower_lsp_server::lsp_types::Range; -use crate::encoding::PositionEncoding; use crate::language::LanguageId; /// In-memory representation of an open document in the LSP. @@ -77,9 +77,10 @@ impl TextDocument { #[must_use] pub fn get_text_range(&self, range: Range, encoding: PositionEncoding) -> Option { let start_offset = - self.offset_for_position_with_text(range.start, &self.content, encoding)? as usize; + Self::calculate_offset(&self.line_index, range.start, &self.content, encoding)? + as usize; let end_offset = - self.offset_for_position_with_text(range.end, &self.content, encoding)? as usize; + Self::calculate_offset(&self.line_index, range.end, &self.content, encoding)? as usize; Some(self.content[start_offset..end_offset].to_string()) } @@ -112,20 +113,12 @@ impl TextDocument { if let Some(range) = change.range { // Convert LSP range to byte offsets using the current line index // that matches the current state of new_content - let start_offset = Self::position_to_offset_with( - &new_line_index, - range.start, - &new_content, - encoding, - ) - .unwrap_or(0) as usize; - let end_offset = Self::position_to_offset_with( - &new_line_index, - range.end, - &new_content, - encoding, - ) - .unwrap_or(0) as usize; + let start_offset = + Self::calculate_offset(&new_line_index, range.start, &new_content, encoding) + .unwrap_or(0) as usize; + let end_offset = + Self::calculate_offset(&new_line_index, range.end, &new_content, encoding) + .unwrap_or(0) as usize; // Apply change new_content.replace_range(start_offset..end_offset, &change.text); @@ -144,103 +137,17 @@ impl TextDocument { self.version = version; } - #[must_use] - pub fn position_to_offset( - &self, - position: Position, - encoding: PositionEncoding, - ) -> Option { - self.offset_for_position_with_text(position, &self.content, encoding) - } - - /// Convert position to text offset using a specific line index. - /// - /// This is used during incremental updates where we have a temporary line index - /// that matches the temporary content state. - fn position_to_offset_with( + /// Calculate byte offset from an LSP position using the given line index and text. + fn calculate_offset( line_index: &LineIndex, position: Position, text: &str, encoding: PositionEncoding, ) -> Option { - // Handle line bounds - if line > line_count, return document length - let line_start_utf8 = match line_index.lines().get(position.line as usize) { - Some(start) => *start, - None => return Some(u32::try_from(text.len()).unwrap_or(u32::MAX)), // Past end of document - }; - - if position.character == 0 { - return Some(line_start_utf8); - } - - let next_line_start = line_index - .lines() - .get(position.line as usize + 1) - .copied() - .unwrap_or_else(|| u32::try_from(text.len()).unwrap_or(u32::MAX)); - - let line_text = text.get(line_start_utf8 as usize..next_line_start as usize)?; - - // Fast path optimization for ASCII text, all encodings are equivalent to byte offsets - if line_text.is_ascii() { - let char_offset = position - .character - .min(u32::try_from(line_text.len()).unwrap_or(u32::MAX)); - return Some(line_start_utf8 + char_offset); - } - - match encoding { - PositionEncoding::Utf8 => { - // UTF-8: character positions are already byte offsets - let char_offset = position - .character - .min(u32::try_from(line_text.len()).unwrap_or(u32::MAX)); - Some(line_start_utf8 + char_offset) - } - PositionEncoding::Utf16 => { - // UTF-16: count UTF-16 code units - let mut utf16_pos = 0; - let mut utf8_pos = 0; - - for c in line_text.chars() { - if utf16_pos >= position.character { - break; - } - utf16_pos += u32::try_from(c.len_utf16()).unwrap_or(0); - utf8_pos += u32::try_from(c.len_utf8()).unwrap_or(0); - } - - // If character position exceeds line length, clamp to line end - Some(line_start_utf8 + utf8_pos) - } - PositionEncoding::Utf32 => { - // UTF-32: count Unicode code points (characters) - let mut utf8_pos = 0; - - for (char_count, c) in line_text.chars().enumerate() { - if char_count >= position.character as usize { - break; - } - utf8_pos += u32::try_from(c.len_utf8()).unwrap_or(0); - } - - // If character position exceeds line length, clamp to line end - Some(line_start_utf8 + utf8_pos) - } - } - } - - /// Convert position to text offset using the specified encoding. - /// - /// Returns a valid offset, clamping out-of-bounds positions to document/line boundaries. - /// This method uses the document's current line index and content. - fn offset_for_position_with_text( - &self, - position: Position, - text: &str, - encoding: PositionEncoding, - ) -> Option { - Self::position_to_offset_with(&self.line_index, position, text, encoding) + let line_col = djls_source::LineCol((position.line, position.character)); + encoding + .line_col_to_offset(line_index, line_col, text) + .map(|djls_source::ByteOffset(offset)| offset) } } @@ -391,20 +298,24 @@ mod tests { let content = "Hello 🌍!\nSecond 行 line"; let doc = TextDocument::new(content.to_string(), 1, LanguageId::HtmlDjango); - // Test position after emoji + // Test position after emoji by extracting text up to that position // "Hello 🌍!" - the 🌍 emoji is 4 UTF-8 bytes but 2 UTF-16 code units - // Position after the emoji should be at UTF-16 position 7 (Hello + space + emoji) - let pos_after_emoji = Position::new(0, 7); - let offset = doc - .position_to_offset(pos_after_emoji, PositionEncoding::Utf16) - .expect("Should get offset"); + // "Hello " = 6 UTF-16 units, emoji = 2 UTF-16 units, so position 8 is after emoji + let range_to_after_emoji = Range::new(Position::new(0, 0), Position::new(0, 8)); + let text_to_after_emoji = doc + .get_text_range(range_to_after_emoji, PositionEncoding::Utf16) + .expect("Should get text range"); + assert_eq!(text_to_after_emoji, "Hello 🌍"); - // The UTF-8 byte offset should be at the "!" character - assert_eq!(doc.content().chars().nth(7).unwrap(), '!'); - assert_eq!(&doc.content()[(offset as usize)..=(offset as usize)], "!"); + // Verify the next character is "!" + let range_exclamation = Range::new(Position::new(0, 8), Position::new(0, 9)); + let exclamation = doc + .get_text_range(range_exclamation, PositionEncoding::Utf16) + .expect("Should get exclamation"); + assert_eq!(exclamation, "!"); // Test range extraction with non-ASCII characters - let range = Range::new(Position::new(0, 0), Position::new(0, 7)); + let range = Range::new(Position::new(0, 0), Position::new(0, 8)); let text = doc .get_text_range(range, PositionEncoding::Utf16) .expect("Should get text range"); @@ -413,16 +324,18 @@ mod tests { // Test position on second line with CJK character // "Second 行 line" - 行 is 3 UTF-8 bytes but 1 UTF-16 code unit // Position after the CJK character should be at UTF-16 position 8 - let pos_after_cjk = Position::new(1, 8); - let offset_cjk = doc - .position_to_offset(pos_after_cjk, PositionEncoding::Utf16) - .expect("Should get offset"); - - // Find the start of line 2 in UTF-8 bytes - let line2_start = doc.content().find('\n').unwrap() + 1; - let line2_offset = offset_cjk as usize - line2_start; - let line2 = &doc.content()[line2_start..]; - assert_eq!(&line2[line2_offset..=line2_offset], " "); + let range_to_after_cjk = Range::new(Position::new(1, 0), Position::new(1, 8)); + let text_to_after_cjk = doc + .get_text_range(range_to_after_cjk, PositionEncoding::Utf16) + .expect("Should get text range"); + assert_eq!(text_to_after_cjk, "Second 行"); + + // Verify the next character is a space + let range_space = Range::new(Position::new(1, 8), Position::new(1, 9)); + let space = doc + .get_text_range(range_space, PositionEncoding::Utf16) + .expect("Should get space"); + assert_eq!(space, " "); } #[test] diff --git a/crates/djls-workspace/src/encoding.rs b/crates/djls-workspace/src/encoding.rs index 93cda1f1..49d9285d 100644 --- a/crates/djls-workspace/src/encoding.rs +++ b/crates/djls-workspace/src/encoding.rs @@ -1,86 +1,51 @@ -use std::fmt; -use std::str::FromStr; - -use tower_lsp_server::lsp_types::InitializeParams; -use tower_lsp_server::lsp_types::PositionEncodingKind; - -#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] -pub enum PositionEncoding { - Utf8, - #[default] - Utf16, - Utf32, -} - -impl PositionEncoding { - /// Negotiate the best encoding with the client based on their capabilities. - /// Prefers UTF-8 > UTF-32 > UTF-16 for performance reasons. - pub fn negotiate(params: &InitializeParams) -> Self { - let client_encodings: &[PositionEncodingKind] = params - .capabilities - .general - .as_ref() - .and_then(|general| general.position_encodings.as_ref()) - .map_or(&[], |encodings| encodings.as_slice()); - - // Try to find the best encoding in preference order - for preferred in [ - PositionEncoding::Utf8, - PositionEncoding::Utf32, - PositionEncoding::Utf16, - ] { - if client_encodings - .iter() - .any(|kind| PositionEncoding::try_from(kind.clone()).ok() == Some(preferred)) - { - return preferred; - } +use djls_source::PositionEncoding; +use tower_lsp_server::lsp_types; + +/// Negotiate the best encoding with the client based on their capabilities. +/// Prefers UTF-8 > UTF-32 > UTF-16 for performance reasons. +pub fn negotiate_position_encoding(params: &lsp_types::InitializeParams) -> PositionEncoding { + let client_encodings: &[lsp_types::PositionEncodingKind] = params + .capabilities + .general + .as_ref() + .and_then(|general| general.position_encodings.as_ref()) + .map_or(&[], |encodings| encodings.as_slice()); + + for preferred in [ + PositionEncoding::Utf8, + PositionEncoding::Utf32, + PositionEncoding::Utf16, + ] { + if client_encodings + .iter() + .any(|kind| position_encoding_from_lsp(kind) == Some(preferred)) + { + return preferred; } - - // Fallback to UTF-16 if client doesn't specify encodings - PositionEncoding::Utf16 } -} -impl FromStr for PositionEncoding { - type Err = (); - - fn from_str(s: &str) -> Result { - match s { - "utf-8" => Ok(PositionEncoding::Utf8), - "utf-16" => Ok(PositionEncoding::Utf16), - "utf-32" => Ok(PositionEncoding::Utf32), - _ => Err(()), - } - } + // Fallback to UTF-16 if client doesn't specify encodings + PositionEncoding::Utf16 } -impl fmt::Display for PositionEncoding { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let s = match self { - PositionEncoding::Utf8 => "utf-8", - PositionEncoding::Utf16 => "utf-16", - PositionEncoding::Utf32 => "utf-32", - }; - write!(f, "{s}") +#[must_use] +pub fn position_encoding_to_lsp(encoding: PositionEncoding) -> lsp_types::PositionEncodingKind { + match encoding { + PositionEncoding::Utf8 => lsp_types::PositionEncodingKind::new("utf-8"), + PositionEncoding::Utf16 => lsp_types::PositionEncodingKind::new("utf-16"), + PositionEncoding::Utf32 => lsp_types::PositionEncodingKind::new("utf-32"), } } -impl From for PositionEncodingKind { - fn from(encoding: PositionEncoding) -> Self { - match encoding { - PositionEncoding::Utf8 => PositionEncodingKind::new("utf-8"), - PositionEncoding::Utf16 => PositionEncodingKind::new("utf-16"), - PositionEncoding::Utf32 => PositionEncodingKind::new("utf-32"), - } - } -} - -impl TryFrom for PositionEncoding { - type Error = (); - - fn try_from(kind: PositionEncodingKind) -> Result { - kind.as_str().parse() +#[must_use] +pub fn position_encoding_from_lsp( + kind: &lsp_types::PositionEncodingKind, +) -> Option { + match kind.as_str() { + "utf-8" => Some(PositionEncoding::Utf8), + "utf-16" => Some(PositionEncoding::Utf16), + "utf-32" => Some(PositionEncoding::Utf32), + _ => None, } } @@ -92,74 +57,51 @@ mod tests { use super::*; #[test] - fn test_string_parsing_and_display() { - // Valid encodings parse correctly + fn test_lsp_type_conversions() { + // position_encoding_from_lsp for valid encodings assert_eq!( - "utf-8".parse::(), - Ok(PositionEncoding::Utf8) + position_encoding_from_lsp(&lsp_types::PositionEncodingKind::new("utf-8")), + Some(PositionEncoding::Utf8) ); assert_eq!( - "utf-16".parse::(), - Ok(PositionEncoding::Utf16) + position_encoding_from_lsp(&lsp_types::PositionEncodingKind::new("utf-16")), + Some(PositionEncoding::Utf16) ); assert_eq!( - "utf-32".parse::(), - Ok(PositionEncoding::Utf32) + position_encoding_from_lsp(&lsp_types::PositionEncodingKind::new("utf-32")), + Some(PositionEncoding::Utf32) ); - // Invalid encoding returns error - assert!("invalid".parse::().is_err()); - assert!("UTF-8".parse::().is_err()); // case sensitive - - // Display produces correct strings - assert_eq!(PositionEncoding::Utf8.to_string(), "utf-8"); - assert_eq!(PositionEncoding::Utf16.to_string(), "utf-16"); - assert_eq!(PositionEncoding::Utf32.to_string(), "utf-32"); - } - - #[test] - fn test_lsp_type_conversions() { - // TryFrom for valid encodings - assert_eq!( - PositionEncoding::try_from(PositionEncodingKind::new("utf-8")), - Ok(PositionEncoding::Utf8) - ); + // Invalid encoding returns None assert_eq!( - PositionEncoding::try_from(PositionEncodingKind::new("utf-16")), - Ok(PositionEncoding::Utf16) + position_encoding_from_lsp(&lsp_types::PositionEncodingKind::new("unknown")), + None ); - assert_eq!( - PositionEncoding::try_from(PositionEncodingKind::new("utf-32")), - Ok(PositionEncoding::Utf32) - ); - - // Invalid encoding returns error - assert!(PositionEncoding::try_from(PositionEncodingKind::new("unknown")).is_err()); - // From produces correct LSP types + // position_encoding_to_lsp produces correct LSP types assert_eq!( - PositionEncodingKind::from(PositionEncoding::Utf8).as_str(), + position_encoding_to_lsp(PositionEncoding::Utf8).as_str(), "utf-8" ); assert_eq!( - PositionEncodingKind::from(PositionEncoding::Utf16).as_str(), + position_encoding_to_lsp(PositionEncoding::Utf16).as_str(), "utf-16" ); assert_eq!( - PositionEncodingKind::from(PositionEncoding::Utf32).as_str(), + position_encoding_to_lsp(PositionEncoding::Utf32).as_str(), "utf-32" ); } #[test] fn test_negotiate_prefers_utf8_when_all_available() { - let params = InitializeParams { + let params = lsp_types::InitializeParams { capabilities: ClientCapabilities { general: Some(GeneralClientCapabilities { position_encodings: Some(vec![ - PositionEncodingKind::new("utf-16"), - PositionEncodingKind::new("utf-8"), - PositionEncodingKind::new("utf-32"), + lsp_types::PositionEncodingKind::new("utf-16"), + lsp_types::PositionEncodingKind::new("utf-8"), + lsp_types::PositionEncodingKind::new("utf-32"), ]), ..Default::default() }), @@ -168,17 +110,17 @@ mod tests { ..Default::default() }; - assert_eq!(PositionEncoding::negotiate(¶ms), PositionEncoding::Utf8); + assert_eq!(negotiate_position_encoding(¶ms), PositionEncoding::Utf8); } #[test] fn test_negotiate_prefers_utf32_over_utf16() { - let params = InitializeParams { + let params = lsp_types::InitializeParams { capabilities: ClientCapabilities { general: Some(GeneralClientCapabilities { position_encodings: Some(vec![ - PositionEncodingKind::new("utf-16"), - PositionEncodingKind::new("utf-32"), + lsp_types::PositionEncodingKind::new("utf-16"), + lsp_types::PositionEncodingKind::new("utf-32"), ]), ..Default::default() }), @@ -188,17 +130,17 @@ mod tests { }; assert_eq!( - PositionEncoding::negotiate(¶ms), + negotiate_position_encoding(¶ms), PositionEncoding::Utf32 ); } #[test] fn test_negotiate_accepts_utf16_when_only_option() { - let params = InitializeParams { + let params = lsp_types::InitializeParams { capabilities: ClientCapabilities { general: Some(GeneralClientCapabilities { - position_encodings: Some(vec![PositionEncodingKind::new("utf-16")]), + position_encodings: Some(vec![lsp_types::PositionEncodingKind::new("utf-16")]), ..Default::default() }), ..Default::default() @@ -207,14 +149,14 @@ mod tests { }; assert_eq!( - PositionEncoding::negotiate(¶ms), + negotiate_position_encoding(¶ms), PositionEncoding::Utf16 ); } #[test] fn test_negotiate_fallback_with_empty_encodings() { - let params = InitializeParams { + let params = lsp_types::InitializeParams { capabilities: ClientCapabilities { general: Some(GeneralClientCapabilities { position_encodings: Some(vec![]), @@ -226,28 +168,28 @@ mod tests { }; assert_eq!( - PositionEncoding::negotiate(¶ms), + negotiate_position_encoding(¶ms), PositionEncoding::Utf16 ); } #[test] fn test_negotiate_fallback_with_no_capabilities() { - let params = InitializeParams::default(); + let params = lsp_types::InitializeParams::default(); assert_eq!( - PositionEncoding::negotiate(¶ms), + negotiate_position_encoding(¶ms), PositionEncoding::Utf16 ); } #[test] fn test_negotiate_fallback_with_unknown_encodings() { - let params = InitializeParams { + let params = lsp_types::InitializeParams { capabilities: ClientCapabilities { general: Some(GeneralClientCapabilities { position_encodings: Some(vec![ - PositionEncodingKind::new("utf-7"), - PositionEncodingKind::new("ascii"), + lsp_types::PositionEncodingKind::new("utf-7"), + lsp_types::PositionEncodingKind::new("ascii"), ]), ..Default::default() }), @@ -257,7 +199,7 @@ mod tests { }; assert_eq!( - PositionEncoding::negotiate(¶ms), + negotiate_position_encoding(¶ms), PositionEncoding::Utf16 ); } diff --git a/crates/djls-workspace/src/lib.rs b/crates/djls-workspace/src/lib.rs index d28a48e9..dffc8af9 100644 --- a/crates/djls-workspace/src/lib.rs +++ b/crates/djls-workspace/src/lib.rs @@ -23,8 +23,11 @@ mod workspace; pub use buffers::Buffers; pub use db::Db; +pub use djls_source::PositionEncoding; pub use document::TextDocument; -pub use encoding::PositionEncoding; +pub use encoding::negotiate_position_encoding; +pub use encoding::position_encoding_from_lsp; +pub use encoding::position_encoding_to_lsp; pub use fs::FileSystem; pub use fs::InMemoryFileSystem; pub use fs::OsFileSystem; diff --git a/crates/djls-workspace/src/workspace.rs b/crates/djls-workspace/src/workspace.rs index 500be313..99a5d698 100644 --- a/crates/djls-workspace/src/workspace.rs +++ b/crates/djls-workspace/src/workspace.rs @@ -10,6 +10,7 @@ use camino::Utf8Path; use camino::Utf8PathBuf; use dashmap::DashMap; use djls_source::File; +use djls_source::PositionEncoding; use tower_lsp_server::lsp_types::TextDocumentContentChangeEvent; use url::Url; @@ -110,7 +111,7 @@ impl Workspace { url: &Url, changes: Vec, version: i32, - encoding: crate::encoding::PositionEncoding, + encoding: PositionEncoding, ) -> Option { if let Some(mut document) = self.buffers.get(url) { document.update(changes, version, encoding); @@ -208,7 +209,6 @@ mod tests { use url::Url; use super::*; - use crate::encoding::PositionEncoding; use crate::LanguageId; #[salsa::db]