mirror of https://github.com/helix-editor/helix
Merge branch 'helix-editor:master' into trim-colons-from-paths
commit
b322752f04
@ -1,76 +1,137 @@
|
|||||||
use crate::{Range, RopeSlice, Selection, Syntax};
|
use crate::{movement::Direction, syntax::TreeCursor, Range, RopeSlice, Selection, Syntax};
|
||||||
use tree_sitter::Node;
|
|
||||||
|
|
||||||
pub fn expand_selection(syntax: &Syntax, text: RopeSlice, selection: Selection) -> Selection {
|
pub fn expand_selection(syntax: &Syntax, text: RopeSlice, selection: Selection) -> Selection {
|
||||||
select_node_impl(syntax, text, selection, |mut node, from, to| {
|
let cursor = &mut syntax.walk();
|
||||||
while node.start_byte() == from && node.end_byte() == to {
|
|
||||||
node = node.parent()?;
|
selection.transform(|range| {
|
||||||
|
let from = text.char_to_byte(range.from());
|
||||||
|
let to = text.char_to_byte(range.to());
|
||||||
|
|
||||||
|
let byte_range = from..to;
|
||||||
|
cursor.reset_to_byte_range(from, to);
|
||||||
|
|
||||||
|
while cursor.node().byte_range() == byte_range {
|
||||||
|
if !cursor.goto_parent() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Some(node)
|
|
||||||
|
let node = cursor.node();
|
||||||
|
let from = text.byte_to_char(node.start_byte());
|
||||||
|
let to = text.byte_to_char(node.end_byte());
|
||||||
|
|
||||||
|
Range::new(to, from).with_direction(range.direction())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn shrink_selection(syntax: &Syntax, text: RopeSlice, selection: Selection) -> Selection {
|
pub fn shrink_selection(syntax: &Syntax, text: RopeSlice, selection: Selection) -> Selection {
|
||||||
select_node_impl(syntax, text, selection, |descendant, _from, _to| {
|
select_node_impl(
|
||||||
descendant.child(0).or(Some(descendant))
|
syntax,
|
||||||
})
|
text,
|
||||||
|
selection,
|
||||||
|
|cursor| {
|
||||||
|
cursor.goto_first_child();
|
||||||
|
},
|
||||||
|
None,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn select_sibling<F>(
|
pub fn select_next_sibling(syntax: &Syntax, text: RopeSlice, selection: Selection) -> Selection {
|
||||||
syntax: &Syntax,
|
select_node_impl(
|
||||||
text: RopeSlice,
|
syntax,
|
||||||
selection: Selection,
|
text,
|
||||||
sibling_fn: &F,
|
selection,
|
||||||
) -> Selection
|
|cursor| {
|
||||||
where
|
while !cursor.goto_next_sibling() {
|
||||||
F: Fn(Node) -> Option<Node>,
|
if !cursor.goto_parent() {
|
||||||
{
|
break;
|
||||||
select_node_impl(syntax, text, selection, |descendant, _from, _to| {
|
}
|
||||||
find_sibling_recursive(descendant, sibling_fn)
|
}
|
||||||
|
},
|
||||||
|
Some(Direction::Forward),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn select_all_siblings(syntax: &Syntax, text: RopeSlice, selection: Selection) -> Selection {
|
||||||
|
selection.transform_iter(|range| {
|
||||||
|
let mut cursor = syntax.walk();
|
||||||
|
let (from, to) = range.into_byte_range(text);
|
||||||
|
cursor.reset_to_byte_range(from, to);
|
||||||
|
|
||||||
|
if !cursor.goto_parent_with(|parent| parent.child_count() > 1) {
|
||||||
|
return vec![range].into_iter();
|
||||||
|
}
|
||||||
|
|
||||||
|
select_children(&mut cursor, text, range).into_iter()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn find_sibling_recursive<F>(node: Node, sibling_fn: F) -> Option<Node>
|
pub fn select_all_children(syntax: &Syntax, text: RopeSlice, selection: Selection) -> Selection {
|
||||||
where
|
selection.transform_iter(|range| {
|
||||||
F: Fn(Node) -> Option<Node>,
|
let mut cursor = syntax.walk();
|
||||||
{
|
let (from, to) = range.into_byte_range(text);
|
||||||
sibling_fn(node).or_else(|| {
|
cursor.reset_to_byte_range(from, to);
|
||||||
node.parent()
|
select_children(&mut cursor, text, range).into_iter()
|
||||||
.and_then(|node| find_sibling_recursive(node, sibling_fn))
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn select_children<'n>(
|
||||||
|
cursor: &'n mut TreeCursor<'n>,
|
||||||
|
text: RopeSlice,
|
||||||
|
range: Range,
|
||||||
|
) -> Vec<Range> {
|
||||||
|
let children = cursor
|
||||||
|
.named_children()
|
||||||
|
.map(|child| Range::from_node(child, text, range.direction()))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
if !children.is_empty() {
|
||||||
|
children
|
||||||
|
} else {
|
||||||
|
vec![range]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn select_prev_sibling(syntax: &Syntax, text: RopeSlice, selection: Selection) -> Selection {
|
||||||
|
select_node_impl(
|
||||||
|
syntax,
|
||||||
|
text,
|
||||||
|
selection,
|
||||||
|
|cursor| {
|
||||||
|
while !cursor.goto_prev_sibling() {
|
||||||
|
if !cursor.goto_parent() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Some(Direction::Backward),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
fn select_node_impl<F>(
|
fn select_node_impl<F>(
|
||||||
syntax: &Syntax,
|
syntax: &Syntax,
|
||||||
text: RopeSlice,
|
text: RopeSlice,
|
||||||
selection: Selection,
|
selection: Selection,
|
||||||
select_fn: F,
|
motion: F,
|
||||||
|
direction: Option<Direction>,
|
||||||
) -> Selection
|
) -> Selection
|
||||||
where
|
where
|
||||||
F: Fn(Node, usize, usize) -> Option<Node>,
|
F: Fn(&mut TreeCursor),
|
||||||
{
|
{
|
||||||
let tree = syntax.tree();
|
let cursor = &mut syntax.walk();
|
||||||
|
|
||||||
selection.transform(|range| {
|
selection.transform(|range| {
|
||||||
let from = text.char_to_byte(range.from());
|
let from = text.char_to_byte(range.from());
|
||||||
let to = text.char_to_byte(range.to());
|
let to = text.char_to_byte(range.to());
|
||||||
|
|
||||||
let node = match tree
|
cursor.reset_to_byte_range(from, to);
|
||||||
.root_node()
|
|
||||||
.descendant_for_byte_range(from, to)
|
motion(cursor);
|
||||||
.and_then(|node| select_fn(node, from, to))
|
|
||||||
{
|
|
||||||
Some(node) => node,
|
|
||||||
None => return range,
|
|
||||||
};
|
|
||||||
|
|
||||||
|
let node = cursor.node();
|
||||||
let from = text.byte_to_char(node.start_byte());
|
let from = text.byte_to_char(node.start_byte());
|
||||||
let to = text.byte_to_char(node.end_byte());
|
let to = text.byte_to_char(node.end_byte());
|
||||||
|
|
||||||
if range.head < range.anchor {
|
Range::new(from, to).with_direction(direction.unwrap_or_else(|| range.direction()))
|
||||||
Range::new(to, from)
|
|
||||||
} else {
|
|
||||||
Range::new(from, to)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,264 @@
|
|||||||
|
use std::{cmp::Reverse, ops::Range};
|
||||||
|
|
||||||
|
use super::{LanguageLayer, LayerId};
|
||||||
|
|
||||||
|
use slotmap::HopSlotMap;
|
||||||
|
use tree_sitter::Node;
|
||||||
|
|
||||||
|
/// The byte range of an injection layer.
|
||||||
|
///
|
||||||
|
/// Injection ranges may overlap, but all overlapping parts are subsets of their parent ranges.
|
||||||
|
/// This allows us to sort the ranges ahead of time in order to efficiently find a range that
|
||||||
|
/// contains a point with maximum depth.
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct InjectionRange {
|
||||||
|
start: usize,
|
||||||
|
end: usize,
|
||||||
|
layer_id: LayerId,
|
||||||
|
depth: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct TreeCursor<'a> {
|
||||||
|
layers: &'a HopSlotMap<LayerId, LanguageLayer>,
|
||||||
|
root: LayerId,
|
||||||
|
current: LayerId,
|
||||||
|
injection_ranges: Vec<InjectionRange>,
|
||||||
|
// TODO: Ideally this would be a `tree_sitter::TreeCursor<'a>` but
|
||||||
|
// that returns very surprising results in testing.
|
||||||
|
cursor: Node<'a>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> TreeCursor<'a> {
|
||||||
|
pub(super) fn new(layers: &'a HopSlotMap<LayerId, LanguageLayer>, root: LayerId) -> Self {
|
||||||
|
let mut injection_ranges = Vec::new();
|
||||||
|
|
||||||
|
for (layer_id, layer) in layers.iter() {
|
||||||
|
// Skip the root layer
|
||||||
|
if layer.parent.is_none() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for byte_range in layer.ranges.iter() {
|
||||||
|
let range = InjectionRange {
|
||||||
|
start: byte_range.start_byte,
|
||||||
|
end: byte_range.end_byte,
|
||||||
|
layer_id,
|
||||||
|
depth: layer.depth,
|
||||||
|
};
|
||||||
|
injection_ranges.push(range);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
injection_ranges.sort_unstable_by_key(|range| (range.end, Reverse(range.depth)));
|
||||||
|
|
||||||
|
let cursor = layers[root].tree().root_node();
|
||||||
|
|
||||||
|
Self {
|
||||||
|
layers,
|
||||||
|
root,
|
||||||
|
current: root,
|
||||||
|
injection_ranges,
|
||||||
|
cursor,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn node(&self) -> Node<'a> {
|
||||||
|
self.cursor
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn goto_parent(&mut self) -> bool {
|
||||||
|
if let Some(parent) = self.node().parent() {
|
||||||
|
self.cursor = parent;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we are already on the root layer, we cannot ascend.
|
||||||
|
if self.current == self.root {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ascend to the parent layer.
|
||||||
|
let range = self.node().byte_range();
|
||||||
|
let parent_id = self.layers[self.current]
|
||||||
|
.parent
|
||||||
|
.expect("non-root layers have a parent");
|
||||||
|
self.current = parent_id;
|
||||||
|
let root = self.layers[self.current].tree().root_node();
|
||||||
|
self.cursor = root
|
||||||
|
.descendant_for_byte_range(range.start, range.end)
|
||||||
|
.unwrap_or(root);
|
||||||
|
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn goto_parent_with<P>(&mut self, predicate: P) -> bool
|
||||||
|
where
|
||||||
|
P: Fn(&Node) -> bool,
|
||||||
|
{
|
||||||
|
while self.goto_parent() {
|
||||||
|
if predicate(&self.node()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Finds the injection layer that has exactly the same range as the given `range`.
|
||||||
|
fn layer_id_of_byte_range(&self, search_range: Range<usize>) -> Option<LayerId> {
|
||||||
|
let start_idx = self
|
||||||
|
.injection_ranges
|
||||||
|
.partition_point(|range| range.end < search_range.end);
|
||||||
|
|
||||||
|
self.injection_ranges[start_idx..]
|
||||||
|
.iter()
|
||||||
|
.take_while(|range| range.end == search_range.end)
|
||||||
|
.find_map(|range| (range.start == search_range.start).then_some(range.layer_id))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn goto_first_child_impl(&mut self, named: bool) -> bool {
|
||||||
|
// Check if the current node's range is an exact injection layer range.
|
||||||
|
if let Some(layer_id) = self
|
||||||
|
.layer_id_of_byte_range(self.node().byte_range())
|
||||||
|
.filter(|&layer_id| layer_id != self.current)
|
||||||
|
{
|
||||||
|
// Switch to the child layer.
|
||||||
|
self.current = layer_id;
|
||||||
|
self.cursor = self.layers[self.current].tree().root_node();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
let child = if named {
|
||||||
|
self.cursor.named_child(0)
|
||||||
|
} else {
|
||||||
|
self.cursor.child(0)
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(child) = child {
|
||||||
|
// Otherwise descend in the current tree.
|
||||||
|
self.cursor = child;
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn goto_first_child(&mut self) -> bool {
|
||||||
|
self.goto_first_child_impl(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn goto_first_named_child(&mut self) -> bool {
|
||||||
|
self.goto_first_child_impl(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn goto_next_sibling_impl(&mut self, named: bool) -> bool {
|
||||||
|
let sibling = if named {
|
||||||
|
self.cursor.next_named_sibling()
|
||||||
|
} else {
|
||||||
|
self.cursor.next_sibling()
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(sibling) = sibling {
|
||||||
|
self.cursor = sibling;
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn goto_next_sibling(&mut self) -> bool {
|
||||||
|
self.goto_next_sibling_impl(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn goto_next_named_sibling(&mut self) -> bool {
|
||||||
|
self.goto_next_sibling_impl(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn goto_prev_sibling_impl(&mut self, named: bool) -> bool {
|
||||||
|
let sibling = if named {
|
||||||
|
self.cursor.prev_named_sibling()
|
||||||
|
} else {
|
||||||
|
self.cursor.prev_sibling()
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(sibling) = sibling {
|
||||||
|
self.cursor = sibling;
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn goto_prev_sibling(&mut self) -> bool {
|
||||||
|
self.goto_prev_sibling_impl(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn goto_prev_named_sibling(&mut self) -> bool {
|
||||||
|
self.goto_prev_sibling_impl(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Finds the injection layer that contains the given start-end range.
|
||||||
|
fn layer_id_containing_byte_range(&self, start: usize, end: usize) -> LayerId {
|
||||||
|
let start_idx = self
|
||||||
|
.injection_ranges
|
||||||
|
.partition_point(|range| range.end < end);
|
||||||
|
|
||||||
|
self.injection_ranges[start_idx..]
|
||||||
|
.iter()
|
||||||
|
.take_while(|range| range.start < end)
|
||||||
|
.find_map(|range| (range.start <= start).then_some(range.layer_id))
|
||||||
|
.unwrap_or(self.root)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn reset_to_byte_range(&mut self, start: usize, end: usize) {
|
||||||
|
self.current = self.layer_id_containing_byte_range(start, end);
|
||||||
|
let root = self.layers[self.current].tree().root_node();
|
||||||
|
self.cursor = root.descendant_for_byte_range(start, end).unwrap_or(root);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns an iterator over the children of the node the TreeCursor is on
|
||||||
|
/// at the time this is called.
|
||||||
|
pub fn children(&'a mut self) -> ChildIter {
|
||||||
|
let parent = self.node();
|
||||||
|
|
||||||
|
ChildIter {
|
||||||
|
cursor: self,
|
||||||
|
parent,
|
||||||
|
named: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns an iterator over the named children of the node the TreeCursor is on
|
||||||
|
/// at the time this is called.
|
||||||
|
pub fn named_children(&'a mut self) -> ChildIter {
|
||||||
|
let parent = self.node();
|
||||||
|
|
||||||
|
ChildIter {
|
||||||
|
cursor: self,
|
||||||
|
parent,
|
||||||
|
named: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ChildIter<'n> {
|
||||||
|
cursor: &'n mut TreeCursor<'n>,
|
||||||
|
parent: Node<'n>,
|
||||||
|
named: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'n> Iterator for ChildIter<'n> {
|
||||||
|
type Item = Node<'n>;
|
||||||
|
|
||||||
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
|
// first iteration, just visit the first child
|
||||||
|
if self.cursor.node() == self.parent {
|
||||||
|
self.cursor
|
||||||
|
.goto_first_child_impl(self.named)
|
||||||
|
.then(|| self.cursor.node())
|
||||||
|
} else {
|
||||||
|
self.cursor
|
||||||
|
.goto_next_sibling_impl(self.named)
|
||||||
|
.then(|| self.cursor.node())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,459 @@
|
|||||||
|
//! From <https://github.com/Freaky/faccess>
|
||||||
|
|
||||||
|
use std::io;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
use bitflags::bitflags;
|
||||||
|
|
||||||
|
// Licensed under MIT from faccess
|
||||||
|
bitflags! {
|
||||||
|
/// Access mode flags for `access` function to test for.
|
||||||
|
pub struct AccessMode: u8 {
|
||||||
|
/// Path exists
|
||||||
|
const EXISTS = 0b0001;
|
||||||
|
/// Path can likely be read
|
||||||
|
const READ = 0b0010;
|
||||||
|
/// Path can likely be written to
|
||||||
|
const WRITE = 0b0100;
|
||||||
|
/// Path can likely be executed
|
||||||
|
const EXECUTE = 0b1000;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
mod imp {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
use rustix::fs::Access;
|
||||||
|
use std::os::unix::fs::{MetadataExt, PermissionsExt};
|
||||||
|
|
||||||
|
pub fn access(p: &Path, mode: AccessMode) -> io::Result<()> {
|
||||||
|
let mut imode = Access::empty();
|
||||||
|
|
||||||
|
if mode.contains(AccessMode::EXISTS) {
|
||||||
|
imode |= Access::EXISTS;
|
||||||
|
}
|
||||||
|
|
||||||
|
if mode.contains(AccessMode::READ) {
|
||||||
|
imode |= Access::READ_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
if mode.contains(AccessMode::WRITE) {
|
||||||
|
imode |= Access::WRITE_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
if mode.contains(AccessMode::EXECUTE) {
|
||||||
|
imode |= Access::EXEC_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
rustix::fs::access(p, imode)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn chown(p: &Path, uid: Option<u32>, gid: Option<u32>) -> io::Result<()> {
|
||||||
|
let uid = uid.map(|n| unsafe { rustix::fs::Uid::from_raw(n) });
|
||||||
|
let gid = gid.map(|n| unsafe { rustix::fs::Gid::from_raw(n) });
|
||||||
|
rustix::fs::chown(p, uid, gid)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn copy_metadata(from: &Path, to: &Path) -> io::Result<()> {
|
||||||
|
let from_meta = std::fs::metadata(from)?;
|
||||||
|
let to_meta = std::fs::metadata(to)?;
|
||||||
|
let from_gid = from_meta.gid();
|
||||||
|
let to_gid = to_meta.gid();
|
||||||
|
|
||||||
|
let mut perms = from_meta.permissions();
|
||||||
|
perms.set_mode(perms.mode() & 0o0777);
|
||||||
|
if from_gid != to_gid && chown(to, None, Some(from_gid)).is_err() {
|
||||||
|
let new_perms = (perms.mode() & 0o0707) | ((perms.mode() & 0o07) << 3);
|
||||||
|
perms.set_mode(new_perms);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::fs::set_permissions(to, perms)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Licensed under MIT from faccess except for `chown`, `copy_metadata` and `is_acl_inherited`
|
||||||
|
#[cfg(windows)]
|
||||||
|
mod imp {
|
||||||
|
|
||||||
|
use windows_sys::Win32::Foundation::{CloseHandle, LocalFree, ERROR_SUCCESS, HANDLE, PSID};
|
||||||
|
use windows_sys::Win32::Security::Authorization::{
|
||||||
|
GetNamedSecurityInfoW, SetNamedSecurityInfoW, SE_FILE_OBJECT,
|
||||||
|
};
|
||||||
|
use windows_sys::Win32::Security::{
|
||||||
|
AccessCheck, AclSizeInformation, GetAce, GetAclInformation, GetSidIdentifierAuthority,
|
||||||
|
ImpersonateSelf, IsValidAcl, IsValidSid, MapGenericMask, RevertToSelf,
|
||||||
|
SecurityImpersonation, ACCESS_ALLOWED_CALLBACK_ACE, ACL, ACL_SIZE_INFORMATION,
|
||||||
|
DACL_SECURITY_INFORMATION, GENERIC_MAPPING, GROUP_SECURITY_INFORMATION, INHERITED_ACE,
|
||||||
|
LABEL_SECURITY_INFORMATION, OBJECT_SECURITY_INFORMATION, OWNER_SECURITY_INFORMATION,
|
||||||
|
PRIVILEGE_SET, PROTECTED_DACL_SECURITY_INFORMATION, PSECURITY_DESCRIPTOR,
|
||||||
|
SID_IDENTIFIER_AUTHORITY, TOKEN_DUPLICATE, TOKEN_QUERY,
|
||||||
|
};
|
||||||
|
use windows_sys::Win32::Storage::FileSystem::{
|
||||||
|
FILE_ACCESS_RIGHTS, FILE_ALL_ACCESS, FILE_GENERIC_EXECUTE, FILE_GENERIC_READ,
|
||||||
|
FILE_GENERIC_WRITE,
|
||||||
|
};
|
||||||
|
use windows_sys::Win32::System::Threading::{GetCurrentThread, OpenThreadToken};
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
use std::ffi::c_void;
|
||||||
|
|
||||||
|
use std::os::windows::{ffi::OsStrExt, fs::OpenOptionsExt};
|
||||||
|
|
||||||
|
struct SecurityDescriptor {
|
||||||
|
sd: PSECURITY_DESCRIPTOR,
|
||||||
|
owner: PSID,
|
||||||
|
group: PSID,
|
||||||
|
dacl: *mut ACL,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for SecurityDescriptor {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
if !self.sd.is_null() {
|
||||||
|
unsafe {
|
||||||
|
LocalFree(self.sd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SecurityDescriptor {
|
||||||
|
fn for_path(p: &Path) -> io::Result<SecurityDescriptor> {
|
||||||
|
let path = std::fs::canonicalize(p)?;
|
||||||
|
let pathos = path.into_os_string();
|
||||||
|
let mut pathw: Vec<u16> = Vec::with_capacity(pathos.len() + 1);
|
||||||
|
pathw.extend(pathos.encode_wide());
|
||||||
|
pathw.push(0);
|
||||||
|
|
||||||
|
let mut sd = std::ptr::null_mut();
|
||||||
|
let mut owner = std::ptr::null_mut();
|
||||||
|
let mut group = std::ptr::null_mut();
|
||||||
|
let mut dacl = std::ptr::null_mut();
|
||||||
|
|
||||||
|
let err = unsafe {
|
||||||
|
GetNamedSecurityInfoW(
|
||||||
|
pathw.as_ptr(),
|
||||||
|
SE_FILE_OBJECT,
|
||||||
|
OWNER_SECURITY_INFORMATION
|
||||||
|
| GROUP_SECURITY_INFORMATION
|
||||||
|
| DACL_SECURITY_INFORMATION
|
||||||
|
| LABEL_SECURITY_INFORMATION,
|
||||||
|
&mut owner,
|
||||||
|
&mut group,
|
||||||
|
&mut dacl,
|
||||||
|
std::ptr::null_mut(),
|
||||||
|
&mut sd,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
if err == ERROR_SUCCESS {
|
||||||
|
Ok(SecurityDescriptor {
|
||||||
|
sd,
|
||||||
|
owner,
|
||||||
|
group,
|
||||||
|
dacl,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
Err(io::Error::last_os_error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_acl_inherited(&self) -> bool {
|
||||||
|
let mut acl_info: ACL_SIZE_INFORMATION = unsafe { ::core::mem::zeroed() };
|
||||||
|
let acl_info_ptr: *mut c_void = &mut acl_info as *mut _ as *mut c_void;
|
||||||
|
let mut ace: ACCESS_ALLOWED_CALLBACK_ACE = unsafe { ::core::mem::zeroed() };
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
GetAclInformation(
|
||||||
|
self.dacl,
|
||||||
|
acl_info_ptr,
|
||||||
|
std::mem::size_of_val(&acl_info) as u32,
|
||||||
|
AclSizeInformation,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
for i in 0..acl_info.AceCount {
|
||||||
|
let mut ptr = &mut ace as *mut _ as *mut c_void;
|
||||||
|
unsafe { GetAce(self.dacl, i, &mut ptr) };
|
||||||
|
if (ace.Header.AceFlags as u32 & INHERITED_ACE) != 0 {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn descriptor(&self) -> &PSECURITY_DESCRIPTOR {
|
||||||
|
&self.sd
|
||||||
|
}
|
||||||
|
|
||||||
|
fn owner(&self) -> &PSID {
|
||||||
|
&self.owner
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ThreadToken(HANDLE);
|
||||||
|
impl Drop for ThreadToken {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
unsafe {
|
||||||
|
CloseHandle(self.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ThreadToken {
|
||||||
|
fn new() -> io::Result<Self> {
|
||||||
|
unsafe {
|
||||||
|
if ImpersonateSelf(SecurityImpersonation) == 0 {
|
||||||
|
return Err(io::Error::last_os_error());
|
||||||
|
}
|
||||||
|
|
||||||
|
let token: *mut HANDLE = std::ptr::null_mut();
|
||||||
|
let err =
|
||||||
|
OpenThreadToken(GetCurrentThread(), TOKEN_DUPLICATE | TOKEN_QUERY, 0, token);
|
||||||
|
|
||||||
|
RevertToSelf();
|
||||||
|
|
||||||
|
if err == 0 {
|
||||||
|
return Err(io::Error::last_os_error());
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Self(*token))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn as_handle(&self) -> &HANDLE {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Based roughly on Tcl's NativeAccess()
|
||||||
|
// https://github.com/tcltk/tcl/blob/2ee77587e4dc2150deb06b48f69db948b4ab0584/win/tclWinFile.c
|
||||||
|
fn eaccess(p: &Path, mut mode: FILE_ACCESS_RIGHTS) -> io::Result<()> {
|
||||||
|
let md = p.metadata()?;
|
||||||
|
|
||||||
|
if !md.is_dir() {
|
||||||
|
// Read Only is ignored for directories
|
||||||
|
if mode & FILE_GENERIC_WRITE == FILE_GENERIC_WRITE && md.permissions().readonly() {
|
||||||
|
return Err(io::Error::new(
|
||||||
|
io::ErrorKind::PermissionDenied,
|
||||||
|
"File is read only",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// If it doesn't have the correct extension it isn't executable
|
||||||
|
if mode & FILE_GENERIC_EXECUTE == FILE_GENERIC_EXECUTE {
|
||||||
|
if let Some(ext) = p.extension().and_then(|s| s.to_str()) {
|
||||||
|
match ext {
|
||||||
|
"exe" | "com" | "bat" | "cmd" => (),
|
||||||
|
_ => {
|
||||||
|
return Err(io::Error::new(
|
||||||
|
io::ErrorKind::InvalidData,
|
||||||
|
"File not executable",
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return std::fs::OpenOptions::new()
|
||||||
|
.access_mode(mode)
|
||||||
|
.open(p)
|
||||||
|
.map(|_| ());
|
||||||
|
}
|
||||||
|
|
||||||
|
let sd = SecurityDescriptor::for_path(p)?;
|
||||||
|
|
||||||
|
// Unmapped Samba users are assigned a top level authority of 22
|
||||||
|
// ACL tests are likely to be misleading
|
||||||
|
const SAMBA_UNMAPPED: SID_IDENTIFIER_AUTHORITY = SID_IDENTIFIER_AUTHORITY {
|
||||||
|
Value: [0, 0, 0, 0, 0, 22],
|
||||||
|
};
|
||||||
|
unsafe {
|
||||||
|
let owner = sd.owner();
|
||||||
|
if IsValidSid(*owner) != 0
|
||||||
|
&& (*GetSidIdentifierAuthority(*owner)).Value == SAMBA_UNMAPPED.Value
|
||||||
|
{
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let token = ThreadToken::new()?;
|
||||||
|
|
||||||
|
let mut privileges: PRIVILEGE_SET = unsafe { std::mem::zeroed() };
|
||||||
|
let mut granted_access: u32 = 0;
|
||||||
|
let mut privileges_length = std::mem::size_of::<PRIVILEGE_SET>() as u32;
|
||||||
|
let mut result = 0;
|
||||||
|
|
||||||
|
let mut mapping = GENERIC_MAPPING {
|
||||||
|
GenericRead: FILE_GENERIC_READ,
|
||||||
|
GenericWrite: FILE_GENERIC_WRITE,
|
||||||
|
GenericExecute: FILE_GENERIC_EXECUTE,
|
||||||
|
GenericAll: FILE_ALL_ACCESS,
|
||||||
|
};
|
||||||
|
|
||||||
|
unsafe { MapGenericMask(&mut mode, &mut mapping) };
|
||||||
|
|
||||||
|
if unsafe {
|
||||||
|
AccessCheck(
|
||||||
|
*sd.descriptor(),
|
||||||
|
*token.as_handle(),
|
||||||
|
mode,
|
||||||
|
&mut mapping,
|
||||||
|
&mut privileges,
|
||||||
|
&mut privileges_length,
|
||||||
|
&mut granted_access,
|
||||||
|
&mut result,
|
||||||
|
)
|
||||||
|
} != 0
|
||||||
|
{
|
||||||
|
if result == 0 {
|
||||||
|
Err(io::Error::new(
|
||||||
|
io::ErrorKind::PermissionDenied,
|
||||||
|
"Permission Denied",
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Err(io::Error::last_os_error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn access(p: &Path, mode: AccessMode) -> io::Result<()> {
|
||||||
|
let mut imode = 0;
|
||||||
|
|
||||||
|
if mode.contains(AccessMode::READ) {
|
||||||
|
imode |= FILE_GENERIC_READ;
|
||||||
|
}
|
||||||
|
|
||||||
|
if mode.contains(AccessMode::WRITE) {
|
||||||
|
imode |= FILE_GENERIC_WRITE;
|
||||||
|
}
|
||||||
|
|
||||||
|
if mode.contains(AccessMode::EXECUTE) {
|
||||||
|
imode |= FILE_GENERIC_EXECUTE;
|
||||||
|
}
|
||||||
|
|
||||||
|
if imode == 0 {
|
||||||
|
if p.exists() {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(io::Error::new(io::ErrorKind::NotFound, "Not Found"))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
eaccess(p, imode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn chown(p: &Path, sd: SecurityDescriptor) -> io::Result<()> {
|
||||||
|
let path = std::fs::canonicalize(p)?;
|
||||||
|
let pathos = path.as_os_str();
|
||||||
|
let mut pathw = Vec::with_capacity(pathos.len() + 1);
|
||||||
|
pathw.extend(pathos.encode_wide());
|
||||||
|
pathw.push(0);
|
||||||
|
|
||||||
|
let mut owner = std::ptr::null_mut();
|
||||||
|
let mut group = std::ptr::null_mut();
|
||||||
|
let mut dacl = std::ptr::null();
|
||||||
|
|
||||||
|
let mut si = OBJECT_SECURITY_INFORMATION::default();
|
||||||
|
if unsafe { IsValidSid(sd.owner) } != 0 {
|
||||||
|
si |= OWNER_SECURITY_INFORMATION;
|
||||||
|
owner = sd.owner;
|
||||||
|
}
|
||||||
|
|
||||||
|
if unsafe { IsValidSid(sd.group) } != 0 {
|
||||||
|
si |= GROUP_SECURITY_INFORMATION;
|
||||||
|
group = sd.group;
|
||||||
|
}
|
||||||
|
|
||||||
|
if unsafe { IsValidAcl(sd.dacl) } != 0 {
|
||||||
|
si |= DACL_SECURITY_INFORMATION;
|
||||||
|
if !sd.is_acl_inherited() {
|
||||||
|
si |= PROTECTED_DACL_SECURITY_INFORMATION;
|
||||||
|
}
|
||||||
|
dacl = sd.dacl as *const _;
|
||||||
|
}
|
||||||
|
|
||||||
|
let err = unsafe {
|
||||||
|
SetNamedSecurityInfoW(
|
||||||
|
pathw.as_ptr(),
|
||||||
|
SE_FILE_OBJECT,
|
||||||
|
si,
|
||||||
|
owner,
|
||||||
|
group,
|
||||||
|
dacl,
|
||||||
|
std::ptr::null(),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
if err == ERROR_SUCCESS {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(io::Error::last_os_error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn copy_metadata(from: &Path, to: &Path) -> io::Result<()> {
|
||||||
|
let sd = SecurityDescriptor::for_path(from)?;
|
||||||
|
chown(to, sd)?;
|
||||||
|
|
||||||
|
let meta = std::fs::metadata(from)?;
|
||||||
|
let perms = meta.permissions();
|
||||||
|
|
||||||
|
std::fs::set_permissions(to, perms)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Licensed under MIT from faccess except for `copy_metadata`
|
||||||
|
#[cfg(not(any(unix, windows)))]
|
||||||
|
mod imp {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
pub fn access(p: &Path, mode: AccessMode) -> io::Result<()> {
|
||||||
|
if mode.contains(AccessMode::WRITE) {
|
||||||
|
if std::fs::metadata(p)?.permissions().readonly() {
|
||||||
|
return Err(io::Error::new(
|
||||||
|
io::ErrorKind::PermissionDenied,
|
||||||
|
"Path is read only",
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.exists() {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(io::Error::new(io::ErrorKind::NotFound, "Path not found"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn copy_metadata(from: &path, to: &Path) -> io::Result<()> {
|
||||||
|
let meta = std::fs::metadata(from)?;
|
||||||
|
let perms = meta.permissions();
|
||||||
|
std::fs::set_permissions(to, perms)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn readonly(p: &Path) -> bool {
|
||||||
|
match imp::access(p, AccessMode::WRITE) {
|
||||||
|
Ok(_) => false,
|
||||||
|
Err(err) if err.kind() == std::io::ErrorKind::NotFound => false,
|
||||||
|
Err(_) => true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn copy_metadata(from: &Path, to: &Path) -> io::Result<()> {
|
||||||
|
imp::copy_metadata(from, to)
|
||||||
|
}
|
@ -1,3 +1,4 @@
|
|||||||
pub mod env;
|
pub mod env;
|
||||||
|
pub mod faccess;
|
||||||
pub mod path;
|
pub mod path;
|
||||||
pub mod rope;
|
pub mod rope;
|
||||||
|
@ -0,0 +1,153 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use helix_lsp::lsp;
|
||||||
|
use tokio::sync::mpsc::Sender;
|
||||||
|
use tokio::time::{Duration, Instant};
|
||||||
|
|
||||||
|
use helix_event::{send_blocking, AsyncHook, CancelRx};
|
||||||
|
use helix_view::Editor;
|
||||||
|
|
||||||
|
use crate::handlers::completion::CompletionItem;
|
||||||
|
use crate::job;
|
||||||
|
|
||||||
|
/// A hook for resolving incomplete completion items.
|
||||||
|
///
|
||||||
|
/// From the [LSP spec](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_completion):
|
||||||
|
///
|
||||||
|
/// > If computing full completion items is expensive, servers can additionally provide a
|
||||||
|
/// > handler for the completion item resolve request. ...
|
||||||
|
/// > A typical use case is for example: the `textDocument/completion` request doesn't fill
|
||||||
|
/// > in the `documentation` property for returned completion items since it is expensive
|
||||||
|
/// > to compute. When the item is selected in the user interface then a
|
||||||
|
/// > 'completionItem/resolve' request is sent with the selected completion item as a parameter.
|
||||||
|
/// > The returned completion item should have the documentation property filled in.
|
||||||
|
pub struct ResolveHandler {
|
||||||
|
last_request: Option<Arc<CompletionItem>>,
|
||||||
|
resolver: Sender<ResolveRequest>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ResolveHandler {
|
||||||
|
pub fn new() -> ResolveHandler {
|
||||||
|
ResolveHandler {
|
||||||
|
last_request: None,
|
||||||
|
resolver: ResolveTimeout {
|
||||||
|
next_request: None,
|
||||||
|
in_flight: None,
|
||||||
|
}
|
||||||
|
.spawn(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ensure_item_resolved(&mut self, editor: &mut Editor, item: &mut CompletionItem) {
|
||||||
|
if item.resolved {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let needs_resolve = item.item.documentation.is_none()
|
||||||
|
|| item.item.detail.is_none()
|
||||||
|
|| item.item.additional_text_edits.is_none();
|
||||||
|
if !needs_resolve {
|
||||||
|
item.resolved = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if self.last_request.as_deref().is_some_and(|it| it == item) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let Some(ls) = editor.language_servers.get_by_id(item.provider).cloned() else {
|
||||||
|
item.resolved = true;
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if matches!(
|
||||||
|
ls.capabilities().completion_provider,
|
||||||
|
Some(lsp::CompletionOptions {
|
||||||
|
resolve_provider: Some(true),
|
||||||
|
..
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
let item = Arc::new(item.clone());
|
||||||
|
self.last_request = Some(item.clone());
|
||||||
|
send_blocking(&self.resolver, ResolveRequest { item, ls })
|
||||||
|
} else {
|
||||||
|
item.resolved = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ResolveRequest {
|
||||||
|
item: Arc<CompletionItem>,
|
||||||
|
ls: Arc<helix_lsp::Client>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
struct ResolveTimeout {
|
||||||
|
next_request: Option<ResolveRequest>,
|
||||||
|
in_flight: Option<(helix_event::CancelTx, Arc<CompletionItem>)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsyncHook for ResolveTimeout {
|
||||||
|
type Event = ResolveRequest;
|
||||||
|
|
||||||
|
fn handle_event(
|
||||||
|
&mut self,
|
||||||
|
request: Self::Event,
|
||||||
|
timeout: Option<tokio::time::Instant>,
|
||||||
|
) -> Option<tokio::time::Instant> {
|
||||||
|
if self
|
||||||
|
.next_request
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|old_request| old_request.item == request.item)
|
||||||
|
{
|
||||||
|
timeout
|
||||||
|
} else if self
|
||||||
|
.in_flight
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|(_, old_request)| old_request.item == request.item.item)
|
||||||
|
{
|
||||||
|
self.next_request = None;
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
self.next_request = Some(request);
|
||||||
|
Some(Instant::now() + Duration::from_millis(150))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn finish_debounce(&mut self) {
|
||||||
|
let Some(request) = self.next_request.take() else { return };
|
||||||
|
let (tx, rx) = helix_event::cancelation();
|
||||||
|
self.in_flight = Some((tx, request.item.clone()));
|
||||||
|
tokio::spawn(request.execute(rx));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ResolveRequest {
|
||||||
|
async fn execute(self, cancel: CancelRx) {
|
||||||
|
let future = self.ls.resolve_completion_item(&self.item.item);
|
||||||
|
let Some(resolved_item) = helix_event::cancelable_future(future, cancel).await else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
job::dispatch(move |_, compositor| {
|
||||||
|
if let Some(completion) = &mut compositor
|
||||||
|
.find::<crate::ui::EditorView>()
|
||||||
|
.unwrap()
|
||||||
|
.completion
|
||||||
|
{
|
||||||
|
let resolved_item = match resolved_item {
|
||||||
|
Ok(item) => CompletionItem {
|
||||||
|
item,
|
||||||
|
resolved: true,
|
||||||
|
..*self.item
|
||||||
|
},
|
||||||
|
Err(err) => {
|
||||||
|
log::error!("completion resolve request failed: {err}");
|
||||||
|
// set item to resolved so we don't request it again
|
||||||
|
// we could also remove it but that oculd be odd ui
|
||||||
|
let mut item = (*self.item).clone();
|
||||||
|
item.resolved = true;
|
||||||
|
item
|
||||||
|
}
|
||||||
|
};
|
||||||
|
completion.replace_item(&self.item, resolved_item);
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,32 @@
|
|||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
pub enum FileChange {
|
||||||
|
Untracked {
|
||||||
|
path: PathBuf,
|
||||||
|
},
|
||||||
|
Modified {
|
||||||
|
path: PathBuf,
|
||||||
|
},
|
||||||
|
Conflict {
|
||||||
|
path: PathBuf,
|
||||||
|
},
|
||||||
|
Deleted {
|
||||||
|
path: PathBuf,
|
||||||
|
},
|
||||||
|
Renamed {
|
||||||
|
from_path: PathBuf,
|
||||||
|
to_path: PathBuf,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FileChange {
|
||||||
|
pub fn path(&self) -> &Path {
|
||||||
|
match self {
|
||||||
|
Self::Untracked { path } => path,
|
||||||
|
Self::Modified { path } => path,
|
||||||
|
Self::Conflict { path } => path,
|
||||||
|
Self::Deleted { path } => path,
|
||||||
|
Self::Renamed { to_path, .. } => to_path,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,37 @@
|
|||||||
|
; adl
|
||||||
|
|
||||||
|
[
|
||||||
|
"module"
|
||||||
|
"struct"
|
||||||
|
"union"
|
||||||
|
"type"
|
||||||
|
"newtype"
|
||||||
|
"annotation"
|
||||||
|
] @keyword
|
||||||
|
|
||||||
|
(adl (scoped_name)) @namespace
|
||||||
|
(comment) @comment
|
||||||
|
(doc_comment) @comment.block.documentation
|
||||||
|
(name) @type
|
||||||
|
|
||||||
|
(fname) @variable.other.member
|
||||||
|
|
||||||
|
(type_expr (scoped_name) @type)
|
||||||
|
|
||||||
|
(type_expr_params (param (scoped_name) @type.parameter))
|
||||||
|
|
||||||
|
; json
|
||||||
|
(key) @string.special
|
||||||
|
|
||||||
|
(string) @string
|
||||||
|
|
||||||
|
(number) @constant.numeric
|
||||||
|
|
||||||
|
[
|
||||||
|
(null)
|
||||||
|
(true)
|
||||||
|
(false)
|
||||||
|
] @constant.builtin
|
||||||
|
|
||||||
|
(escape_sequence) @constant.character.escape
|
||||||
|
|
@ -0,0 +1,12 @@
|
|||||||
|
[
|
||||||
|
(struct)
|
||||||
|
(union)
|
||||||
|
|
||||||
|
(array)
|
||||||
|
(object)
|
||||||
|
] @indent
|
||||||
|
|
||||||
|
; [
|
||||||
|
; "}"
|
||||||
|
; "]"
|
||||||
|
; ] @outdent
|
@ -0,0 +1 @@
|
|||||||
|
(struct (_) @function.inside) @funtion.around
|
@ -0,0 +1,82 @@
|
|||||||
|
|
||||||
|
; variables
|
||||||
|
(variable_assignment (identifier) @variable.other.member)
|
||||||
|
(variable_assignment (concatenation (identifier) @variable.other.member))
|
||||||
|
(unset_statement (identifier) @variable.other.member)
|
||||||
|
(export_statement (identifier) @variable.other.member)
|
||||||
|
(variable_expansion (identifier) @variable.other.member)
|
||||||
|
(python_function_definition (parameters (python_identifier) @variable.other.member))
|
||||||
|
|
||||||
|
(variable_assignment (override) @keyword.storage.modifier)
|
||||||
|
(overrides_statement (identifier) @keyword.storage.modifier)
|
||||||
|
(flag) @keyword.storage.modifier
|
||||||
|
|
||||||
|
[
|
||||||
|
"="
|
||||||
|
"?="
|
||||||
|
"??="
|
||||||
|
":="
|
||||||
|
"=+"
|
||||||
|
"+="
|
||||||
|
".="
|
||||||
|
"=."
|
||||||
|
|
||||||
|
] @operator
|
||||||
|
|
||||||
|
(variable_expansion [ "${" "}" ] @punctuation.special)
|
||||||
|
[ "(" ")" "{" "}" "[" "]" ] @punctuation.bracket
|
||||||
|
|
||||||
|
[
|
||||||
|
"noexec"
|
||||||
|
"INHERIT"
|
||||||
|
"OVERRIDES"
|
||||||
|
"$BB_ENV_PASSTHROUGH"
|
||||||
|
"$BB_ENV_PASSTHROUGH_ADDITIONS"
|
||||||
|
] @variable.builtin
|
||||||
|
|
||||||
|
; functions
|
||||||
|
|
||||||
|
(python_function_definition (python_identifier) @function)
|
||||||
|
(anonymous_python_function (identifier) @function)
|
||||||
|
(function_definition (identifier) @function)
|
||||||
|
(export_functions_statement (identifier) @function)
|
||||||
|
(addtask_statement (identifier) @function)
|
||||||
|
(deltask_statement (identifier) @function)
|
||||||
|
(addhandler_statement (identifier) @function)
|
||||||
|
(function_definition (override) @keyword.storage.modifier)
|
||||||
|
|
||||||
|
[
|
||||||
|
"addtask"
|
||||||
|
"deltask"
|
||||||
|
"addhandler"
|
||||||
|
"unset"
|
||||||
|
"EXPORT_FUNCTIONS"
|
||||||
|
"python"
|
||||||
|
"def"
|
||||||
|
] @keyword.function
|
||||||
|
|
||||||
|
[
|
||||||
|
"append"
|
||||||
|
"prepend"
|
||||||
|
"remove"
|
||||||
|
|
||||||
|
"before"
|
||||||
|
"after"
|
||||||
|
] @keyword.operator
|
||||||
|
|
||||||
|
; imports
|
||||||
|
|
||||||
|
[
|
||||||
|
"inherit"
|
||||||
|
"include"
|
||||||
|
"require"
|
||||||
|
"export"
|
||||||
|
"import"
|
||||||
|
] @keyword.control.import
|
||||||
|
|
||||||
|
(inherit_path) @namespace
|
||||||
|
(include_path) @namespace
|
||||||
|
|
||||||
|
|
||||||
|
(string) @string
|
||||||
|
(comment) @comment
|
@ -0,0 +1,18 @@
|
|||||||
|
((python_function_definition) @injection.content
|
||||||
|
(#set! injection.language "python")
|
||||||
|
(#set! injection.include-children))
|
||||||
|
|
||||||
|
((anonymous_python_function (block) @injection.content)
|
||||||
|
(#set! injection.language "python")
|
||||||
|
(#set! injection.include-children))
|
||||||
|
|
||||||
|
((inline_python) @injection.content
|
||||||
|
(#set! injection.language "python")
|
||||||
|
(#set! injection.include-children))
|
||||||
|
|
||||||
|
((function_definition) @injection.content
|
||||||
|
(#set! injection.language "bash")
|
||||||
|
(#set! injection.include-children))
|
||||||
|
|
||||||
|
((comment) @injection.content
|
||||||
|
(#set! injection.language "comment"))
|
@ -0,0 +1,74 @@
|
|||||||
|
(string_array "," @punctuation.delimiter)
|
||||||
|
(string_array ["[" "]"] @punctuation.bracket)
|
||||||
|
|
||||||
|
[
|
||||||
|
"ARG"
|
||||||
|
"AS LOCAL"
|
||||||
|
"BUILD"
|
||||||
|
"CACHE"
|
||||||
|
"CMD"
|
||||||
|
"COPY"
|
||||||
|
"DO"
|
||||||
|
"ENTRYPOINT"
|
||||||
|
"ENV"
|
||||||
|
"EXPOSE"
|
||||||
|
"FROM DOCKERFILE"
|
||||||
|
"FROM"
|
||||||
|
"FUNCTION"
|
||||||
|
"GIT CLONE"
|
||||||
|
"HOST"
|
||||||
|
"IMPORT"
|
||||||
|
"LABEL"
|
||||||
|
"LET"
|
||||||
|
"PROJECT"
|
||||||
|
"RUN"
|
||||||
|
"SAVE ARTIFACT"
|
||||||
|
"SAVE IMAGE"
|
||||||
|
"SET"
|
||||||
|
"USER"
|
||||||
|
"VERSION"
|
||||||
|
"VOLUME"
|
||||||
|
"WORKDIR"
|
||||||
|
] @keyword
|
||||||
|
|
||||||
|
(for_command ["FOR" "IN" "END"] @keyword.control.repeat)
|
||||||
|
|
||||||
|
(if_command ["IF" "END"] @keyword.control.conditional)
|
||||||
|
(elif_block ["ELSE IF"] @keyword.control.conditional)
|
||||||
|
(else_block ["ELSE"] @keyword.control.conditional)
|
||||||
|
|
||||||
|
(import_command ["IMPORT" "AS"] @keyword.control.import)
|
||||||
|
|
||||||
|
(try_command ["TRY" "FINALLY" "END"] @keyword.control.exception)
|
||||||
|
|
||||||
|
(wait_command ["WAIT" "END"] @keyword.control)
|
||||||
|
(with_docker_command ["WITH DOCKER" "END"] @keyword.control)
|
||||||
|
|
||||||
|
[
|
||||||
|
(comment)
|
||||||
|
(line_continuation_comment)
|
||||||
|
] @comment
|
||||||
|
|
||||||
|
(line_continuation) @operator
|
||||||
|
|
||||||
|
[
|
||||||
|
(target_ref)
|
||||||
|
(target_artifact)
|
||||||
|
(function_ref)
|
||||||
|
] @function
|
||||||
|
|
||||||
|
(target (identifier) @function)
|
||||||
|
|
||||||
|
[
|
||||||
|
(double_quoted_string)
|
||||||
|
(single_quoted_string)
|
||||||
|
] @string
|
||||||
|
(unquoted_string) @string.special
|
||||||
|
(escape_sequence) @constant.character.escape
|
||||||
|
|
||||||
|
(variable) @variable
|
||||||
|
(expansion ["$" "{" "}" "(" ")"] @punctuation.special)
|
||||||
|
(build_arg) @variable
|
||||||
|
(options (_) @variable.parameter)
|
||||||
|
|
||||||
|
"=" @operator
|
@ -0,0 +1 @@
|
|||||||
|
(target) @indent
|
@ -0,0 +1,9 @@
|
|||||||
|
((comment) @injection.content
|
||||||
|
(#set! injection.language "comment"))
|
||||||
|
|
||||||
|
((line_continuation_comment) @injection.content
|
||||||
|
(#set! injection.language "comment"))
|
||||||
|
|
||||||
|
((shell_fragment) @injection.content
|
||||||
|
(#set! injection.language "bash")
|
||||||
|
(#set! injection.include-children))
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue