use lintspec_core::textindex::{TextIndex, compute_indices};
use serde_json::Value;
use std::sync::OnceLock;
use tower_lsp::lsp_types::{Position, PositionEncodingKind};
static ENCODING: OnceLock<PositionEncoding> = OnceLock::new();
pub fn set_encoding(enc: PositionEncoding) {
let _ = ENCODING.set(enc);
}
pub fn encoding() -> PositionEncoding {
ENCODING.get().copied().unwrap_or_default()
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum PositionEncoding {
Utf8,
#[default]
Utf16,
}
impl PositionEncoding {
pub fn negotiate(client_encodings: Option<&[PositionEncodingKind]>) -> Self {
let Some(encodings) = client_encodings else {
return Self::default();
};
if encodings.contains(&PositionEncodingKind::UTF8) {
PositionEncoding::Utf8
} else {
PositionEncoding::Utf16
}
}
}
impl From<PositionEncoding> for PositionEncodingKind {
fn from(value: PositionEncoding) -> Self {
match value {
PositionEncoding::Utf8 => PositionEncodingKind::UTF8,
PositionEncoding::Utf16 => PositionEncodingKind::UTF16,
}
}
}
pub fn byte_column_to_position(
source: &str,
row: usize,
byte_column: usize,
byte_offset: usize,
) -> Position {
let column = match encoding() {
PositionEncoding::Utf8 => byte_column as u32,
PositionEncoding::Utf16 => {
let line_start = byte_offset.saturating_sub(byte_column);
source
.get(line_start..byte_offset)
.map_or(byte_column as u32, |prefix| {
prefix.encode_utf16().count() as u32
})
}
};
Position::new(row as u32, column)
}
pub fn byte_offset_to_position(source: &str, byte_offset: usize) -> Position {
if source.is_empty() {
return Position::new(0, 0);
}
let idx = if byte_offset >= source.len() {
let mut ti = TextIndex::ZERO;
let mut chars = source.chars().peekable();
while let Some(c) = chars.next() {
ti.advance(c, chars.peek());
}
ti
} else {
let indices = compute_indices(source, &[byte_offset]);
match indices.first() {
Some(ti) => *ti,
None => return Position::new(0, 0),
}
};
Position {
line: idx.line,
character: match encoding() {
PositionEncoding::Utf8 => idx.col_utf8,
PositionEncoding::Utf16 => idx.col_utf16,
},
}
}
pub fn position_to_byte_offset(source: &str, pos: Position) -> usize {
if source.is_empty() {
return 0;
}
let enc = encoding();
let chunk_offsets: Vec<usize> = (0..source.len()).step_by(128).collect();
let chunk_indices = compute_indices(source, &chunk_offsets);
let start = chunk_indices
.iter()
.take_while(|ti| {
if ti.line < pos.line {
return true;
}
if ti.line == pos.line {
let col = match enc {
PositionEncoding::Utf8 => ti.col_utf8,
PositionEncoding::Utf16 => ti.col_utf16,
};
return col <= pos.character;
}
false
})
.last()
.copied()
.unwrap_or(TextIndex::ZERO);
let mut idx = start;
let mut chars = source[idx.utf8..].chars().peekable();
while let Some(c) = chars.next() {
let col = match enc {
PositionEncoding::Utf8 => idx.col_utf8,
PositionEncoding::Utf16 => idx.col_utf16,
};
if idx.line >= pos.line && col >= pos.character {
return idx.utf8;
}
if idx.line == pos.line && c == '\n' {
return idx.utf8; }
idx.advance(c, chars.peek());
}
source.len() }
pub fn is_valid_solidity_identifier(name: &str) -> bool {
let mut chars = name.chars();
let Some(first) = chars.next() else {
return false;
};
if !first.is_ascii_alphabetic() && first != '_' && first != '$' {
return false;
}
if !chars.all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '$') {
return false;
}
if SOLIDITY_KEYWORDS.contains(&name) {
return false;
}
if is_numeric_type_keyword(name) {
return false;
}
true
}
const SOLIDITY_KEYWORDS: &[&str] = &[
"abstract",
"address",
"anonymous",
"as",
"assembly",
"bool",
"break",
"bytes",
"calldata",
"catch",
"constant",
"constructor",
"continue",
"contract",
"delete",
"do",
"else",
"emit",
"enum",
"event",
"external",
"fallback",
"false",
"fixed",
"for",
"function",
"hex",
"if",
"immutable",
"import",
"indexed",
"interface",
"internal",
"is",
"library",
"mapping",
"memory",
"modifier",
"new",
"override",
"payable",
"pragma",
"private",
"public",
"pure",
"receive",
"return",
"returns",
"storage",
"string",
"struct",
"true",
"try",
"type",
"ufixed",
"unchecked",
"unicode",
"using",
"view",
"virtual",
"while",
"after",
"alias",
"apply",
"auto",
"byte",
"case",
"copyof",
"default",
"define",
"final",
"implements",
"in",
"inline",
"let",
"macro",
"match",
"mutable",
"null",
"of",
"partial",
"promise",
"reference",
"relocatable",
"sealed",
"sizeof",
"static",
"supports",
"switch",
"typedef",
"typeof",
"var",
];
fn is_numeric_type_keyword(name: &str) -> bool {
if let Some(suffix) = name
.strip_prefix("uint")
.or_else(|| name.strip_prefix("int"))
{
if suffix.is_empty() {
return true;
}
let Ok(n) = suffix.parse::<u16>() else {
return false;
};
return (8..=256).contains(&n) && n % 8 == 0;
}
if let Some(suffix) = name.strip_prefix("bytes") {
let Ok(n) = suffix.parse::<u16>() else {
return false;
};
return (1..=32).contains(&n);
}
false
}
pub fn push_if_node_or_array<'a>(tree: &'a Value, key: &str, stack: &mut Vec<&'a Value>) {
if let Some(value) = tree.get(key) {
match value {
Value::Array(arr) => stack.extend(arr),
Value::Object(_) => stack.push(value),
_ => {}
}
}
}