mirror of https://github.com/astral-sh/ruff
314 lines
9.7 KiB
Rust
314 lines
9.7 KiB
Rust
use super::PositionEncoding;
|
|
use crate::system::file_to_url;
|
|
use ty_python_semantic::Db;
|
|
|
|
use lsp_types as types;
|
|
use lsp_types::{Location, Position, Url};
|
|
use ruff_db::files::{File, FileRange};
|
|
use ruff_db::source::{line_index, source_text};
|
|
use ruff_source_file::LineIndex;
|
|
use ruff_source_file::{OneIndexed, SourceLocation};
|
|
use ruff_text_size::{Ranged, TextRange, TextSize};
|
|
|
|
/// Represents a range that has been prepared for LSP conversion but requires
|
|
/// a decision about how to use it - either as a local range within the same
|
|
/// document/cell, or as a location that can reference any document in the project.
|
|
#[derive(Clone)]
|
|
pub(crate) struct LspRange<'db> {
|
|
file: File,
|
|
range: TextRange,
|
|
db: &'db dyn Db,
|
|
encoding: PositionEncoding,
|
|
}
|
|
|
|
impl std::fmt::Debug for LspRange<'_> {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
f.debug_struct("LspRange")
|
|
.field("range", &self.range)
|
|
.field("file", &self.file)
|
|
.field("encoding", &self.encoding)
|
|
.finish_non_exhaustive()
|
|
}
|
|
}
|
|
|
|
impl LspRange<'_> {
|
|
/// Convert to an LSP Range for use within the same document/cell.
|
|
/// Returns only the LSP Range without any URI information.
|
|
///
|
|
/// Use this when you already have a URI context and this range is guaranteed
|
|
/// to be within the same document/cell:
|
|
/// - Selection ranges within a `LocationLink` (where `target_uri` provides context)
|
|
/// - Additional ranges in the same cell (e.g., `selection_range` when you already have `target_range`)
|
|
///
|
|
/// Do NOT use this for standalone ranges - use `to_location()` instead to ensure
|
|
/// the URI and range are consistent.
|
|
pub(crate) fn to_local_range(&self) -> types::Range {
|
|
self.to_uri_and_range().1
|
|
}
|
|
|
|
/// Convert to a Location that can reference any document.
|
|
/// Returns a Location with both URI and Range.
|
|
///
|
|
/// Use this for:
|
|
/// - Go-to-definition targets
|
|
/// - References
|
|
/// - Diagnostics related information
|
|
/// - Any cross-file navigation
|
|
pub(crate) fn to_location(&self) -> Option<Location> {
|
|
let (uri, range) = self.to_uri_and_range();
|
|
Some(Location { uri: uri?, range })
|
|
}
|
|
|
|
pub(crate) fn to_uri_and_range(&self) -> (Option<Url>, lsp_types::Range) {
|
|
let source = source_text(self.db, self.file);
|
|
let index = line_index(self.db, self.file);
|
|
|
|
let uri = file_to_url(self.db, self.file);
|
|
let range = text_range_to_lsp_range(self.range, &source, &index, self.encoding);
|
|
(uri, range)
|
|
}
|
|
}
|
|
|
|
/// Represents a position that has been prepared for LSP conversion but requires
|
|
/// a decision about how to use it - either as a local position within the same
|
|
/// document/cell, or as a location with a single-point range that can reference
|
|
/// any document in the project.
|
|
#[derive(Clone)]
|
|
pub(crate) struct LspPosition<'db> {
|
|
file: File,
|
|
position: TextSize,
|
|
db: &'db dyn Db,
|
|
encoding: PositionEncoding,
|
|
}
|
|
|
|
impl std::fmt::Debug for LspPosition<'_> {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
f.debug_struct("LspPosition")
|
|
.field("position", &self.position)
|
|
.field("file", &self.file)
|
|
.field("encoding", &self.encoding)
|
|
.finish_non_exhaustive()
|
|
}
|
|
}
|
|
|
|
impl LspPosition<'_> {
|
|
/// Convert to an LSP Position for use within the same document/cell.
|
|
/// Returns only the LSP Position without any URI information.
|
|
///
|
|
/// Use this when you already have a URI context and this position is guaranteed
|
|
/// to be within the same document/cell:
|
|
/// - Inlay hints (where the document URI is already known)
|
|
/// - Positions within the same cell as a parent range
|
|
///
|
|
/// Do NOT use this for standalone positions that might need a URI - use
|
|
/// `to_location()` instead to ensure the URI and position are consistent.
|
|
pub(crate) fn to_local_position(&self) -> types::Position {
|
|
self.to_location().1
|
|
}
|
|
|
|
/// Convert to a Location with a single-point range that can reference any document.
|
|
/// Returns a Location with both URI and a range where start == end.
|
|
///
|
|
/// Use this for any cross-file navigation where you need both URI and position.
|
|
pub(crate) fn to_location(&self) -> (Option<lsp_types::Url>, Position) {
|
|
let source = source_text(self.db, self.file);
|
|
let index = line_index(self.db, self.file);
|
|
|
|
let uri = file_to_url(self.db, self.file);
|
|
let position = text_size_to_lsp_position(self.position, &source, &index, self.encoding);
|
|
(uri, position)
|
|
}
|
|
}
|
|
|
|
pub(crate) trait RangeExt {
|
|
/// Convert an LSP Range to internal `TextRange`.
|
|
fn to_text_range(
|
|
&self,
|
|
db: &dyn Db,
|
|
file: File,
|
|
url: &lsp_types::Url,
|
|
encoding: PositionEncoding,
|
|
) -> TextRange;
|
|
}
|
|
|
|
impl RangeExt for lsp_types::Range {
|
|
fn to_text_range(
|
|
&self,
|
|
db: &dyn Db,
|
|
file: File,
|
|
url: &lsp_types::Url,
|
|
encoding: PositionEncoding,
|
|
) -> TextRange {
|
|
let start = self.start.to_text_size(db, file, url, encoding);
|
|
let end = self.end.to_text_size(db, file, url, encoding);
|
|
|
|
TextRange::new(start, end)
|
|
}
|
|
}
|
|
|
|
pub(crate) trait PositionExt {
|
|
/// Convert an LSP Position to internal `TextSize`.
|
|
fn to_text_size(
|
|
&self,
|
|
db: &dyn Db,
|
|
file: File,
|
|
url: &lsp_types::Url,
|
|
encoding: PositionEncoding,
|
|
) -> TextSize;
|
|
}
|
|
|
|
impl PositionExt for lsp_types::Position {
|
|
fn to_text_size(
|
|
&self,
|
|
db: &dyn Db,
|
|
file: File,
|
|
_url: &lsp_types::Url,
|
|
encoding: PositionEncoding,
|
|
) -> TextSize {
|
|
let source = source_text(db, file);
|
|
let index = line_index(db, file);
|
|
|
|
lsp_position_to_text_size(*self, &source, &index, encoding)
|
|
}
|
|
}
|
|
|
|
pub(crate) trait TextSizeExt {
|
|
/// Converts this position to an `LspPosition`, which then requires an explicit
|
|
/// decision about how to use it (as a local position or as a location).
|
|
fn as_lsp_position<'db>(
|
|
&self,
|
|
db: &'db dyn Db,
|
|
file: File,
|
|
encoding: PositionEncoding,
|
|
) -> LspPosition<'db>
|
|
where
|
|
Self: Sized;
|
|
}
|
|
|
|
impl TextSizeExt for TextSize {
|
|
fn as_lsp_position<'db>(
|
|
&self,
|
|
db: &'db dyn Db,
|
|
file: File,
|
|
encoding: PositionEncoding,
|
|
) -> LspPosition<'db> {
|
|
LspPosition {
|
|
file,
|
|
position: *self,
|
|
db,
|
|
encoding,
|
|
}
|
|
}
|
|
}
|
|
|
|
pub(crate) trait ToRangeExt {
|
|
/// Converts this range to an `LspRange`, which then requires an explicit
|
|
/// decision about how to use it (as a local range or as a location).
|
|
fn as_lsp_range<'db>(
|
|
&self,
|
|
db: &'db dyn Db,
|
|
file: File,
|
|
encoding: PositionEncoding,
|
|
) -> LspRange<'db>;
|
|
}
|
|
|
|
fn u32_index_to_usize(index: u32) -> usize {
|
|
usize::try_from(index).expect("u32 fits in usize")
|
|
}
|
|
|
|
fn text_size_to_lsp_position(
|
|
offset: TextSize,
|
|
text: &str,
|
|
index: &LineIndex,
|
|
encoding: PositionEncoding,
|
|
) -> types::Position {
|
|
let source_location = index.source_location(offset, text, encoding.into());
|
|
source_location_to_position(&source_location)
|
|
}
|
|
|
|
fn text_range_to_lsp_range(
|
|
range: TextRange,
|
|
text: &str,
|
|
index: &LineIndex,
|
|
encoding: PositionEncoding,
|
|
) -> types::Range {
|
|
types::Range {
|
|
start: text_size_to_lsp_position(range.start(), text, index, encoding),
|
|
end: text_size_to_lsp_position(range.end(), text, index, encoding),
|
|
}
|
|
}
|
|
|
|
/// Helper function to convert an LSP Position to internal `TextSize`.
|
|
/// This is used internally by the `PositionExt` trait and other helpers.
|
|
fn lsp_position_to_text_size(
|
|
position: lsp_types::Position,
|
|
text: &str,
|
|
index: &LineIndex,
|
|
encoding: PositionEncoding,
|
|
) -> TextSize {
|
|
index.offset(
|
|
SourceLocation {
|
|
line: OneIndexed::from_zero_indexed(u32_index_to_usize(position.line)),
|
|
character_offset: OneIndexed::from_zero_indexed(u32_index_to_usize(position.character)),
|
|
},
|
|
text,
|
|
encoding.into(),
|
|
)
|
|
}
|
|
|
|
/// Helper function to convert an LSP Range to internal `TextRange`.
|
|
/// This is used internally by the `RangeExt` trait and in special cases
|
|
/// where `db` and `file` are not available (e.g., when applying document changes).
|
|
pub(crate) fn lsp_range_to_text_range(
|
|
range: lsp_types::Range,
|
|
text: &str,
|
|
index: &LineIndex,
|
|
encoding: PositionEncoding,
|
|
) -> TextRange {
|
|
TextRange::new(
|
|
lsp_position_to_text_size(range.start, text, index, encoding),
|
|
lsp_position_to_text_size(range.end, text, index, encoding),
|
|
)
|
|
}
|
|
|
|
impl ToRangeExt for TextRange {
|
|
fn as_lsp_range<'db>(
|
|
&self,
|
|
db: &'db dyn Db,
|
|
file: File,
|
|
encoding: PositionEncoding,
|
|
) -> LspRange<'db> {
|
|
LspRange {
|
|
file,
|
|
range: *self,
|
|
db,
|
|
encoding,
|
|
}
|
|
}
|
|
}
|
|
|
|
fn source_location_to_position(location: &SourceLocation) -> types::Position {
|
|
types::Position {
|
|
line: u32::try_from(location.line.to_zero_indexed()).expect("line usize fits in u32"),
|
|
character: u32::try_from(location.character_offset.to_zero_indexed())
|
|
.expect("character usize fits in u32"),
|
|
}
|
|
}
|
|
|
|
pub(crate) trait FileRangeExt {
|
|
/// Converts this file range to an `LspRange`, which then requires an explicit
|
|
/// decision about how to use it (as a local range or as a location).
|
|
fn as_lsp_range<'db>(&self, db: &'db dyn Db, encoding: PositionEncoding) -> LspRange<'db>;
|
|
}
|
|
|
|
impl FileRangeExt for FileRange {
|
|
fn as_lsp_range<'db>(&self, db: &'db dyn Db, encoding: PositionEncoding) -> LspRange<'db> {
|
|
LspRange {
|
|
file: self.file(),
|
|
range: self.range(),
|
|
db,
|
|
encoding,
|
|
}
|
|
}
|
|
}
|