Files
ARCADIA-Studio/crates/oa-desktop/src/source_navigation/query.rs
T

1132 lines
35 KiB
Rust

//! Owned, bounded queries over exact current source buffers.
//!
//! Includes are inspected from these bytes only. A previously validated bundle
//! is an equality witness, never a source of missing text or navigation spans.
//! The caller must advance the workspace key on every indexed-input change and
//! reject asynchronous results whose key, query serial or byte readback changed.
use super::symbols::{self, Index};
use oa_core::Id;
use oa_core::language::{self, OutlineLimits, Span, TokenClass, TokenSpan};
use oa_core::source_project::{self, IncludeLocation, ProjectLimits};
use std::collections::BTreeMap;
pub use super::symbols::{Evidence, Identity, Membership, Position, SnapshotKey};
#[derive(Debug, Clone, Copy)]
pub struct DraftFile<'a> {
pub path: &'a str,
pub text: &'a str,
}
/// The caller asserts that this complete source bundle passed explicit semantic
/// validation. Capture independently compares its entry, membership and bytes.
#[derive(Debug, Clone, Copy)]
pub struct ValidatedSources<'a> {
pub entry: &'a str,
pub files: &'a [DraftFile<'a>],
}
#[derive(Debug, Clone, Copy)]
pub struct Input<'a> {
pub key: SnapshotKey,
pub entry: &'a str,
pub files: &'a [DraftFile<'a>],
pub applied: bool,
pub validation: Option<ValidatedSources<'a>>,
}
#[derive(Debug, Clone, Copy)]
pub struct Limits {
pub index: symbols::Limits,
pub include_depth: usize,
pub include_edges: usize,
pub result_rows: usize,
/// Upper bound on copied result strings, including quoted name input before
/// JSON decoding. Row/vector overhead is separately bounded by result_rows.
pub result_bytes: usize,
}
impl Default for Limits {
fn default() -> Self {
Self {
index: symbols::Limits {
files: 256,
source_bytes: 64 * 1024 * 1024,
path_bytes: 64 * 1024,
tokens: 1_000_000,
records: 250_000,
},
include_depth: 32,
include_edges: 1024,
result_rows: 200,
result_bytes: 256 * 1024,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AdmissionError {
Index(symbols::BuildError),
Outline {
file: usize,
error: language::OutlineLimit,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ScopeIssueKind {
MissingEntry,
InvalidIncludes,
MissingInclude,
IncludeCycle,
IncludeDepth,
IncludeBudget,
}
/// File indices refer to Snapshot::file. Include spans retain the original
/// quoted include location; diagnostics do not copy unbounded source strings.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ScopeIssue {
pub kind: ScopeIssueKind,
pub file: Option<usize>,
pub span: Span,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Scope {
pub closure_complete: bool,
pub applied: bool,
pub validated: bool,
pub included_files: usize,
pub detached_files: usize,
pub unverified_files: usize,
pub issues: Vec<ScopeIssue>,
}
impl Scope {
pub fn partial(&self) -> bool {
!self.closure_complete || !self.applied
}
fn index_scope(&self) -> symbols::Scope {
if !self.applied {
symbols::Scope::Partial(symbols::PartialReason::UnappliedWorkspace)
} else if !self.closure_complete {
let missing = self.issues.iter().any(|issue| {
matches!(
issue.kind,
ScopeIssueKind::MissingEntry | ScopeIssueKind::MissingInclude
)
});
symbols::Scope::Partial(if missing {
symbols::PartialReason::MissingFiles
} else {
symbols::PartialReason::IncludesUnverified
})
} else {
symbols::Scope::CompleteProject
}
}
}
#[derive(Debug)]
struct File {
path: String,
text: String,
tokens: Vec<TokenSpan>,
declarations: Vec<symbols::DeclarationInput>,
membership: Membership,
evidence: Evidence,
}
/// Immutable and Send: callers can retain it in an Arc for asynchronous work.
/// No Workspace, accepted Project, filesystem handle or mutation is retained.
#[derive(Debug)]
pub struct Snapshot {
key: SnapshotKey,
entry: String,
files: Vec<File>,
scope: Scope,
limits: Limits,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct QueryStamp {
pub snapshot: SnapshotKey,
pub serial: u64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Action {
Definition,
References,
}
#[derive(Debug, Clone, Copy)]
pub struct Query<'a> {
pub stamp: QueryStamp,
pub path: &'a str,
pub byte_offset: usize,
pub action: Action,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Disposition {
NoSymbol,
NoDefinition,
/// All candidates require an explicit choice, including a sole draft or
/// detached candidate. Truncation can never imply unique resolution.
Choices,
Direct,
LexicalReferences,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Declaration {
pub alias: String,
pub kind: String,
pub name: Option<String>,
pub span: Span,
pub authored_id: Option<Id>,
pub identity: Identity,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DefinitionRow {
pub path: String,
/// The alias token to select, rather than an enclosing declaration body.
pub span: Span,
pub position: Position,
pub end_position: Position,
pub declaration: Declaration,
pub membership: Membership,
pub evidence: Evidence,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ReferenceRow {
pub path: String,
pub alias: String,
pub span: Span,
/// Separate live @ token: intervening comments are never selected as a hit.
pub marker: Span,
pub position: Position,
pub end_position: Position,
/// Innermost tolerant declaration containing this occurrence, if available.
/// This describes its lexical location, not its semantic target or owner.
pub context: Option<Declaration>,
pub membership: Membership,
pub evidence: Evidence,
}
/// Exact lexical occurrence that answered the query, independent of row limits.
/// The optional @ marker is separate from the alias; comments between them do
/// not become part of either hit.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Origin {
pub span: Span,
pub marker: Option<Span>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Results {
pub stamp: QueryStamp,
pub action: Action,
pub origin_path: String,
pub origin_offset: usize,
pub origin: Option<Origin>,
pub alias: Option<String>,
pub scope: Scope,
pub disposition: Disposition,
pub definitions: Vec<DefinitionRow>,
pub references: Vec<ReferenceRow>,
pub total_definitions: usize,
pub total_references: usize,
pub omitted_definitions: usize,
pub omitted_references: usize,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum QueryError {
Index(symbols::BuildError),
Query(symbols::QueryError),
MissingFile,
ResultEnvelopeLimit,
}
impl From<symbols::QueryError> for QueryError {
fn from(error: symbols::QueryError) -> Self {
Self::Query(error)
}
}
fn charge(
total: &mut usize,
value: usize,
limit: usize,
resource: symbols::Resource,
) -> Result<(), AdmissionError> {
let next =
total
.checked_add(value)
.ok_or(AdmissionError::Index(symbols::BuildError::Limit {
resource,
limit,
observed_at_least: usize::MAX,
}))?;
if next > limit {
return Err(AdmissionError::Index(symbols::BuildError::Limit {
resource,
limit,
observed_at_least: next,
}));
}
*total = next;
Ok(())
}
/// No path normalization, outline extraction, accepted-source copy or index
/// allocation takes place until aggregate raw input admission succeeds.
fn admit_raw(input: Input<'_>, limits: Limits) -> Result<(), AdmissionError> {
charge(
&mut 0,
input.files.len(),
limits.index.files,
symbols::Resource::Files,
)?;
let mut bytes = 0;
let mut paths = 0;
charge(
&mut paths,
input.entry.len(),
limits.index.path_bytes,
symbols::Resource::PathBytes,
)?;
for file in input.files {
charge(
&mut bytes,
file.text.len(),
limits.index.source_bytes,
symbols::Resource::SourceBytes,
)?;
charge(
&mut paths,
file.path.len(),
limits.index.path_bytes,
symbols::Resource::PathBytes,
)?;
}
Ok(())
}
struct Extracted {
tokens: Vec<TokenSpan>,
declarations: Vec<symbols::DeclarationInput>,
}
#[derive(Debug)]
struct OwnedFile {
path: String,
text: String,
}
#[derive(Debug, Default)]
struct ValidationProof {
admitted: bool,
file_count: usize,
matches: Vec<bool>,
}
impl ValidationProof {
fn capture(input: Input<'_>, limits: Limits) -> Self {
let Some(validation) = input.validation else {
return Self::default();
};
if validation.entry != input.entry
|| admit_raw(
Input {
files: validation.files,
entry: validation.entry,
validation: None,
..input
},
limits,
)
.is_err()
{
return Self::default();
}
let files: BTreeMap<_, _> = validation
.files
.iter()
.map(|file| (file.path, file.text))
.collect();
if files.len() != validation.files.len() {
return Self::default();
}
Self {
admitted: true,
file_count: files.len(),
matches: input
.files
.iter()
.map(|file| files.get(file.path).is_some_and(|text| *text == file.text))
.collect(),
}
}
fn matches(&self, memberships: &[Membership], scope: &Scope) -> bool {
self.admitted
&& scope.closure_complete
&& scope.applied
&& self.file_count == scope.included_files
&& self.matches.len() == memberships.len()
&& memberships
.iter()
.zip(&self.matches)
.all(|(membership, equal)| *membership != Membership::Included || *equal)
}
}
/// Request-time data for spawn_blocking: raw admission plus one current-text
/// clone, with no lexer/parser work. The private equality proof contains only
/// booleans/counts, never a second accepted SourceBundle. These private fields
/// prevent changing the bytes after their equality evidence was captured.
#[derive(Debug)]
pub struct OwnedInput {
key: SnapshotKey,
entry: String,
files: Vec<OwnedFile>,
applied: bool,
validation: ValidationProof,
limits: Limits,
}
pub fn admit_owned(input: Input<'_>, limits: Limits) -> Result<OwnedInput, AdmissionError> {
admit_raw(input, limits)?;
let validation = ValidationProof::capture(input, limits);
Ok(OwnedInput {
key: input.key,
entry: input.entry.to_owned(),
files: input
.files
.iter()
.map(|file| OwnedFile {
path: file.path.to_owned(),
text: file.text.to_owned(),
})
.collect(),
applied: input.applied,
validation,
limits,
})
}
impl OwnedInput {
/// Run on the blocking worker. Token/declaration/reference budgets are
/// enforced before extraction/index allocation, then strings are moved into
/// Snapshot without another copy. A limit error never returns partial data.
pub fn capture(self) -> Result<Snapshot, AdmissionError> {
let files: Vec<_> = self
.files
.iter()
.map(|file| DraftFile {
path: &file.path,
text: &file.text,
})
.collect();
let input = Input {
key: self.key,
entry: &self.entry,
files: &files,
applied: self.applied,
validation: None,
};
let prepared = prepare(input, self.limits, Some(&self.validation))?;
Ok(finish(
self.key,
self.entry,
self.files,
self.limits,
prepared,
))
}
}
struct Prepared {
extracted: Vec<Extracted>,
memberships: Vec<Membership>,
scope: Scope,
}
#[cfg(test)]
pub fn capture(input: Input<'_>, limits: Limits) -> Result<Snapshot, AdmissionError> {
let prepared = prepare(input, limits, None)?;
let files = input
.files
.iter()
.map(|file| OwnedFile {
path: file.path.to_owned(),
text: file.text.to_owned(),
})
.collect();
Ok(finish(
input.key,
input.entry.to_owned(),
files,
limits,
prepared,
))
}
fn prepare(
input: Input<'_>,
limits: Limits,
proof: Option<&ValidationProof>,
) -> Result<Prepared, AdmissionError> {
admit_raw(input, limits)?;
let mut extracted = Vec::with_capacity(input.files.len());
let mut tokens_left = limits.index.tokens;
let mut records_left = limits.index.records;
for (file, draft) in input.files.iter().enumerate() {
// Remaining aggregate budgets are passed before this file is lexed.
// A stopped outline returns an error, never a complete zero-result file.
let outline = language::outline(
draft.text,
OutlineLimits {
max_source_bytes: limits.index.source_bytes,
max_tokens: tokens_left,
max_declarations: records_left,
},
)
.map_err(|error| AdmissionError::Outline { file, error })?;
tokens_left -= outline.tokens.len();
records_left -= outline.declarations.len();
extracted.push(Extracted {
tokens: outline.tokens,
declarations: outline
.declarations
.into_iter()
.map(|declaration| symbols::DeclarationInput {
span: declaration.declaration.span,
kind_span: declaration.kind_span,
alias_span: declaration.alias_span,
identity: declaration
.declaration
.id
.zip(declaration.id_literal_span)
.map(|(id, literal_span)| symbols::AuthoredIdentity { id, literal_span }),
})
.collect(),
});
}
// Reuse the reviewed live-reference counter and structural admission, rather
// than maintain a second lexer/reference recognizer. No text has been copied.
let inputs: Vec<_> = input
.files
.iter()
.zip(&extracted)
.map(|(file, outline)| symbols::FileInput {
path: file.path,
text: file.text,
tokens: &outline.tokens,
declarations: &outline.declarations,
syntax: symbols::SyntaxCoverage::FullFile,
membership: Membership::Unverified,
evidence: Evidence::Draft,
})
.collect();
Index::build(
input.key,
&inputs,
symbols::Scope::Partial(symbols::PartialReason::IncludesUnverified),
limits.index,
)
.map_err(AdmissionError::Index)?;
let (memberships, mut scope) = inspect_closure(input, limits);
scope.validated = proof.map_or_else(
|| validation_matches(input, &memberships, &scope, limits),
|proof| proof.matches(&memberships, &scope),
);
Ok(Prepared {
extracted,
memberships,
scope,
})
}
fn finish(
key: SnapshotKey,
entry: String,
files: Vec<OwnedFile>,
limits: Limits,
prepared: Prepared,
) -> Snapshot {
let Prepared {
extracted,
memberships,
scope,
} = prepared;
let files = files
.into_iter()
.zip(extracted)
.zip(memberships)
.map(|((draft, outline), membership)| File {
path: draft.path,
text: draft.text,
tokens: outline.tokens,
declarations: outline.declarations,
membership,
evidence: if scope.validated && membership == Membership::Included {
Evidence::Validated
} else {
Evidence::Draft
},
})
.collect();
Snapshot {
key,
entry,
files,
scope,
limits,
}
}
fn validation_matches(
input: Input<'_>,
memberships: &[Membership],
scope: &Scope,
limits: Limits,
) -> bool {
if !scope.closure_complete || !input.applied {
return false;
}
ValidationProof::capture(input, limits).matches(memberships, scope)
}
struct Frame {
file: usize,
depth: usize,
includes: std::vec::IntoIter<IncludeLocation>,
}
fn inspect_closure(input: Input<'_>, limits: Limits) -> (Vec<Membership>, Scope) {
let paths: BTreeMap<_, _> = input
.files
.iter()
.enumerate()
.map(|(index, file)| (file.path, index))
.collect();
let mut memberships = vec![Membership::Unverified; input.files.len()];
let mut states = vec![0_u8; input.files.len()];
let mut issues = Vec::new();
let mut stack: Vec<Frame> = Vec::new();
let mut edges_left = limits.include_edges;
let mut pending = paths.get(input.entry).copied().map(|file| (file, 0, None));
if pending.is_none() {
issues.push(ScopeIssue {
kind: ScopeIssueKind::MissingEntry,
file: None,
span: Span::default(),
});
}
loop {
if let Some((file, depth, origin)) = pending.take() {
memberships[file] = Membership::Included;
let (at_file, at_span) = origin.unwrap_or((file, Span::default()));
let problem = if states[file] == 1 {
Some(ScopeIssueKind::IncludeCycle)
} else if depth > limits.include_depth {
Some(ScopeIssueKind::IncludeDepth)
} else {
None
};
if let Some(kind) = problem {
issues.push(ScopeIssue {
kind,
file: Some(at_file),
span: at_span,
});
} else if states[file] == 0 {
states[file] = 1;
let draft = input.files[file];
let project_limits = ProjectLimits {
max_files: limits.index.files,
max_include_depth: limits.include_depth,
max_include_edges: edges_left,
model: language::ParseLimits {
max_source_bytes: limits.index.source_bytes,
max_graph_objects: limits.index.records,
max_records: limits.index.records,
max_step_references: limits.index.records,
},
};
match source_project::inspect_includes(
draft.path,
draft.text,
draft.path == input.entry,
project_limits,
) {
Ok(includes) => {
edges_left -= includes.len();
stack.push(Frame {
file,
depth,
includes: includes.into_iter(),
});
}
Err(diagnostics) => {
let first = diagnostics.first();
let kind = if first.is_some_and(|d| d.diagnostic.code == "project_limit") {
ScopeIssueKind::IncludeBudget
} else {
ScopeIssueKind::InvalidIncludes
};
issues.push(ScopeIssue {
kind,
file: Some(file),
span: first.map_or(Span::default(), |d| d.diagnostic.span),
});
states[file] = 2;
}
}
}
}
let Some(frame) = stack.last_mut() else {
break;
};
if let Some(include) = frame.includes.next() {
if let Some(&file) = paths.get(include.target.as_str()) {
pending = Some((
file,
frame.depth.saturating_add(1),
Some((frame.file, include.location.span)),
));
} else {
issues.push(ScopeIssue {
kind: ScopeIssueKind::MissingInclude,
file: Some(frame.file),
span: include.location.span,
});
}
} else {
states[frame.file] = 2;
stack.pop();
}
}
let complete = issues.is_empty();
if complete {
for membership in &mut memberships {
if *membership == Membership::Unverified {
*membership = Membership::Detached;
}
}
}
let scope = Scope {
closure_complete: complete,
applied: input.applied,
validated: false,
included_files: memberships
.iter()
.filter(|m| **m == Membership::Included)
.count(),
detached_files: memberships
.iter()
.filter(|m| **m == Membership::Detached)
.count(),
unverified_files: memberships
.iter()
.filter(|m| **m == Membership::Unverified)
.count(),
issues,
};
(memberships, scope)
}
impl Snapshot {
pub fn key(&self) -> SnapshotKey {
self.key
}
#[cfg(test)]
pub fn scope(&self) -> &Scope {
&self.scope
}
#[cfg(test)]
pub fn file(&self, index: usize) -> Option<DraftFile<'_>> {
self.files.get(index).map(|file| DraftFile {
path: &file.path,
text: &file.text,
})
}
/// Exact loaded file-set/byte comparison, independent of input order. The
/// evidence outcome is rechecked too; irrelevant old witness text is not kept.
/// The key check is mandatory even if bytes happen to match after Undo.
pub fn matches(&self, input: Input<'_>) -> bool {
if input.key != self.key
|| input.entry != self.entry
|| input.applied != self.scope.applied
|| input.files.len() != self.files.len()
|| admit_raw(input, self.limits).is_err()
{
return false;
}
let mut memberships = Vec::with_capacity(input.files.len());
for draft in input.files {
let Some(file) = self
.files
.iter()
.find(|file| file.path == draft.path && file.text == draft.text)
else {
return false;
};
// Duplicate paths are not the same file set, even if their bytes match.
if input
.files
.iter()
.filter(|other| other.path == draft.path)
.count()
!= 1
{
return false;
}
memberships.push(file.membership);
}
validation_matches(input, &memberships, &self.scope, self.limits) == self.scope.validated
}
fn index(&self) -> Result<Index<'_>, QueryError> {
let inputs: Vec<_> = self
.files
.iter()
.map(|file| symbols::FileInput {
path: &file.path,
text: &file.text,
tokens: &file.tokens,
declarations: &file.declarations,
syntax: symbols::SyntaxCoverage::FullFile,
membership: file.membership,
evidence: file.evidence,
})
.collect();
Index::build(
self.key,
&inputs,
self.scope.index_scope(),
self.limits.index,
)
.map_err(QueryError::Index)
}
pub fn query(&self, query: Query<'_>) -> Result<Results, QueryError> {
if query.stamp.snapshot != self.key {
return Err(symbols::QueryError::StaleSnapshot.into());
}
let file = self
.files
.iter()
.position(|file| file.path == query.path)
.ok_or(QueryError::MissingFile)?;
let index = self.index()?;
let hit = index.hit(file, query.byte_offset)?;
let (alias, origin) = match hit {
Some(symbols::Hit::Definition(handle)) => {
let definition = index.definition(handle)?;
(
Some(definition.alias),
Some(Origin {
span: definition.alias_location.span,
marker: None,
}),
)
}
Some(symbols::Hit::Reference(handle)) => {
let reference = index.reference(handle)?;
(
Some(reference.alias),
Some(Origin {
span: reference.alias_location.span,
marker: Some(reference.at.span),
}),
)
}
None => (None, None),
};
let mut budget = RowBudget {
rows: self.limits.result_rows,
bytes: self.limits.result_bytes,
};
if !budget.strings(query.path.len().saturating_add(alias.map_or(0, str::len))) {
return Err(QueryError::ResultEnvelopeLimit);
}
let mut result = Results {
stamp: query.stamp,
action: query.action,
origin_path: query.path.to_owned(),
origin_offset: query.byte_offset,
origin,
alias: alias.map(str::to_owned),
scope: self.scope.clone(),
disposition: Disposition::NoSymbol,
definitions: Vec::new(),
references: Vec::new(),
total_definitions: 0,
total_references: 0,
omitted_definitions: 0,
omitted_references: 0,
};
let Some(alias) = alias else {
return Ok(result);
};
result.total_definitions = index
.definitions_for_alias(alias, symbols::Domain::Loaded)
.count();
for handle in index.definitions_for_alias(alias, symbols::Domain::Loaded) {
let definition = index.definition(handle)?;
let source = &self.files[definition.declaration.file];
let display = self.display(&index, handle)?;
if !budget.row(source.path.len().saturating_add(display.bytes())) {
break;
}
result.definitions.push(DefinitionRow {
path: source.path.clone(),
span: definition.alias_location.span,
position: Position {
line: 0,
byte_column: 0,
},
end_position: Position {
line: 0,
byte_column: 0,
},
declaration: display.owned(),
membership: source.membership,
evidence: source.evidence,
});
}
result.omitted_definitions = result.total_definitions - result.definitions.len();
result.disposition = if result.total_definitions == 0 {
Disposition::NoDefinition
} else {
Disposition::Choices
};
if query.action == Action::Definition
&& result.total_definitions == 1
&& result.definitions.len() == 1
&& self.scope.validated
&& self.files[file].membership == Membership::Included
{
let only = &result.definitions[0];
if only.membership == Membership::Included
&& only.evidence == Evidence::Validated
&& !matches!(only.declaration.identity, Identity::Collision { .. })
{
result.disposition = Disposition::Direct;
}
}
if query.action == Action::References {
result.disposition = Disposition::LexicalReferences;
result.total_references = index
.references_for_alias(alias, symbols::Domain::Loaded)
.count();
for handle in index.references_for_alias(alias, symbols::Domain::Loaded) {
let reference = index.reference(handle)?;
let source = &self.files[reference.alias_location.file];
let context = self.context(&index, reference.alias_location)?;
let bytes = source
.path
.len()
.saturating_add(alias.len())
.saturating_add(context.as_ref().map_or(0, Display::bytes));
if !budget.row(bytes) {
break;
}
result.references.push(ReferenceRow {
path: source.path.clone(),
alias: alias.to_owned(),
span: reference.alias_location.span,
marker: reference.at.span,
position: Position {
line: 0,
byte_column: 0,
},
end_position: Position {
line: 0,
byte_column: 0,
},
context: context.map(Display::owned),
membership: source.membership,
evidence: source.evidence,
});
}
result.omitted_references = result.total_references - result.references.len();
}
self.locate_rows(&mut result);
Ok(result)
}
fn locate_rows(&self, result: &mut Results) {
// Index admission already proved every row span and UTF-8 boundary.
// Scan each selected source prefix once instead of rescanning a large
// buffer for every occurrence. Scratch has at most two entries per row.
for file in &self.files {
let mut offsets = Vec::new();
for (row, definition) in result.definitions.iter().enumerate() {
if definition.path == file.path {
offsets.push((definition.span.start, true, row, false));
offsets.push((definition.span.end, true, row, true));
}
}
for (row, reference) in result.references.iter().enumerate() {
if reference.path == file.path {
offsets.push((reference.span.start, false, row, false));
offsets.push((reference.span.end, false, row, true));
}
}
offsets.sort_unstable_by_key(|&(offset, _, _, _)| offset);
let (mut scanned, mut line, mut line_start) = (0, 0, 0);
for (offset, definition, row, end) in offsets {
for (relative, byte) in file.text.as_bytes()[scanned..offset].iter().enumerate() {
if *byte == b'\n' {
line += 1;
line_start = scanned + relative + 1;
}
}
scanned = offset;
let position = Position {
line,
byte_column: offset - line_start,
};
match (definition, end) {
(true, false) => result.definitions[row].position = position,
(true, true) => result.definitions[row].end_position = position,
(false, false) => result.references[row].position = position,
(false, true) => result.references[row].end_position = position,
}
}
}
}
fn display<'a>(
&'a self,
index: &Index<'a>,
handle: symbols::DefinitionHandle,
) -> Result<Display<'a>, QueryError> {
let definition = index.definition(handle)?;
let file = &self.files[definition.declaration.file];
let alias_end = definition.alias_location.span.end;
let next = file
.tokens
.partition_point(|token| token.span.start < alias_end);
// The coordinated outline has already proved the declaration and alias
// roles. This optional decoration only decodes its immediate name token.
let name = (definition.kind != "node")
.then(|| {
file.tokens[next..].iter().find(|token| {
!matches!(token.class, TokenClass::Whitespace | TokenClass::Comment)
})
})
.flatten()
.filter(|token| {
token.class == TokenClass::String
&& token.span.end <= definition.declaration.span.end
})
.map(|token| &file.text[token.span.start..token.span.end]);
Ok(Display {
alias: definition.alias,
kind: definition.kind,
quoted_name: name,
span: definition.declaration.span,
authored_id: definition.authored_id,
identity: index.identity(handle)?,
})
}
fn context<'a>(
&'a self,
index: &Index<'a>,
location: symbols::Location,
) -> Result<Option<Display<'a>>, QueryError> {
let file = &self.files[location.file];
let inner = file
.declarations
.iter()
.filter(|declaration| {
declaration.span.start <= location.span.start
&& location.span.end <= declaration.span.end
})
.min_by_key(|declaration| declaration.span.end - declaration.span.start);
let Some(inner) = inner else {
return Ok(None);
};
let Some(symbols::Hit::Definition(handle)) =
index.hit(location.file, inner.alias_span.start)?
else {
return Ok(None);
};
self.display(index, handle).map(Some)
}
}
struct Display<'a> {
alias: &'a str,
kind: &'a str,
quoted_name: Option<&'a str>,
span: Span,
authored_id: Option<Id>,
identity: Identity,
}
impl Display<'_> {
fn bytes(&self) -> usize {
self.alias
.len()
.saturating_add(self.kind.len())
.saturating_add(self.quoted_name.map_or(0, str::len))
}
fn owned(self) -> Declaration {
Declaration {
alias: self.alias.to_owned(),
kind: self.kind.to_owned(),
name: self
.quoted_name
.and_then(|name| serde_json::from_str(name).ok()),
span: self.span,
authored_id: self.authored_id,
identity: self.identity,
}
}
}
struct RowBudget {
rows: usize,
bytes: usize,
}
impl RowBudget {
fn strings(&mut self, bytes: usize) -> bool {
if bytes > self.bytes {
return false;
}
self.bytes -= bytes;
true
}
fn row(&mut self, bytes: usize) -> bool {
if self.rows == 0 || !self.strings(bytes) {
return false;
}
self.rows -= 1;
true
}
}
#[cfg(test)]
#[path = "query_tests.rs"]
mod tests;