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 tree_sitter::Node;
|
||||
use crate::{movement::Direction, syntax::TreeCursor, Range, RopeSlice, Selection, Syntax};
|
||||
|
||||
pub fn expand_selection(syntax: &Syntax, text: RopeSlice, selection: Selection) -> Selection {
|
||||
select_node_impl(syntax, text, selection, |mut node, from, to| {
|
||||
while node.start_byte() == from && node.end_byte() == to {
|
||||
node = node.parent()?;
|
||||
let cursor = &mut syntax.walk();
|
||||
|
||||
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 {
|
||||
select_node_impl(syntax, text, selection, |descendant, _from, _to| {
|
||||
descendant.child(0).or(Some(descendant))
|
||||
})
|
||||
select_node_impl(
|
||||
syntax,
|
||||
text,
|
||||
selection,
|
||||
|cursor| {
|
||||
cursor.goto_first_child();
|
||||
},
|
||||
None,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn select_sibling<F>(
|
||||
syntax: &Syntax,
|
||||
text: RopeSlice,
|
||||
selection: Selection,
|
||||
sibling_fn: &F,
|
||||
) -> Selection
|
||||
where
|
||||
F: Fn(Node) -> Option<Node>,
|
||||
{
|
||||
select_node_impl(syntax, text, selection, |descendant, _from, _to| {
|
||||
find_sibling_recursive(descendant, sibling_fn)
|
||||
pub fn select_next_sibling(syntax: &Syntax, text: RopeSlice, selection: Selection) -> Selection {
|
||||
select_node_impl(
|
||||
syntax,
|
||||
text,
|
||||
selection,
|
||||
|cursor| {
|
||||
while !cursor.goto_next_sibling() {
|
||||
if !cursor.goto_parent() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
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>
|
||||
where
|
||||
F: Fn(Node) -> Option<Node>,
|
||||
{
|
||||
sibling_fn(node).or_else(|| {
|
||||
node.parent()
|
||||
.and_then(|node| find_sibling_recursive(node, sibling_fn))
|
||||
pub fn select_all_children(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);
|
||||
select_children(&mut cursor, text, range).into_iter()
|
||||
})
|
||||
}
|
||||
|
||||
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>(
|
||||
syntax: &Syntax,
|
||||
text: RopeSlice,
|
||||
selection: Selection,
|
||||
select_fn: F,
|
||||
motion: F,
|
||||
direction: Option<Direction>,
|
||||
) -> Selection
|
||||
where
|
||||
F: Fn(Node, usize, usize) -> Option<Node>,
|
||||
F: Fn(&mut TreeCursor),
|
||||
{
|
||||
let tree = syntax.tree();
|
||||
let cursor = &mut syntax.walk();
|
||||
|
||||
selection.transform(|range| {
|
||||
let from = text.char_to_byte(range.from());
|
||||
let to = text.char_to_byte(range.to());
|
||||
|
||||
let node = match tree
|
||||
.root_node()
|
||||
.descendant_for_byte_range(from, to)
|
||||
.and_then(|node| select_fn(node, from, to))
|
||||
{
|
||||
Some(node) => node,
|
||||
None => return range,
|
||||
};
|
||||
cursor.reset_to_byte_range(from, to);
|
||||
|
||||
motion(cursor);
|
||||
|
||||
let node = cursor.node();
|
||||
let from = text.byte_to_char(node.start_byte());
|
||||
let to = text.byte_to_char(node.end_byte());
|
||||
|
||||
if range.head < range.anchor {
|
||||
Range::new(to, from)
|
||||
} else {
|
||||
Range::new(from, to)
|
||||
}
|
||||
Range::new(from, to).with_direction(direction.unwrap_or_else(|| range.direction()))
|
||||
})
|
||||
}
|
||||
|
@ -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 faccess;
|
||||
pub mod path;
|
||||
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