Initial public release of Warp.

Repo-Sync-Origin: warpdotdev/warp-internal@12af1d983b
This commit is contained in:
David Stern
2026-04-28 08:43:33 -05:00
commit 0dbd3d567a
4982 changed files with 1431549 additions and 0 deletions
+25
View File
@@ -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"] }
+388
View File
@@ -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
);
}
+2
View File
@@ -0,0 +1,2 @@
pub mod highlight_query;
pub mod indent_query;