Initial public release of Warp.
Repo-Sync-Origin: warpdotdev/warp-internal@12af1d983b
This commit is contained in:
@@ -0,0 +1,25 @@
|
||||
[package]
|
||||
name = "syntax_tree"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
authors = ["Warp Team <dev@warp.dev>"]
|
||||
publish.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[dependencies]
|
||||
rangemap.workspace = true
|
||||
arborium.workspace = true
|
||||
warpui.workspace = true
|
||||
warp_editor.workspace = true
|
||||
languages.workspace = true
|
||||
futures.workspace = true
|
||||
futures-lite.workspace = true
|
||||
parking_lot.workspace = true
|
||||
log.workspace = true
|
||||
string-offset.workspace = true
|
||||
streaming-iterator = "0.1.9"
|
||||
warp_util.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
warp_editor = { workspace = true, features = ["test-util"] }
|
||||
warpui = { workspace = true, features = ["test-util"] }
|
||||
@@ -0,0 +1,388 @@
|
||||
mod queries;
|
||||
use languages::Language;
|
||||
pub use queries::highlight_query::{ColorMap, TextSlice};
|
||||
|
||||
use std::{
|
||||
cell::{Ref, RefCell},
|
||||
collections::{HashMap, HashSet},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use parking_lot::Mutex;
|
||||
|
||||
use arborium::tree_sitter::{InputEdit, Parser, Tree};
|
||||
use futures::stream::AbortHandle;
|
||||
use queries::{
|
||||
highlight_query::HighlightQuery,
|
||||
indent_query::{indentation_delta, IndentDelta},
|
||||
};
|
||||
use rangemap::{RangeMap, RangeSet};
|
||||
use string_offset::{ByteOffset, CharOffset};
|
||||
use warpui::{color::ColorU, AppContext, Entity, ModelContext, WeakModelHandle};
|
||||
|
||||
use warp_editor::{
|
||||
content::{
|
||||
buffer::{Buffer, BufferSnapshot},
|
||||
edit::PreciseDelta,
|
||||
text::IndentUnit,
|
||||
version::BufferVersion,
|
||||
},
|
||||
decoration::DecorationLayer,
|
||||
};
|
||||
use warpui::text::point::Point;
|
||||
|
||||
const MAX_SYNTAX_TREES: usize = 3;
|
||||
|
||||
thread_local! {
|
||||
static PARSER: RefCell<Parser> = RefCell::new(Parser::new());
|
||||
}
|
||||
pub enum DecorationStateEvent {
|
||||
DecorationUpdated { version: BufferVersion },
|
||||
}
|
||||
|
||||
struct LanguageQueries {
|
||||
language: Arc<Language>,
|
||||
syntax_query: HighlightQuery,
|
||||
}
|
||||
|
||||
/// Single-entry cache for highlight queries.
|
||||
/// Stores the most recent highlight computation result.
|
||||
struct HighlightCache {
|
||||
key: HighlightCacheKey,
|
||||
highlights: RangeMap<CharOffset, ColorU>,
|
||||
}
|
||||
|
||||
struct HighlightCacheKey {
|
||||
version: BufferVersion,
|
||||
ranges: RangeSet<CharOffset>,
|
||||
language_id: Option<arborium::tree_sitter::Language>,
|
||||
}
|
||||
|
||||
impl HighlightCacheKey {
|
||||
/// Check if this cache entry matches the given content version, ranges, and language.
|
||||
fn matches(
|
||||
&self,
|
||||
version: BufferVersion,
|
||||
ranges: &RangeSet<CharOffset>,
|
||||
language_id: &Option<arborium::tree_sitter::Language>,
|
||||
) -> bool {
|
||||
if self.version != version {
|
||||
return false;
|
||||
}
|
||||
if &self.language_id != language_id {
|
||||
return false;
|
||||
}
|
||||
// RangeSet derives PartialEq, so we can compare directly
|
||||
&self.ranges == ranges
|
||||
}
|
||||
}
|
||||
|
||||
/// Manages the decoration styles derived from the underlying text source (e.g. syntax highlighting).
|
||||
/// The updates are computed asynchronously and we notify the editor model upon completion via
|
||||
/// DecorationUpdated event.
|
||||
pub struct SyntaxTreeState {
|
||||
syntax_tree: Mutex<HashMap<BufferVersion, Tree>>,
|
||||
language_queries: Option<LanguageQueries>,
|
||||
buffer_version: BufferVersion,
|
||||
color_map: ColorMap,
|
||||
buffer_handle: WeakModelHandle<Buffer>,
|
||||
parsing_handle: Option<AbortHandle>,
|
||||
/// Cache for highlight results to avoid recomputing for the same viewport ranges.
|
||||
highlight_cache: RefCell<Option<HighlightCache>>,
|
||||
}
|
||||
|
||||
impl SyntaxTreeState {
|
||||
pub fn new(
|
||||
buffer_handle: WeakModelHandle<Buffer>,
|
||||
buffer_version: BufferVersion,
|
||||
color_map: ColorMap,
|
||||
) -> Self {
|
||||
Self {
|
||||
color_map,
|
||||
syntax_tree: Mutex::new(HashMap::new()),
|
||||
buffer_version,
|
||||
buffer_handle,
|
||||
parsing_handle: None,
|
||||
language_queries: None,
|
||||
highlight_cache: RefCell::new(None),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_language(&mut self, language: Arc<Language>) {
|
||||
self.language_queries = Some(LanguageQueries {
|
||||
syntax_query: HighlightQuery::new(&language.highlight_query, self.color_map),
|
||||
language,
|
||||
});
|
||||
}
|
||||
|
||||
pub fn has_supported_highlighting(&self) -> bool {
|
||||
self.language_queries.is_some()
|
||||
}
|
||||
|
||||
pub fn indent_unit(&self) -> Option<IndentUnit> {
|
||||
self.language_queries
|
||||
.as_ref()
|
||||
.map(|queries| queries.language.indent_unit)
|
||||
}
|
||||
|
||||
pub fn bracket_pairs(&self) -> Option<&[(char, char)]> {
|
||||
self.language_queries
|
||||
.as_ref()
|
||||
.map(|queries| queries.language.bracket_pairs.as_slice())
|
||||
}
|
||||
|
||||
pub fn comment_prefix(&self) -> Option<&str> {
|
||||
self.language_queries
|
||||
.as_ref()
|
||||
.and_then(|queries| queries.language.comment_prefix.as_ref())
|
||||
.map(|s| s.as_str())
|
||||
}
|
||||
|
||||
/// Given multiple character ranges, return their corresponding highlight colors.
|
||||
/// If the tree is not ready or the buffer model has been deallocated, this returns None.
|
||||
pub fn highlights_in_ranges(
|
||||
&self,
|
||||
ranges: RangeSet<CharOffset>,
|
||||
render_content_version: Option<BufferVersion>,
|
||||
ctx: &AppContext,
|
||||
) -> Option<Ref<'_, RangeMap<CharOffset, ColorU>>> {
|
||||
// If no render content version is provided, default the most recent content version.
|
||||
let buffer_version = render_content_version.unwrap_or(self.buffer_version);
|
||||
|
||||
let language_id = self
|
||||
.language_queries
|
||||
.as_ref()
|
||||
.map(|q| q.language.grammar.clone());
|
||||
|
||||
// Check cache first
|
||||
if let Ok(cache) = Ref::filter_map(self.highlight_cache.borrow(), |c| c.as_ref()) {
|
||||
if cache.key.matches(buffer_version, &ranges, &language_id) {
|
||||
// Return a borrowed reference to the cached highlights
|
||||
return Some(Ref::map(cache, |c| &c.highlights));
|
||||
}
|
||||
}
|
||||
|
||||
// Cache miss - compute highlights
|
||||
let mut syntax_tree_lock = self.syntax_tree.lock();
|
||||
let tree = syntax_tree_lock.get(&buffer_version)?;
|
||||
let buffer = self.buffer_handle.upgrade(ctx)?;
|
||||
let language_queries = self.language_queries.as_ref()?;
|
||||
|
||||
let mut combined_highlights = RangeMap::new();
|
||||
|
||||
// Iterate over all ranges and collect highlights for each
|
||||
for range in ranges.iter() {
|
||||
let highlights = language_queries.syntax_query.get_highlighted_chunks(
|
||||
range.clone(),
|
||||
&language_queries.language.highlight_query,
|
||||
buffer.as_ref(ctx),
|
||||
tree,
|
||||
);
|
||||
|
||||
// Merge the highlights into the combined map
|
||||
for (highlight_range, color) in highlights.iter() {
|
||||
combined_highlights.insert(highlight_range.clone(), *color);
|
||||
}
|
||||
}
|
||||
|
||||
// Once we have rendered content version X, we could discard syntax trees belonging to versions before X.
|
||||
if let Some(render_content_version) = render_content_version {
|
||||
// First, drop any versions older than the rendered one in a single pass.
|
||||
syntax_tree_lock.retain(|version, _| *version >= render_content_version);
|
||||
Self::truncate_tree_state(&mut syntax_tree_lock, self.buffer_version);
|
||||
}
|
||||
|
||||
// Store in cache before returning
|
||||
*self.highlight_cache.borrow_mut() = Some(HighlightCache {
|
||||
key: HighlightCacheKey {
|
||||
version: buffer_version,
|
||||
ranges,
|
||||
language_id,
|
||||
},
|
||||
highlights: combined_highlights,
|
||||
});
|
||||
|
||||
// Return a borrowed reference to the cached highlights
|
||||
Ref::filter_map(self.highlight_cache.borrow(), |c| {
|
||||
c.as_ref().map(|cache| &cache.highlights)
|
||||
})
|
||||
.ok()
|
||||
}
|
||||
|
||||
/// Given a point in buffer, return the absolute indentation level the point should have.
|
||||
pub fn indentation_at_point(&self, point: Point, ctx: &AppContext) -> Option<IndentDelta> {
|
||||
let syntax_tree_lock = self.syntax_tree.lock();
|
||||
let tree = syntax_tree_lock.get(&self.buffer_version)?;
|
||||
let buffer = self.buffer_handle.upgrade(ctx)?;
|
||||
let language_queries = self.language_queries.as_ref()?;
|
||||
|
||||
indentation_delta(
|
||||
buffer.as_ref(ctx),
|
||||
tree,
|
||||
point,
|
||||
language_queries.language.indents_query.as_ref()?,
|
||||
)
|
||||
}
|
||||
|
||||
/// Re-parse the tree based on the updated tree and source content.
|
||||
async fn parse_text(
|
||||
content: BufferSnapshot,
|
||||
old_tree: Option<Tree>,
|
||||
language: &Language,
|
||||
) -> Tree {
|
||||
PARSER.with(|parser| {
|
||||
let mut parser = parser.borrow_mut();
|
||||
parser
|
||||
.set_language(&language.grammar)
|
||||
.expect("incompatible grammar");
|
||||
let mut bytes = content.bytes();
|
||||
let mut callback = |byte_offset: usize, _point: arborium::tree_sitter::Point| {
|
||||
// Add 1 since the buffer is 1 indexed.
|
||||
bytes.seek(ByteOffset::from(byte_offset + 1));
|
||||
bytes.next().unwrap_or_default()
|
||||
};
|
||||
parser
|
||||
.parse_with_options(&mut callback, old_tree.as_ref(), None)
|
||||
.expect("Should succeed")
|
||||
})
|
||||
}
|
||||
|
||||
/// Translate an incoming edit delta into an InputEdit for incrementally updating the syntax
|
||||
/// tree. Uses the precomputed byte edit info (which was captured from the correct intermediate
|
||||
/// buffer state) and `replaced_points` instead of re-deriving from the final buffer.
|
||||
fn delta_to_input_edit(delta: &PreciseDelta) -> InputEdit {
|
||||
// Convert 1-indexed ByteOffset values to 0-indexed for tree-sitter.
|
||||
let start_byte = delta.replaced_byte_range.start.as_usize().saturating_sub(1);
|
||||
let old_end_byte = delta.replaced_byte_range.end.as_usize().saturating_sub(1);
|
||||
|
||||
InputEdit {
|
||||
start_byte,
|
||||
old_end_byte,
|
||||
new_end_byte: start_byte + delta.new_byte_length,
|
||||
start_position: point_to_syntax_point(delta.replaced_points.start),
|
||||
old_end_position: point_to_syntax_point(delta.replaced_points.end),
|
||||
new_end_position: point_to_syntax_point(delta.new_end_point),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn invalidate_highlight_cache_for_version(&self, version: BufferVersion) {
|
||||
// Check if the cache exists and if it matches the version being invalidated
|
||||
let mut cache = self.highlight_cache.borrow_mut();
|
||||
if let Some(ref cached) = *cache {
|
||||
if cached.key.version == version {
|
||||
*cache = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_color_map(&mut self, color_map: ColorMap) {
|
||||
self.color_map = color_map;
|
||||
if let Some(language_query) = self.language_queries.take() {
|
||||
self.set_language(language_query.language);
|
||||
}
|
||||
// Clear highlight cache since colors have changed
|
||||
*self.highlight_cache.borrow_mut() = None;
|
||||
}
|
||||
|
||||
/// Truncates the syntax tree cache to maintain the MAX_SYNTAX_TREES policy.
|
||||
/// Keeps the oldest MAX_SYNTAX_TREES - 1 versions and the provided content_version.
|
||||
fn truncate_tree_state(
|
||||
syntax_tree_lock: &mut HashMap<BufferVersion, Tree>,
|
||||
buffer_version: BufferVersion,
|
||||
) {
|
||||
if syntax_tree_lock.len() <= MAX_SYNTAX_TREES {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut versions: Vec<BufferVersion> = syntax_tree_lock.keys().copied().collect();
|
||||
versions.sort();
|
||||
|
||||
let mut keep: HashSet<BufferVersion> = versions
|
||||
.iter()
|
||||
.take(MAX_SYNTAX_TREES - 1)
|
||||
.copied()
|
||||
.collect();
|
||||
keep.insert(buffer_version);
|
||||
|
||||
syntax_tree_lock.retain(|v, _| keep.contains(v));
|
||||
}
|
||||
}
|
||||
|
||||
impl DecorationLayer for SyntaxTreeState {
|
||||
fn update_internal_state_with_delta(
|
||||
&mut self,
|
||||
deltas: &[PreciseDelta],
|
||||
version: BufferVersion,
|
||||
content: BufferSnapshot,
|
||||
ctx: &mut ModelContext<Self>,
|
||||
) {
|
||||
// If there is an active parsing in progress. Abort that first before starting another one.
|
||||
if let Some(handle) = self.parsing_handle.take() {
|
||||
handle.abort();
|
||||
}
|
||||
|
||||
let Some(language) = self
|
||||
.language_queries
|
||||
.as_ref()
|
||||
.map(|language_queries| language_queries.language.clone())
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
let mut syntax_tree_lock = self.syntax_tree.lock();
|
||||
let mut tree = syntax_tree_lock.get(&self.buffer_version).cloned();
|
||||
if let Some(tree) = &mut tree {
|
||||
for delta in deltas {
|
||||
let edit = Self::delta_to_input_edit(delta);
|
||||
tree.edit(&edit);
|
||||
}
|
||||
|
||||
// We write to the tree immediately after editing first to prevent flickering in the render
|
||||
// state before reparsing gets completed.
|
||||
if let Some(existing) = syntax_tree_lock.get_mut(&version) {
|
||||
existing.clone_from(tree);
|
||||
} else {
|
||||
syntax_tree_lock.insert(version, tree.clone());
|
||||
Self::truncate_tree_state(&mut syntax_tree_lock, version);
|
||||
}
|
||||
}
|
||||
|
||||
let handle = ctx
|
||||
.spawn(
|
||||
async move {
|
||||
let new_tree = Self::parse_text(content, tree, &language).await;
|
||||
futures_lite::future::yield_now().await;
|
||||
new_tree
|
||||
},
|
||||
move |model, new_tree, ctx| {
|
||||
let mut syntax_tree_lock = model.syntax_tree.lock();
|
||||
model.invalidate_highlight_cache_for_version(version);
|
||||
if let Some(old_tree) = syntax_tree_lock.get_mut(&version) {
|
||||
*old_tree = new_tree;
|
||||
} else {
|
||||
// This is for the case where we are updating the syntax tree for the first time.
|
||||
syntax_tree_lock.insert(version, new_tree);
|
||||
Self::truncate_tree_state(&mut syntax_tree_lock, model.buffer_version);
|
||||
}
|
||||
ctx.emit(DecorationStateEvent::DecorationUpdated { version });
|
||||
},
|
||||
)
|
||||
.abort_handle();
|
||||
|
||||
self.buffer_version = version;
|
||||
self.parsing_handle = Some(handle);
|
||||
}
|
||||
}
|
||||
|
||||
impl Entity for SyntaxTreeState {
|
||||
type Event = DecorationStateEvent;
|
||||
}
|
||||
|
||||
/// Convert a 1-indexed buffer Point into a 0-indexed tree-sitter Point.
|
||||
fn point_to_syntax_point(point: Point) -> arborium::tree_sitter::Point {
|
||||
// Subtracting 1 from row to convert from 1-indexed buffer rows to 0-indexed tree-sitter rows.
|
||||
arborium::tree_sitter::Point {
|
||||
row: point.row.saturating_sub(1) as usize,
|
||||
column: point.column as usize,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
use std::{iter, ops::Range};
|
||||
|
||||
use arborium::tree_sitter::{Node, Query, QueryCursor, TextProvider, Tree};
|
||||
use rangemap::RangeMap;
|
||||
use streaming_iterator::StreamingIterator;
|
||||
use string_offset::{ByteOffset, CharOffset};
|
||||
use warp_editor::content::{
|
||||
buffer::{Buffer, ToBufferByteOffset, ToBufferCharOffset},
|
||||
text::Bytes,
|
||||
};
|
||||
use warpui::color::ColorU;
|
||||
|
||||
/// Color mapping from parsed syntax token name to its corresponding highlighting color.
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct ColorMap {
|
||||
pub keyword_color: ColorU,
|
||||
pub function_color: ColorU,
|
||||
pub string_color: ColorU,
|
||||
pub type_color: ColorU,
|
||||
pub number_color: ColorU,
|
||||
pub comment_color: ColorU,
|
||||
pub property_color: ColorU,
|
||||
pub tag_color: ColorU,
|
||||
}
|
||||
|
||||
/// Query for retrieving syntax highlighting information on the tokens.
|
||||
pub struct HighlightQuery {
|
||||
highlight_map: Vec<Option<ColorU>>,
|
||||
}
|
||||
|
||||
impl HighlightQuery {
|
||||
pub fn new(query: &Query, color_map: ColorMap) -> Self {
|
||||
let highlight_map = query
|
||||
.capture_names()
|
||||
.iter()
|
||||
.map(|name| convert_capture_name_to_color(name, &color_map))
|
||||
.collect();
|
||||
|
||||
Self { highlight_map }
|
||||
}
|
||||
|
||||
/// Given the a character range, return its corresponding highlight colors.
|
||||
pub fn get_highlighted_chunks(
|
||||
&self,
|
||||
range: Range<CharOffset>,
|
||||
query: &Query,
|
||||
buffer: &Buffer,
|
||||
tree: &Tree,
|
||||
) -> RangeMap<CharOffset, ColorU> {
|
||||
let mut range_map = RangeMap::new();
|
||||
|
||||
let mut cursor = QueryCursor::new();
|
||||
let byte_start = range.start.to_buffer_byte_offset(buffer).as_usize();
|
||||
let byte_end = range.end.to_buffer_byte_offset(buffer).as_usize();
|
||||
cursor.set_byte_range(byte_start..byte_end);
|
||||
let mut captures = cursor.captures(query, tree.root_node(), TextBuffer(buffer));
|
||||
|
||||
while let Some(matches) = captures.next() {
|
||||
for cap in matches.0.captures {
|
||||
let insertion_range = cap.node.byte_range();
|
||||
let color = self
|
||||
.highlight_map
|
||||
.get(cap.index as usize)
|
||||
.and_then(|inner| *inner);
|
||||
|
||||
if let Some(color) = color {
|
||||
let char_start =
|
||||
ByteOffset::from(insertion_range.start).to_buffer_char_offset(buffer);
|
||||
let char_end =
|
||||
ByteOffset::from(insertion_range.end).to_buffer_char_offset(buffer);
|
||||
if char_start < char_end {
|
||||
range_map.insert(char_start..char_end, color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
range_map
|
||||
}
|
||||
}
|
||||
|
||||
fn convert_capture_name_to_color(name: &str, color_map: &ColorMap) -> Option<ColorU> {
|
||||
match name.split('.').next() {
|
||||
Some("keyword") => Some(color_map.keyword_color),
|
||||
Some("function") => Some(color_map.function_color),
|
||||
Some("string") => Some(color_map.string_color),
|
||||
Some("type") => Some(color_map.type_color),
|
||||
Some("number") => Some(color_map.number_color),
|
||||
Some("comment") => Some(color_map.comment_color),
|
||||
Some("property") => Some(color_map.property_color),
|
||||
Some("tag") => Some(color_map.tag_color),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
// The default tree-sitter implementation here is unsafe (since the cursor could query invalid ranges outside of content length).
|
||||
// TODO(kevin): Once we migrate buffer to store ArrayStrings. We should implement the chunks API on buffer directly to avoid collecting
|
||||
// into a String and then chunking them again for highlighting.
|
||||
pub struct TextSlice<'a>(pub &'a [u8]);
|
||||
|
||||
impl TextSlice<'_> {
|
||||
fn get(&self, range: Range<usize>) -> Self {
|
||||
Self(self.0.get(range).unwrap_or_default())
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<[u8]> for TextSlice<'_> {
|
||||
fn as_ref(&self) -> &[u8] {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> TextProvider<TextSlice<'a>> for TextSlice<'a> {
|
||||
type I = iter::Once<TextSlice<'a>>;
|
||||
|
||||
fn text(&mut self, node: Node) -> Self::I {
|
||||
iter::once(self.get(node.byte_range()))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TextBuffer<'a>(pub &'a Buffer);
|
||||
|
||||
impl<'a> TextProvider<&'a [u8]> for TextBuffer<'a> {
|
||||
type I = Bytes<'a>;
|
||||
|
||||
fn text(&mut self, node: Node) -> Self::I {
|
||||
let range = node.range();
|
||||
self.0.bytes_in_range(
|
||||
ByteOffset::from(range.start_byte),
|
||||
ByteOffset::from(range.end_byte),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
use std::{collections::HashMap, ops::Range};
|
||||
|
||||
use arborium::tree_sitter::{Node, Query, QueryCursor, Tree};
|
||||
use streaming_iterator::StreamingIterator;
|
||||
use warp_editor::content::buffer::Buffer;
|
||||
use warpui::text::point::Point;
|
||||
|
||||
use super::highlight_query::TextBuffer;
|
||||
|
||||
/// The absolute indentation unit from start of the line.
|
||||
#[derive(Debug)]
|
||||
pub struct IndentDelta {
|
||||
pub delta: u8,
|
||||
}
|
||||
|
||||
/// Given the current syntax tree and a point in the buffer. Calculate the correct indentation level for that point.
|
||||
pub fn indentation_delta(
|
||||
buffer: &Buffer,
|
||||
tree: &Tree,
|
||||
position: Point,
|
||||
query: &Query,
|
||||
) -> Option<IndentDelta> {
|
||||
let mut cursor = QueryCursor::new();
|
||||
let tree_sitter_point = arborium::tree_sitter::Point {
|
||||
row: position.row as usize,
|
||||
column: position.column as usize,
|
||||
};
|
||||
let (mut node, byte_range) = find_indent_query_range(tree, tree_sitter_point)?;
|
||||
|
||||
cursor.set_byte_range(byte_range);
|
||||
let mut captures = cursor.captures(query, tree.root_node(), TextBuffer(buffer));
|
||||
|
||||
// We want to group indents and outdents by nodes. This is necessary given in lines where we have multiple
|
||||
// indents fragments, the absolute indent / outdent level should be capped to one. Take the following example line:
|
||||
// `if self.x > 0 {`. Here there are multiple indent sources [self.] (field_expression), [{] (block), yet
|
||||
// the absolute indentation level after the line should be 1.
|
||||
let mut delta_with_node: HashMap<usize, i8> = HashMap::new();
|
||||
while let Some(matches) = captures.next() {
|
||||
for capture in matches.0.captures {
|
||||
// Do not look at nodes that are after the current point. Note that we still want to look at the current node in case
|
||||
// it is an outdent node. In this case, we want to reduce the overall indentation level by 1.
|
||||
if capture.node.start_position() > tree_sitter_point {
|
||||
break;
|
||||
}
|
||||
|
||||
let capture_name = query.capture_names()[capture.index as usize];
|
||||
let base = delta_with_node.entry(capture.node.id()).or_default();
|
||||
|
||||
// Cap indent / outdent to 1.
|
||||
*base = match capture_name {
|
||||
"indent" if capture.node.start_position() != tree_sitter_point => {
|
||||
(*base + 1).min(1)
|
||||
}
|
||||
"outdent" => (*base - 1).max(-1),
|
||||
_ => 0,
|
||||
};
|
||||
}
|
||||
}
|
||||
let mut sum = 0;
|
||||
let mut previous_line = None;
|
||||
let mut total_line_delta = 0;
|
||||
|
||||
// Starting from the source syntax node, iterate over its parents and add the indent delta over every single node.
|
||||
// Take the following code example:
|
||||
//
|
||||
// impl Element { // Parent 2
|
||||
// fn some_func_1() {...}
|
||||
// fn some_func() { // Parent 1
|
||||
// [syntax node]
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// The total indentation level should be 2 because (parent 1) has a delta of 1, and (parent 2) has a delta of 1.
|
||||
loop {
|
||||
// Similar to above, there could be multiple nodes in a single line, we also want to cap the max indent/outdent.
|
||||
// returned to 1.
|
||||
if let Some(line_delta) = delta_with_node.remove(&node.id()) {
|
||||
let current_line = node.start_position().row;
|
||||
|
||||
if previous_line.is_none() || previous_line != Some(current_line) {
|
||||
sum += total_line_delta;
|
||||
previous_line = Some(current_line);
|
||||
total_line_delta = line_delta;
|
||||
} else {
|
||||
total_line_delta = (total_line_delta + line_delta).clamp(-1, 1);
|
||||
}
|
||||
}
|
||||
|
||||
match node.parent() {
|
||||
Some(parent) => {
|
||||
node = parent;
|
||||
}
|
||||
None => {
|
||||
sum += total_line_delta;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some(IndentDelta {
|
||||
delta: sum.max(0) as u8,
|
||||
})
|
||||
}
|
||||
|
||||
/// Given a position in the buffer, find the corresponding syntax node and byte range we should
|
||||
/// use for indentation query.
|
||||
fn find_indent_query_range(
|
||||
tree: &Tree,
|
||||
tree_sitter_point: arborium::tree_sitter::Point,
|
||||
) -> Option<(Node<'_>, Range<usize>)> {
|
||||
// Find the exact syntax node for the given position.
|
||||
let node = tree
|
||||
.root_node()
|
||||
.descendant_for_point_range(tree_sitter_point, tree_sitter_point)
|
||||
.and_then(|node| {
|
||||
// Handle edge case where the node we want to start traversing the tree from can't be
|
||||
// found with `descendant_for_point_range`. This happens because there can be "empty"
|
||||
// nodes that don't span any points. In that case, the fall back is always to the
|
||||
// `Tree`'s root node which always spans all valid points. We actually want the leaf
|
||||
// node that is spans the `tree_sitter::Point` one column to the left because we need to
|
||||
// start summing indentation levels from there.
|
||||
// TODO(INT-614): Remove this special case.
|
||||
if node == tree.root_node() {
|
||||
let new_ts_point = arborium::tree_sitter::Point {
|
||||
row: tree_sitter_point.row,
|
||||
column: tree_sitter_point.column.saturating_sub(1),
|
||||
};
|
||||
tree.root_node()
|
||||
.descendant_for_point_range(new_ts_point, new_ts_point)
|
||||
} else {
|
||||
Some(node)
|
||||
}
|
||||
})?;
|
||||
|
||||
let mut cursor = tree.walk();
|
||||
let mut last_child_row: Option<(usize, Range<usize>)> = None;
|
||||
|
||||
// Find the line range right before the given position, this will be the source for determining
|
||||
// the correct syntax tree range to query.
|
||||
for child in node.children(&mut cursor) {
|
||||
if child.start_position() >= tree_sitter_point {
|
||||
break;
|
||||
}
|
||||
|
||||
if let Some((row_idx, range)) = &mut last_child_row {
|
||||
if *row_idx < child.start_position().row {
|
||||
*row_idx = child.start_position().row;
|
||||
*range = child.byte_range();
|
||||
} else {
|
||||
range.end = child.end_byte();
|
||||
}
|
||||
} else {
|
||||
last_child_row = Some((child.start_position().row, child.byte_range()));
|
||||
}
|
||||
}
|
||||
|
||||
// If node has no children, fallback to use node's byte range.
|
||||
let query_range = last_child_row.map(|row| row.1).unwrap_or(node.byte_range());
|
||||
Some((node, query_range))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "indent_query_tests.rs"]
|
||||
mod tests;
|
||||
@@ -0,0 +1,733 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use arborium::tree_sitter::Tree;
|
||||
use languages::{language_by_filename, Language};
|
||||
use warp_editor::content::buffer::{Buffer, BufferSnapshot};
|
||||
use warp_editor::content::selection_model::BufferSelectionModel;
|
||||
use warp_editor::content::text::IndentBehavior;
|
||||
use warpui::App;
|
||||
|
||||
use crate::SyntaxTreeState;
|
||||
|
||||
use super::*;
|
||||
|
||||
// Simple stub function to allow compilation - can be improved later
|
||||
fn mock_buffer_and_tree(text_content: &str, language: Arc<Language>) -> (Buffer, Tree) {
|
||||
// Create a tree by parsing the text
|
||||
let snapshot = BufferSnapshot::from_plain_text(text_content);
|
||||
let tree = warpui::r#async::block_on(async {
|
||||
SyntaxTreeState::parse_text(snapshot, None, &language).await
|
||||
});
|
||||
|
||||
// Create a minimal buffer
|
||||
let buffer = Buffer::new(Box::new(|_, _| IndentBehavior::Ignore));
|
||||
(buffer, tree)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_indent_query() {
|
||||
App::test((), |mut app| async move {
|
||||
let language = language_by_filename(std::path::Path::new("test.rs"))
|
||||
.expect("Should contain language rule for rust");
|
||||
let text_content = r#"impl Test {
|
||||
fn first_func() {
|
||||
|
||||
}
|
||||
|
||||
fn second_func() {
|
||||
if true {
|
||||
|
||||
}
|
||||
}
|
||||
}"#;
|
||||
|
||||
let buffer_handle = app.add_model(|_| Buffer::new(Box::new(|_, _| IndentBehavior::Ignore)));
|
||||
let selection = app.add_model(|_| BufferSelectionModel::new(buffer_handle.clone()));
|
||||
|
||||
buffer_handle.update(&mut app, |buffer, ctx| {
|
||||
*buffer = Buffer::from_plain_text(
|
||||
text_content,
|
||||
None,
|
||||
Box::new(|_, _| IndentBehavior::Ignore),
|
||||
selection,
|
||||
ctx,
|
||||
);
|
||||
});
|
||||
|
||||
let buffer_snapshot = buffer_handle.read(&app, |buffer, _| buffer.buffer_snapshot());
|
||||
let tree = warpui::r#async::block_on(async {
|
||||
SyntaxTreeState::parse_text(buffer_snapshot, None, &language).await
|
||||
});
|
||||
|
||||
let query = language.as_ref().indents_query.as_ref().unwrap();
|
||||
|
||||
buffer_handle.read(&app, |buffer, _| {
|
||||
// Check that the top level code is not improperly marked as indented because of the indentation
|
||||
// in the string literal.
|
||||
assert_eq!(
|
||||
indentation_delta(buffer, &tree, Point { row: 0, column: 0 }, query)
|
||||
.unwrap()
|
||||
.delta,
|
||||
0
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
indentation_delta(buffer, &tree, Point { row: 1, column: 0 }, query)
|
||||
.unwrap()
|
||||
.delta,
|
||||
1
|
||||
);
|
||||
|
||||
// Indentation level in first_func should be 2.
|
||||
assert_eq!(
|
||||
indentation_delta(buffer, &tree, Point { row: 2, column: 0 }, query)
|
||||
.unwrap()
|
||||
.delta,
|
||||
2
|
||||
);
|
||||
|
||||
// Indentation level between first_func and second_func definition should be 1.
|
||||
assert_eq!(
|
||||
indentation_delta(buffer, &tree, Point { row: 4, column: 0 }, query)
|
||||
.unwrap()
|
||||
.delta,
|
||||
1
|
||||
);
|
||||
|
||||
// Indentation level inside the if statement in second_func should be 3.
|
||||
assert_eq!(
|
||||
indentation_delta(buffer, &tree, Point { row: 7, column: 0 }, query)
|
||||
.unwrap()
|
||||
.delta,
|
||||
3
|
||||
);
|
||||
|
||||
// Indentation level at the start of the closing bracket should be 1.
|
||||
assert_eq!(
|
||||
indentation_delta(buffer, &tree, Point { row: 9, column: 0 }, query)
|
||||
.unwrap()
|
||||
.delta,
|
||||
1
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_indent_query_on_go() {
|
||||
App::test((), |mut app| async move {
|
||||
let language = language_by_filename(std::path::Path::new("test.go"))
|
||||
.expect("Should contain language rule for go");
|
||||
let text_content = r#"package logic
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type TestType struct {
|
||||
Attribute1 int
|
||||
Attribute2 int
|
||||
}
|
||||
|
||||
func CreateTestType(ctx context.Context, db types.SqlQuerier) (*TestType, error) {
|
||||
if !testTypeExist() {
|
||||
return nil, testTypeNotExistError
|
||||
}
|
||||
|
||||
return ctx.GetTest(), nil
|
||||
}"#;
|
||||
|
||||
let buffer_handle = app.add_model(|_| Buffer::new(Box::new(|_, _| IndentBehavior::Ignore)));
|
||||
let selection = app.add_model(|_| BufferSelectionModel::new(buffer_handle.clone()));
|
||||
|
||||
buffer_handle.update(&mut app, |buffer, ctx| {
|
||||
*buffer = Buffer::from_plain_text(
|
||||
text_content,
|
||||
None,
|
||||
Box::new(|_, _| IndentBehavior::Ignore),
|
||||
selection,
|
||||
ctx,
|
||||
);
|
||||
});
|
||||
|
||||
let buffer_snapshot = buffer_handle.read(&app, |buffer, _| buffer.buffer_snapshot());
|
||||
let tree = warpui::r#async::block_on(async {
|
||||
SyntaxTreeState::parse_text(buffer_snapshot, None, &language).await
|
||||
});
|
||||
|
||||
let query = &language.as_ref().indents_query.as_ref().unwrap();
|
||||
|
||||
buffer_handle.read(&app, |buffer, _| {
|
||||
// Check that the top level code is not improperly marked as indented because of the indentation
|
||||
// in the string literal.
|
||||
assert_eq!(
|
||||
indentation_delta(buffer, &tree, Point { row: 0, column: 0 }, query)
|
||||
.unwrap()
|
||||
.delta,
|
||||
0
|
||||
);
|
||||
assert_eq!(
|
||||
indentation_delta(buffer, &tree, Point { row: 1, column: 0 }, query)
|
||||
.unwrap()
|
||||
.delta,
|
||||
0
|
||||
);
|
||||
|
||||
// Indentation level in import statements should be 1.
|
||||
assert_eq!(
|
||||
indentation_delta(buffer, &tree, Point { row: 3, column: 0 }, query)
|
||||
.unwrap()
|
||||
.delta,
|
||||
1
|
||||
);
|
||||
|
||||
// Indentation level in type definition should be 1.
|
||||
assert_eq!(
|
||||
indentation_delta(buffer, &tree, Point { row: 7, column: 0 }, query)
|
||||
.unwrap()
|
||||
.delta,
|
||||
1
|
||||
);
|
||||
|
||||
// Indentation level in if statement should be 2.
|
||||
assert_eq!(
|
||||
indentation_delta(buffer, &tree, Point { row: 13, column: 0 }, query)
|
||||
.unwrap()
|
||||
.delta,
|
||||
2
|
||||
);
|
||||
|
||||
// Indentation level at the start of the if statement closing bracket should be 2.
|
||||
assert_eq!(
|
||||
indentation_delta(buffer, &tree, Point { row: 14, column: 0 }, query)
|
||||
.unwrap()
|
||||
.delta,
|
||||
2
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_indent_query_on_go_bracket_expansion() {
|
||||
let language = language_by_filename(std::path::Path::new("test.go"))
|
||||
.expect("Should contain language rule for go");
|
||||
let (buffer, tree) = mock_buffer_and_tree(
|
||||
r#"func test(){}
|
||||
func test() {
|
||||
go func() {}
|
||||
}"#,
|
||||
language.clone(),
|
||||
);
|
||||
let query = &language.as_ref().indents_query.as_ref().unwrap();
|
||||
|
||||
// Indentation level on first line between parentheses should be 0 (considering the closing bracket).
|
||||
assert_eq!(
|
||||
indentation_delta(&buffer, &tree, Point { row: 0, column: 10 }, query)
|
||||
.unwrap()
|
||||
.delta,
|
||||
0
|
||||
);
|
||||
|
||||
// Indentation level on first line between brackets should be 0 (considering the closing bracket).
|
||||
assert_eq!(
|
||||
indentation_delta(&buffer, &tree, Point { row: 0, column: 12 }, query)
|
||||
.unwrap()
|
||||
.delta,
|
||||
0
|
||||
);
|
||||
|
||||
// Indentation level on the third line between brackets should be 2 since this is a
|
||||
// go func nested in another func.
|
||||
assert_eq!(
|
||||
indentation_delta(&buffer, &tree, Point { row: 2, column: 23 }, query)
|
||||
.unwrap()
|
||||
.delta,
|
||||
2
|
||||
);
|
||||
}
|
||||
|
||||
// source: https://peps.python.org/pep-0008/#indentation
|
||||
#[test]
|
||||
fn test_indent_query_on_python() {
|
||||
let language = language_by_filename(std::path::Path::new("test.py"))
|
||||
.expect("Should contain language rule for python");
|
||||
let (buffer, tree) = mock_buffer_and_tree(
|
||||
r#"# Aligned with opening delimiter.
|
||||
foo = long_function_name(var_one, var_two,
|
||||
var_three, var_four)
|
||||
|
||||
# Add 4 spaces (an extra level of indentation) to distinguish arguments from the rest.
|
||||
def long_function_name(
|
||||
var_one, var_two, var_three,
|
||||
var_four):
|
||||
print(var_one)"#,
|
||||
language.clone(),
|
||||
);
|
||||
let query = &language.as_ref().indents_query.as_ref().unwrap();
|
||||
|
||||
// Check that the top level code is not improperly marked as indented because of the indentation
|
||||
// in the string literal.
|
||||
assert_eq!(
|
||||
indentation_delta(&buffer, &tree, Point { row: 0, column: 0 }, query)
|
||||
.unwrap()
|
||||
.delta,
|
||||
0
|
||||
);
|
||||
assert_eq!(
|
||||
indentation_delta(&buffer, &tree, Point { row: 1, column: 0 }, query)
|
||||
.unwrap()
|
||||
.delta,
|
||||
0
|
||||
);
|
||||
|
||||
// Indentation level argument list split across lines should be 1.
|
||||
assert_eq!(
|
||||
indentation_delta(&buffer, &tree, Point { row: 2, column: 0 }, query)
|
||||
.unwrap()
|
||||
.delta,
|
||||
1
|
||||
);
|
||||
|
||||
// Indentation level in top-level function definition should be 1.
|
||||
assert_eq!(
|
||||
indentation_delta(&buffer, &tree, Point { row: 8, column: 0 }, query)
|
||||
.unwrap()
|
||||
.delta,
|
||||
1
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_indent_query_on_python_colon() {
|
||||
let language = language_by_filename(std::path::Path::new("test.py"))
|
||||
.expect("Should contain language rule for python");
|
||||
let (if_buffer, if_tree) = mock_buffer_and_tree(r#"if x:"#, language.clone());
|
||||
let query = &language.as_ref().indents_query.as_ref().unwrap();
|
||||
|
||||
// `if x:|`
|
||||
assert_eq!(
|
||||
indentation_delta(&if_buffer, &if_tree, Point { row: 0, column: 5 }, query)
|
||||
.unwrap()
|
||||
.delta,
|
||||
1
|
||||
);
|
||||
|
||||
let (empty_next_line, empty_next_line_tree) = mock_buffer_and_tree(
|
||||
r#"if x:
|
||||
"#,
|
||||
language.clone(),
|
||||
);
|
||||
let query = &language.as_ref().indents_query.as_ref().unwrap();
|
||||
|
||||
// `if x:|
|
||||
// `
|
||||
assert_eq!(
|
||||
indentation_delta(
|
||||
&empty_next_line,
|
||||
&empty_next_line_tree,
|
||||
Point { row: 0, column: 5 },
|
||||
query
|
||||
)
|
||||
.unwrap()
|
||||
.delta,
|
||||
1
|
||||
);
|
||||
|
||||
// The `if_statement`'s block would end on line 1, so pressing `Enter` here should go back to
|
||||
// indentation level 0
|
||||
// `if x:
|
||||
// |`
|
||||
assert_eq!(
|
||||
indentation_delta(
|
||||
&empty_next_line,
|
||||
&empty_next_line_tree,
|
||||
Point { row: 1, column: 0 },
|
||||
query
|
||||
)
|
||||
.unwrap()
|
||||
.delta,
|
||||
0
|
||||
);
|
||||
|
||||
// This is invalid Python syntax because the `pass` statement is not indented.
|
||||
let (invalid_syntax_buffer, invalid_syntax_tree) = mock_buffer_and_tree(
|
||||
r#"if x:
|
||||
pass"#,
|
||||
language.clone(),
|
||||
);
|
||||
let query = &language.as_ref().indents_query.as_ref().unwrap();
|
||||
|
||||
// `if x:|
|
||||
// pass`
|
||||
assert_eq!(
|
||||
indentation_delta(
|
||||
&invalid_syntax_buffer,
|
||||
&invalid_syntax_tree,
|
||||
Point { row: 0, column: 5 },
|
||||
query
|
||||
)
|
||||
.unwrap()
|
||||
.delta,
|
||||
1
|
||||
);
|
||||
|
||||
// `if x:
|
||||
// |pass`
|
||||
assert_eq!(
|
||||
indentation_delta(
|
||||
&invalid_syntax_buffer,
|
||||
&invalid_syntax_tree,
|
||||
Point { row: 1, column: 0 },
|
||||
query
|
||||
)
|
||||
.unwrap()
|
||||
.delta,
|
||||
1
|
||||
);
|
||||
|
||||
// `if x:
|
||||
// pass|`
|
||||
assert_eq!(
|
||||
indentation_delta(
|
||||
&invalid_syntax_buffer,
|
||||
&invalid_syntax_tree,
|
||||
Point { row: 1, column: 4 },
|
||||
query
|
||||
)
|
||||
.unwrap()
|
||||
.delta,
|
||||
1
|
||||
);
|
||||
|
||||
let (valid_non_empty_new_line_buffer, valid_non_empty_new_line_tree) = mock_buffer_and_tree(
|
||||
r#"if x:
|
||||
pass"#,
|
||||
language.clone(),
|
||||
);
|
||||
let query = &language.as_ref().indents_query.as_ref().unwrap();
|
||||
|
||||
// `if x:|
|
||||
// pass`
|
||||
assert_eq!(
|
||||
indentation_delta(
|
||||
&valid_non_empty_new_line_buffer,
|
||||
&valid_non_empty_new_line_tree,
|
||||
Point { row: 0, column: 5 },
|
||||
query
|
||||
)
|
||||
.unwrap()
|
||||
.delta,
|
||||
1
|
||||
);
|
||||
|
||||
// `if x:
|
||||
// | pass`
|
||||
assert_eq!(
|
||||
indentation_delta(
|
||||
&valid_non_empty_new_line_buffer,
|
||||
&valid_non_empty_new_line_tree,
|
||||
Point { row: 1, column: 0 },
|
||||
query
|
||||
)
|
||||
.unwrap()
|
||||
.delta,
|
||||
1
|
||||
);
|
||||
|
||||
// `if x:
|
||||
// pass|`
|
||||
assert_eq!(
|
||||
indentation_delta(
|
||||
&valid_non_empty_new_line_buffer,
|
||||
&valid_non_empty_new_line_tree,
|
||||
Point { row: 1, column: 7 },
|
||||
query
|
||||
)
|
||||
.unwrap()
|
||||
.delta,
|
||||
1
|
||||
);
|
||||
|
||||
let (split_line_buffer, split_line_bugger) =
|
||||
mock_buffer_and_tree(r#"if x:pass"#, language.clone());
|
||||
let query = &language.as_ref().indents_query.as_ref().unwrap();
|
||||
|
||||
// `if x:|pass`
|
||||
assert_eq!(
|
||||
indentation_delta(
|
||||
&split_line_buffer,
|
||||
&split_line_bugger,
|
||||
Point { row: 0, column: 5 },
|
||||
query
|
||||
)
|
||||
.unwrap()
|
||||
.delta,
|
||||
1
|
||||
);
|
||||
|
||||
// `if x:pass|`
|
||||
assert_eq!(
|
||||
indentation_delta(
|
||||
&split_line_buffer,
|
||||
&split_line_bugger,
|
||||
Point { row: 0, column: 9 },
|
||||
query
|
||||
)
|
||||
.unwrap()
|
||||
.delta,
|
||||
1
|
||||
);
|
||||
|
||||
let (function_buffer, function_tree) = mock_buffer_and_tree(r#"def foo():"#, language.clone());
|
||||
let query = &language.as_ref().indents_query.as_ref().unwrap();
|
||||
|
||||
// `def foo():|`
|
||||
assert_eq!(
|
||||
indentation_delta(
|
||||
&function_buffer,
|
||||
&function_tree,
|
||||
Point { row: 0, column: 10 },
|
||||
query
|
||||
)
|
||||
.unwrap()
|
||||
.delta,
|
||||
1
|
||||
);
|
||||
|
||||
// Text content is fully outdented to avoid confusion with indentation and Rust raw string
|
||||
// literals.
|
||||
let (multilevel_buffer, multilevel_tree) = mock_buffer_and_tree(
|
||||
r#"
|
||||
def foo():
|
||||
x = True
|
||||
if x:"#,
|
||||
language.clone(),
|
||||
);
|
||||
let query = &language.as_ref().indents_query.as_ref().unwrap();
|
||||
|
||||
// `
|
||||
// def foo():
|
||||
// x = True|
|
||||
// if x:`
|
||||
assert_eq!(
|
||||
indentation_delta(
|
||||
&multilevel_buffer,
|
||||
&multilevel_tree,
|
||||
Point { row: 2, column: 11 },
|
||||
query
|
||||
)
|
||||
.unwrap()
|
||||
.delta,
|
||||
1
|
||||
);
|
||||
|
||||
// `
|
||||
// def foo():
|
||||
// x = True
|
||||
// | if x:`
|
||||
assert_eq!(
|
||||
indentation_delta(
|
||||
&multilevel_buffer,
|
||||
&multilevel_tree,
|
||||
Point { row: 3, column: 0 },
|
||||
query
|
||||
)
|
||||
.unwrap()
|
||||
.delta,
|
||||
1
|
||||
);
|
||||
|
||||
// `
|
||||
// def foo():
|
||||
// x = True
|
||||
// if x:|`
|
||||
assert_eq!(
|
||||
indentation_delta(
|
||||
&multilevel_buffer,
|
||||
&multilevel_tree,
|
||||
Point { row: 3, column: 9 },
|
||||
query
|
||||
)
|
||||
.unwrap()
|
||||
.delta,
|
||||
2
|
||||
);
|
||||
|
||||
let (function_buffer, function_tree) = mock_buffer_and_tree(
|
||||
r#"
|
||||
def foo():
|
||||
x = True
|
||||
if x:
|
||||
"#,
|
||||
language.clone(),
|
||||
);
|
||||
let query = &language.as_ref().indents_query.as_ref().unwrap();
|
||||
|
||||
// `
|
||||
// def foo():
|
||||
// x = True
|
||||
// if x:|
|
||||
// `
|
||||
assert_eq!(
|
||||
indentation_delta(
|
||||
&function_buffer,
|
||||
&function_tree,
|
||||
Point { row: 2, column: 9 },
|
||||
query
|
||||
)
|
||||
.unwrap()
|
||||
.delta,
|
||||
1
|
||||
);
|
||||
|
||||
// `
|
||||
// def foo():
|
||||
// x = True
|
||||
// if x:
|
||||
// |`
|
||||
assert_eq!(
|
||||
indentation_delta(
|
||||
&function_buffer,
|
||||
&function_tree,
|
||||
Point { row: 4, column: 0 },
|
||||
query
|
||||
)
|
||||
.unwrap()
|
||||
.delta,
|
||||
0
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_indent_query_on_javascript() {
|
||||
let language = language_by_filename(std::path::Path::new("test.js"))
|
||||
.expect("Should contain language rule for javascript");
|
||||
let (buffer, tree) = mock_buffer_and_tree(
|
||||
r#"// Import the 'fs' module, commonly used for file operations
|
||||
const fs = require('fs');
|
||||
|
||||
// Function to check if a number is positive
|
||||
function checkIfPositive(number) {
|
||||
// Check if the number is greater than zero
|
||||
if (number > 0) {
|
||||
console.log('The number is positive.');
|
||||
} else if (number === 0) {
|
||||
console.log('The number is zero.');
|
||||
} else {
|
||||
console.log('The number is negative.');
|
||||
}
|
||||
}
|
||||
|
||||
// Example usage of the function
|
||||
checkIfPositive(5);
|
||||
checkIfPositive(-3);
|
||||
checkIfPositive(0);"#,
|
||||
language.clone(),
|
||||
);
|
||||
let query = &language.as_ref().indents_query.as_ref().unwrap();
|
||||
|
||||
// Check that the top level code is not improperly marked as indented because of the indentation
|
||||
// in the string literal.
|
||||
assert_eq!(
|
||||
indentation_delta(&buffer, &tree, Point { row: 0, column: 0 }, query)
|
||||
.unwrap()
|
||||
.delta,
|
||||
0
|
||||
);
|
||||
assert_eq!(
|
||||
indentation_delta(&buffer, &tree, Point { row: 1, column: 0 }, query)
|
||||
.unwrap()
|
||||
.delta,
|
||||
0
|
||||
);
|
||||
|
||||
// Indentation level inside function definition should be 1.
|
||||
assert_eq!(
|
||||
indentation_delta(&buffer, &tree, Point { row: 5, column: 0 }, query)
|
||||
.unwrap()
|
||||
.delta,
|
||||
1
|
||||
);
|
||||
|
||||
// Indentation level in if statement should be 1 more than its parent.
|
||||
assert_eq!(
|
||||
indentation_delta(&buffer, &tree, Point { row: 7, column: 0 }, query)
|
||||
.unwrap()
|
||||
.delta,
|
||||
2
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_indent_query_on_typescript() {
|
||||
let language = language_by_filename(std::path::Path::new("test.ts"))
|
||||
.expect("Should contain language rule for typescript");
|
||||
let (buffer, tree) = mock_buffer_and_tree(
|
||||
r#"import { User } from './types';
|
||||
import { validateEmail } from './utils';
|
||||
|
||||
interface ProcessedUser {
|
||||
id: string;
|
||||
displayName: string;
|
||||
emailStatus: 'valid' | 'invalid';
|
||||
}
|
||||
|
||||
function processUserData(user: User, includeEmail: boolean = false): ProcessedUser {
|
||||
const processedUser: ProcessedUser = {
|
||||
id: user.id,
|
||||
displayName: '',
|
||||
emailStatus: 'invalid'
|
||||
};
|
||||
|
||||
if (user.firstName && user.lastName) {
|
||||
processedUser.displayName = `${user.firstName} ${user.lastName}`;
|
||||
} else if (user.firstName) {
|
||||
processedUser.displayName = user.firstName;
|
||||
} else {
|
||||
processedUser.displayName = 'Anonymous User';
|
||||
}
|
||||
|
||||
if (includeEmail && user.email) {
|
||||
processedUser.emailStatus = validateEmail(user.email) ? 'valid' : 'invalid';
|
||||
}
|
||||
|
||||
return processedUser;
|
||||
}
|
||||
|
||||
export { ProcessedUser, processUserData };"#,
|
||||
language.clone(),
|
||||
);
|
||||
let query = &language.as_ref().indents_query.as_ref().unwrap();
|
||||
|
||||
// Check that the top level code is not improperly marked as indented because of the indentation
|
||||
// in the string literal.
|
||||
assert_eq!(
|
||||
indentation_delta(&buffer, &tree, Point { row: 0, column: 0 }, query)
|
||||
.unwrap()
|
||||
.delta,
|
||||
0
|
||||
);
|
||||
assert_eq!(
|
||||
indentation_delta(&buffer, &tree, Point { row: 1, column: 0 }, query)
|
||||
.unwrap()
|
||||
.delta,
|
||||
0
|
||||
);
|
||||
|
||||
// Indentation level inside top level interface definition should be 1.
|
||||
assert_eq!(
|
||||
indentation_delta(&buffer, &tree, Point { row: 4, column: 0 }, query)
|
||||
.unwrap()
|
||||
.delta,
|
||||
1
|
||||
);
|
||||
|
||||
// Indentation level in if statement should be 1 more than its parent.
|
||||
assert_eq!(
|
||||
indentation_delta(&buffer, &tree, Point { row: 17, column: 0 }, query)
|
||||
.unwrap()
|
||||
.delta,
|
||||
2
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
pub mod highlight_query;
|
||||
pub mod indent_query;
|
||||
Reference in New Issue
Block a user