test rust lsp
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
[package]
|
||||
name = "analysis"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
anyhow = { workspace = true }
|
||||
lsp-types = { workspace = true }
|
||||
tree-sitter = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
use serde::{ Deserialize, Serialize };
|
||||
use std::collections::HashMap;
|
||||
use tree_sitter::{ Node, Tree };
|
||||
use lsp_types as lsp;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ProcDef {
|
||||
pub name: String,
|
||||
pub params: Vec<String>,
|
||||
pub byte_start: usize,
|
||||
pub byte_end: usize,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct ProcIndex {
|
||||
by_uri: HashMap<lsp::Uri, Vec<ProcDef>>,
|
||||
}
|
||||
|
||||
impl ProcIndex {
|
||||
pub fn insert(&mut self, uri: lsp::Uri, procs: Vec<ProcDef>) {
|
||||
self.by_uri.insert(uri, procs);
|
||||
}
|
||||
pub fn get(&self, uri: &lsp::Uri) -> Option<&[ProcDef]> {
|
||||
self.by_uri.get(uri).map(|v| v.as_slice())
|
||||
}
|
||||
pub fn remove(&mut self, uri: &lsp::Uri) {
|
||||
self.by_uri.remove(uri);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn collect_procs(tree: &Tree, source: &str) -> Vec<ProcDef> {
|
||||
let root = tree.root_node();
|
||||
let mut out = Vec::new();
|
||||
collect_from_node(root, source.as_bytes(), &mut out);
|
||||
out
|
||||
}
|
||||
|
||||
fn collect_from_node(node: Node, src: &[u8], out: &mut Vec<ProcDef>) {
|
||||
let mut stack = vec![node];
|
||||
let mut cursor = node.walk();
|
||||
|
||||
while let Some(n) = stack.pop() {
|
||||
if n.kind() == "command" {
|
||||
if let Some(p) = try_extract_proc(n, src) {
|
||||
out.push(p);
|
||||
}
|
||||
}
|
||||
let mut it = n.children(&mut cursor);
|
||||
while let Some(c) = it.next() {
|
||||
stack.push(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn try_extract_proc(cmd: Node, src: &[u8]) -> Option<ProcDef> {
|
||||
// Gather word-like named children
|
||||
let mut cursor = cmd.walk();
|
||||
let mut words: Vec<(Node, String)> = Vec::new();
|
||||
for ch in cmd.named_children(&mut cursor) {
|
||||
match ch.kind() {
|
||||
"word" | "braced_word" | "quoted_word" => {
|
||||
if let Ok(t) = ch.utf8_text(src) {
|
||||
words.push((ch, t.to_string()));
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
if words.len() < 2 {
|
||||
return None;
|
||||
}
|
||||
if words[0].1 != "proc" {
|
||||
return None;
|
||||
}
|
||||
|
||||
let name = words[1].1.trim().to_string();
|
||||
let params = if words.len() >= 3 { parse_params(&words[2].0, &words[2].1) } else { vec![] };
|
||||
|
||||
Some(ProcDef {
|
||||
name,
|
||||
params,
|
||||
byte_start: cmd.start_byte(),
|
||||
byte_end: cmd.end_byte(),
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_params(node: &Node, text: &str) -> Vec<String> {
|
||||
// naive split; good enough for a starter
|
||||
let is_braced =
|
||||
node.kind() == "braced_word" &&
|
||||
text.starts_with('{') &&
|
||||
text.ends_with('}') &&
|
||||
text.len() >= 2;
|
||||
let inner = if is_braced { &text[1..text.len() - 1] } else { text };
|
||||
inner
|
||||
.split_whitespace()
|
||||
.map(|s| s.to_string())
|
||||
.collect()
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
[package]
|
||||
name = "features"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
anyhow = { workspace = true }
|
||||
lsp-types = { workspace = true }
|
||||
analysis = { path = "../analysis" }
|
||||
text = { path = "../text" }
|
||||
url = { workspace = true }
|
||||
@@ -0,0 +1,31 @@
|
||||
use anyhow::Result;
|
||||
use analysis::ProcIndex;
|
||||
use lsp_types as lsp;
|
||||
use text::DocumentStore;
|
||||
|
||||
pub fn completion(
|
||||
docs: &DocumentStore,
|
||||
index: &ProcIndex,
|
||||
params: &lsp::CompletionParams
|
||||
) -> Result<Option<lsp::CompletionResponse>> {
|
||||
let uri = ¶ms.text_document_position.text_document.uri;
|
||||
let Some(_doc) = docs.get(uri) else {
|
||||
return Ok(None);
|
||||
};
|
||||
let Some(procs) = index.get(uri) else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let items: Vec<lsp::CompletionItem> = procs
|
||||
.iter()
|
||||
.map(|p| lsp::CompletionItem {
|
||||
label: p.name.clone(),
|
||||
kind: Some(lsp::CompletionItemKind::FUNCTION),
|
||||
detail: Some(format!("proc {}", p.params.join(" "))),
|
||||
insert_text: Some(p.name.clone()),
|
||||
..Default::default()
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(Some(lsp::CompletionResponse::Array(items)))
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
use anyhow::Result;
|
||||
use analysis::ProcIndex;
|
||||
use lsp_types as lsp;
|
||||
use text::DocumentStore;
|
||||
|
||||
pub fn definition(
|
||||
docs: &DocumentStore,
|
||||
index: &ProcIndex,
|
||||
params: &lsp::GotoDefinitionParams
|
||||
) -> Result<Option<lsp::GotoDefinitionResponse>> {
|
||||
let uri = ¶ms.text_document_position_params.text_document.uri;
|
||||
let pos = params.text_document_position_params.position;
|
||||
let Some(doc) = docs.get(uri) else {
|
||||
return Ok(None);
|
||||
};
|
||||
let Some((word, _range)) = doc.word_under_position(pos) else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let Some(procs) = index.get(uri) else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
if let Some(p) = procs.iter().find(|p| p.name == word) {
|
||||
let loc = lsp::Location {
|
||||
uri: uri.clone(),
|
||||
range: doc.byte_range_to_lsp_range(p.byte_start, p.byte_end),
|
||||
};
|
||||
return Ok(Some(lsp::GotoDefinitionResponse::Scalar(loc)));
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
use anyhow::Result;
|
||||
use analysis::ProcIndex;
|
||||
use lsp_types as lsp;
|
||||
use text::DocumentStore;
|
||||
|
||||
pub fn document_symbol(
|
||||
docs: &DocumentStore,
|
||||
index: &ProcIndex,
|
||||
uri: &lsp::Uri
|
||||
) -> Result<Option<lsp::DocumentSymbolResponse>> {
|
||||
let Some(doc) = docs.get(uri) else {
|
||||
return Ok(None);
|
||||
};
|
||||
let Some(procs) = index.get(uri) else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let mut symbols = Vec::new();
|
||||
for p in procs {
|
||||
let range = doc.byte_range_to_lsp_range(p.byte_start, p.byte_end);
|
||||
symbols.push(lsp::DocumentSymbol {
|
||||
name: p.name.clone(),
|
||||
detail: Some(format!("proc {}", p.params.join(" "))),
|
||||
kind: lsp::SymbolKind::FUNCTION,
|
||||
range,
|
||||
selection_range: range,
|
||||
children: None,
|
||||
tags: None,
|
||||
deprecated: None,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(Some(lsp::DocumentSymbolResponse::Nested(symbols)))
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
mod document_symbol;
|
||||
mod definition;
|
||||
mod completion;
|
||||
|
||||
pub use document_symbol::document_symbol;
|
||||
pub use definition::definition;
|
||||
pub use completion::completion;
|
||||
@@ -0,0 +1,16 @@
|
||||
[package]
|
||||
name = "lsp-main"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
anyhow = { workspace = true }
|
||||
env_logger = { workspace = true }
|
||||
log = { workspace = true }
|
||||
lsp-server = { workspace = true }
|
||||
lsp-types = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
analysis = { path = "../analysis" }
|
||||
features = { path = "../features" }
|
||||
text = { path = "../text" }
|
||||
@@ -0,0 +1,13 @@
|
||||
use anyhow::Result;
|
||||
use env_logger::Env;
|
||||
use lsp_server::Connection;
|
||||
|
||||
mod server;
|
||||
|
||||
fn main() -> Result<()> {
|
||||
env_logger::Builder::from_env(Env::default().default_filter_or("info")).init();
|
||||
let (connection, io_threads) = Connection::stdio();
|
||||
server::Server::new(connection).run()?;
|
||||
io_threads.join()?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
use anyhow::Result;
|
||||
use log;
|
||||
use lsp_server::{ Connection, Message, Response };
|
||||
use lsp_server::Notification as ServerNotification;
|
||||
use lsp_server::Request as ServerRequest;
|
||||
use lsp_types as lsp;
|
||||
use lsp_types::notification::{
|
||||
DidChangeTextDocument,
|
||||
DidCloseTextDocument,
|
||||
DidOpenTextDocument,
|
||||
LogMessage,
|
||||
Notification as LspNotification,
|
||||
};
|
||||
use lsp_types::request::{
|
||||
Completion,
|
||||
DocumentSymbolRequest,
|
||||
GotoDefinition,
|
||||
Request as LspRequest,
|
||||
};
|
||||
use serde_json::Value;
|
||||
|
||||
use analysis::{ collect_procs, ProcIndex };
|
||||
use features::{ completion, definition, document_symbol };
|
||||
use text::DocumentStore;
|
||||
|
||||
pub struct Server {
|
||||
conn: Connection,
|
||||
docs: DocumentStore,
|
||||
procs: ProcIndex,
|
||||
}
|
||||
|
||||
impl Server {
|
||||
pub fn new(conn: Connection) -> Self {
|
||||
Self {
|
||||
conn,
|
||||
docs: DocumentStore::default(),
|
||||
procs: ProcIndex::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run(mut self) -> Result<()> {
|
||||
// Initialize handshake
|
||||
let (id, _params) = self.conn.initialize_start()?;
|
||||
let result = lsp::InitializeResult {
|
||||
capabilities: server_capabilities(),
|
||||
server_info: Some(lsp::ServerInfo {
|
||||
name: "tcl-lsp".into(),
|
||||
version: Some(env!("CARGO_PKG_VERSION").into()),
|
||||
}),
|
||||
};
|
||||
self.conn.initialize_finish(id, serde_json::to_value(result)?)?;
|
||||
|
||||
let _ = self.log("tcl-lsp ready");
|
||||
|
||||
let receiver = self.conn.receiver.clone();
|
||||
|
||||
// main loop
|
||||
for msg in receiver.iter() {
|
||||
match msg {
|
||||
lsp_server::Message::Request(req) => {
|
||||
if self.conn.handle_shutdown(&req)? {
|
||||
break;
|
||||
}
|
||||
if let Err(e) = self.on_request(req) {
|
||||
log::error!("request error: {e:#}");
|
||||
}
|
||||
}
|
||||
lsp_server::Message::Notification(n) => {
|
||||
if let Err(e) = self.on_notification(n) {
|
||||
log::error!("notification error: {e:#}");
|
||||
}
|
||||
}
|
||||
lsp_server::Message::Response(_) => {}
|
||||
}
|
||||
}
|
||||
|
||||
let _ = self.log("tcl-lsp stopped");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn on_notification(&mut self, n: ServerNotification) -> Result<()> {
|
||||
match n.method.as_str() {
|
||||
DidOpenTextDocument::METHOD => {
|
||||
let params: lsp::DidOpenTextDocumentParams = serde_json::from_value(n.params)?;
|
||||
let uri = params.text_document.uri.clone();
|
||||
self.docs.open(params.text_document)?;
|
||||
self.reindex(&uri)?;
|
||||
self.log(&format!("Opened {:?}", uri))?;
|
||||
}
|
||||
DidChangeTextDocument::METHOD => {
|
||||
let params: lsp::DidChangeTextDocumentParams = serde_json::from_value(n.params)?;
|
||||
let uri = params.text_document.uri.clone();
|
||||
// FULL sync: we expect one change with whole text
|
||||
self.docs.apply_full_change(&uri, ¶ms.content_changes)?;
|
||||
self.reindex(&uri)?;
|
||||
}
|
||||
DidCloseTextDocument::METHOD => {
|
||||
let params: lsp::DidCloseTextDocumentParams = serde_json::from_value(n.params)?;
|
||||
self.procs.remove(¶ms.text_document.uri);
|
||||
self.docs.close(¶ms.text_document.uri);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn on_request(&mut self, req: ServerRequest) -> Result<()> {
|
||||
match req.method.as_str() {
|
||||
DocumentSymbolRequest::METHOD => {
|
||||
let params: lsp::DocumentSymbolParams = serde_json::from_value(req.params)?;
|
||||
let uri = params.text_document.uri;
|
||||
let result = document_symbol(&self.docs, &self.procs, &uri)?;
|
||||
send_ok(&self.conn, req.id, result)?;
|
||||
}
|
||||
GotoDefinition::METHOD => {
|
||||
let params: lsp::GotoDefinitionParams = serde_json::from_value(req.params)?;
|
||||
let result = definition(&self.docs, &self.procs, ¶ms)?;
|
||||
send_ok(&self.conn, req.id, result)?;
|
||||
}
|
||||
Completion::METHOD => {
|
||||
let params: lsp::CompletionParams = serde_json::from_value(req.params)?;
|
||||
let _ = self.log("Completion");
|
||||
let result = completion(&self.docs, &self.procs, ¶ms)?;
|
||||
send_ok(&self.conn, req.id, result)?;
|
||||
}
|
||||
other => {
|
||||
let resp = Response::new_err(
|
||||
req.id,
|
||||
lsp::error_codes::REQUEST_FAILED as i32,
|
||||
format!("Unknown method: {other}")
|
||||
);
|
||||
self.conn.sender.send(Message::Response(resp))?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn reindex(&mut self, uri: &lsp::Uri) -> Result<()> {
|
||||
if let Some(doc) = self.docs.get(uri) {
|
||||
if let Some(tree) = doc.tree() {
|
||||
let text = doc.text();
|
||||
let procs = collect_procs(tree, &text);
|
||||
self.procs.insert(uri.clone(), procs);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn log(&self, message: &str) -> Result<()> {
|
||||
let notif = LogMessage::METHOD.to_string();
|
||||
let params = lsp::LogMessageParams {
|
||||
typ: lsp::MessageType::INFO,
|
||||
message: message.to_string(),
|
||||
};
|
||||
self.conn.sender.send(Message::Notification(ServerNotification::new(notif, params)))?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn send_ok<T: serde::Serialize>(
|
||||
conn: &Connection,
|
||||
id: lsp_server::RequestId,
|
||||
result: T
|
||||
) -> Result<()> {
|
||||
let v: Value = serde_json::to_value(result)?;
|
||||
conn.sender.send(Message::Response(Response::new_ok(id, v)))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn server_capabilities() -> lsp::ServerCapabilities {
|
||||
lsp::ServerCapabilities {
|
||||
completion_provider: Some(lsp::CompletionOptions {
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
[package]
|
||||
name = "text"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
anyhow = { workspace = true }
|
||||
lsp-types = { workspace = true }
|
||||
tree-sitter = { workspace = true }
|
||||
tree-sitter-tcl = { workspace = true }
|
||||
url = { workspace = true }
|
||||
@@ -0,0 +1,179 @@
|
||||
use anyhow::{ bail, Context, Result };
|
||||
use lsp_types as lsp;
|
||||
use std::collections::HashMap;
|
||||
use tree_sitter::{ Parser, Tree };
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct DocumentStore {
|
||||
docs: HashMap<lsp::Uri, Document>,
|
||||
}
|
||||
|
||||
impl DocumentStore {
|
||||
pub fn open(&mut self, item: lsp::TextDocumentItem) -> Result<()> {
|
||||
let uri = item.uri.clone();
|
||||
let doc = Document::open(item.text)?;
|
||||
self.docs.insert(uri, doc);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn apply_full_change(
|
||||
&mut self,
|
||||
uri: &lsp::Uri,
|
||||
changes: &[lsp::TextDocumentContentChangeEvent]
|
||||
) -> Result<()> {
|
||||
let Some(doc) = self.docs.get_mut(uri) else {
|
||||
bail!("no such document: {:?}", uri);
|
||||
};
|
||||
// FULL sync: assume single change with whole text
|
||||
let new_text = changes.last().context("empty changes for full sync")?.text.clone();
|
||||
doc.reparse(&new_text)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn close(&mut self, uri: &lsp::Uri) {
|
||||
self.docs.remove(uri);
|
||||
}
|
||||
|
||||
pub fn get(&self, uri: &lsp::Uri) -> Option<&Document> {
|
||||
self.docs.get(uri)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Document {
|
||||
text: String,
|
||||
parser: Parser,
|
||||
tree: Option<Tree>,
|
||||
}
|
||||
|
||||
impl Document {
|
||||
pub fn open(text: String) -> Result<Self> {
|
||||
let mut parser = Parser::new();
|
||||
parser.set_language(&tree_sitter_tcl::LANGUAGE.into()).context("load tcl grammar")?;
|
||||
let tree = parser.parse(&text, None);
|
||||
Ok(Self { text, parser, tree })
|
||||
}
|
||||
|
||||
pub fn reparse(&mut self, new_text: &str) -> Result<()> {
|
||||
self.text.clear();
|
||||
self.text.push_str(new_text);
|
||||
self.tree = self.parser.parse(&self.text, None);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn text(&self) -> String {
|
||||
self.text.clone()
|
||||
}
|
||||
|
||||
pub fn tree(&self) -> Option<&Tree> {
|
||||
self.tree.as_ref()
|
||||
}
|
||||
|
||||
/// Convert a byte range (UTF-8) into an LSP Range (UTF-16 columns).
|
||||
pub fn byte_range_to_lsp_range(&self, start: usize, end: usize) -> lsp::Range {
|
||||
fn byte_to_line_col_utf16(s: &str, byte: usize) -> (u32, u32) {
|
||||
let clamped = byte.min(s.len());
|
||||
let slice = &s[..clamped];
|
||||
let mut line = 0u32;
|
||||
let mut col_u16 = 0u32;
|
||||
let mut last_nl = 0usize;
|
||||
|
||||
for (i, b) in slice.bytes().enumerate() {
|
||||
if b == b'\n' {
|
||||
line += 1;
|
||||
last_nl = i + 1;
|
||||
}
|
||||
}
|
||||
// compute utf16 col from last newline to clamped
|
||||
let seg = &slice[last_nl..];
|
||||
col_u16 = seg.encode_utf16().count() as u32;
|
||||
(line, col_u16)
|
||||
}
|
||||
|
||||
let (sl, sc) = byte_to_line_col_utf16(&self.text, start);
|
||||
let (el, ec) = byte_to_line_col_utf16(&self.text, end);
|
||||
lsp::Range {
|
||||
start: lsp::Position {
|
||||
line: sl,
|
||||
character: sc,
|
||||
},
|
||||
end: lsp::Position {
|
||||
line: el,
|
||||
character: ec,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the UTF-8 byte offset of the "word" under a UTF-16 position (very naive).
|
||||
pub fn word_under_position(
|
||||
&self,
|
||||
pos: lsp::Position
|
||||
) -> Option<(String, std::ops::Range<usize>)> {
|
||||
// Convert UTF-16 (LSP) position -> UTF-8 byte offset in self.text
|
||||
fn line_col_utf16_to_byte(s: &str, line: u32, col_u16: u32) -> usize {
|
||||
// Find start byte of the requested line
|
||||
let mut cur_line: u32 = 0;
|
||||
let mut line_start_byte: usize = 0;
|
||||
|
||||
if line == 0 {
|
||||
line_start_byte = 0;
|
||||
} else {
|
||||
for (i, b) in s.bytes().enumerate() {
|
||||
if b == b'\n' {
|
||||
cur_line += 1;
|
||||
if cur_line == line {
|
||||
line_start_byte = i + 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
// If line beyond EOF, clamp to end
|
||||
if cur_line < line {
|
||||
return s.len();
|
||||
}
|
||||
}
|
||||
|
||||
// Advance by UTF-16 code units from line_start_byte
|
||||
let mut remaining = col_u16 as isize;
|
||||
let tail = &s[line_start_byte..];
|
||||
for (off, ch) in tail.char_indices() {
|
||||
if remaining <= 0 {
|
||||
return line_start_byte + off;
|
||||
}
|
||||
// subtract the number of UTF-16 code units this char occupies (1 for BMP, 2 for surrogates)
|
||||
let mut buf = [0u16; 2];
|
||||
remaining -= ch.encode_utf16(&mut buf).len() as isize;
|
||||
}
|
||||
// If column beyond EOL, clamp to end
|
||||
s.len()
|
||||
}
|
||||
|
||||
let bytes = self.text.as_bytes();
|
||||
let byte = line_col_utf16_to_byte(&self.text, pos.line, pos.character);
|
||||
if byte > self.text.len() {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Expand to simple Tcl-ish identifier characters: letters/digits/_ : / . -
|
||||
fn is_ident(c: u8) -> bool {
|
||||
c.is_ascii_alphanumeric() || matches!(c, b'_' | b':' | b'/' | b'.' | b'-')
|
||||
}
|
||||
|
||||
// Walk backward to start
|
||||
let mut start = byte;
|
||||
while start > 0 && is_ident(bytes[start - 1]) {
|
||||
start -= 1;
|
||||
}
|
||||
// Walk forward to end
|
||||
let mut end = byte;
|
||||
while end < bytes.len() && is_ident(bytes[end]) {
|
||||
end += 1;
|
||||
}
|
||||
|
||||
if start >= end {
|
||||
return None;
|
||||
}
|
||||
|
||||
let text = self.text[start..end].to_string();
|
||||
Some((text, start..end)) // <-- no semicolon here, this is the tail expression
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user