Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -235,16 +235,17 @@ Input -> Output`. The signature part `Input -> Output` is optional. The `:`
`@waiter + ^milliways`); the special `@*` resets the scope. Attributes
are effectively parallel steps and create scopes within which parallel or
dependent steps can be nested.
- This is invalid:
- This is well-formed but semantically incorrect:

```technique
invalid :
- Top level parallel step
- nested parallel substep
```

because there is no way for the parser to differentiate between the two.

the author's intent was to nest parallel steps but this is not possible to
articulate in Technique because; there is no way for the parser to
differentiate between the two. Unfortunately we cannot generate an error for
this because of being whitespace agnostic.
- The free form descriptive text can be escaped to code using an inline code block, delimited with braces `{ ... }`
- Within code blocks there are basic control flow: `repeat`, `foreach` loops.
- Within code blocks there are function calls to builtin functions: `exec()`
Expand Down
29 changes: 3 additions & 26 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 3 additions & 5 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "technique"
version = "0.6.5"
version = "0.6.6"
edition = "2024"
description = "A domain specific language for procedures."
authors = [ "Andrew Cowie" ]
Expand All @@ -11,14 +11,12 @@ license = "MIT"
clap = { version = "4.5.16", features = [ "wrap_help" ] }
crossterm = "0.29"
ignore = "0.4"
lsp-server = "0.8.0"
lsp-server = "0.9.0"
lsp-types = "0.97"
owo-colors = "4"
regex = "1.11.1"
serde = { version = "1.0.209", features = [ "derive" ] }
serde_json = "1.0"
time = { version = "0.3", features = [ "formatting" ] }
tinytemplate = "1.2.1"
time = "0.3"

tracing = "0.1.40"
tracing-subscriber = { version = "0.3.18", features = [ "env-filter" ] }
Expand Down
168 changes: 168 additions & 0 deletions src/editor/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -880,8 +880,176 @@ fn offset_to_position(text: &str, offset: usize) -> Position {
#[cfg(test)]
mod tests {
use super::*;
use lsp_server::{ErrorCode, RequestId, ResponseKind};
use lsp_types::TextDocumentItem;
use std::cell::RefCell;
use std::convert::Infallible;
use technique::language::Span;

fn collecting_sender(
messages: &RefCell<Vec<Message>>,
) -> impl Fn(Message) -> Result<(), Infallible> + '_ {
move |msg| {
messages
.borrow_mut()
.push(msg);
Ok(())
}
}

#[test]
fn test_handle_request_unknown_method_returns_error() {
let mut server = TechniqueLanguageServer::new(InitializeParams::default());
let messages = RefCell::new(Vec::new());
let sender = collecting_sender(&messages);

let request = Request::new(
RequestId::from(1),
"textDocument/nonsense".to_string(),
Value::Null,
);
server
.handle_request(request, &sender)
.unwrap();
drop(sender);

let messages = messages.into_inner();
assert_eq!(messages.len(), 1);
match &messages[0] {
Message::Response(response) => {
assert_eq!(response.id, RequestId::from(1));
match &response.response_kind {
ResponseKind::Err { error } => {
assert_eq!(error.code, ErrorCode::MethodNotFound as i32);
}
ResponseKind::Ok { .. } => panic!("expected an error response"),
}
}
_ => panic!("expected a Response message"),
}
}

#[test]
fn test_handle_request_shutdown_returns_ok() {
let mut server = TechniqueLanguageServer::new(InitializeParams::default());
let messages = RefCell::new(Vec::new());
let sender = collecting_sender(&messages);

let request = Request::new(RequestId::from(2), "shutdown".to_string(), Value::Null);
server
.handle_request(request, &sender)
.unwrap();
drop(sender);

let messages = messages.into_inner();
assert_eq!(messages.len(), 1);
match &messages[0] {
Message::Response(response) => {
assert_eq!(response.id, RequestId::from(2));
match &response.response_kind {
ResponseKind::Ok { result } => assert_eq!(*result, Value::Null),
ResponseKind::Err { .. } => panic!("expected an ok response"),
}
}
_ => panic!("expected a Response message"),
}
}

#[test]
fn test_handle_notification_did_open_publishes_diagnostics() {
let mut server = TechniqueLanguageServer::new(InitializeParams::default());
let messages = RefCell::new(Vec::new());
let sender = collecting_sender(&messages);

let uri: Uri = "file:///Broken.tq"
.parse()
.unwrap();
let text = "making_coffee : Ingredients Coffee\n\nmakingCoffee :\n".to_string();
let params = DidOpenTextDocumentParams {
text_document: TextDocumentItem::new(uri, "technique".to_string(), 1, text),
};
let notification = Notification::new(
"textDocument/didOpen".to_string(),
to_value(params).unwrap(),
);
server
.handle_notification(notification, &sender)
.unwrap();
drop(sender);

let messages = messages.into_inner();
assert_eq!(messages.len(), 1);
match &messages[0] {
Message::Notification(notification) => {
assert_eq!(notification.method, "textDocument/publishDiagnostics");
let params: PublishDiagnosticsParams = from_value(
notification
.params
.clone(),
)
.unwrap();
assert!(
!params
.diagnostics
.is_empty()
);
}
_ => panic!("expected a Notification message"),
}
}

#[test]
fn test_workspace_symbol_finds_procedure_indexed_from_workspace_folder() {
let uri: Uri = format!("file://{}/examples/minimal", env!("CARGO_MANIFEST_DIR"))
.parse()
.unwrap();
let params = InitializeParams {
workspace_folders: Some(vec![WorkspaceFolder {
uri,
name: "minimal".to_string(),
}]),
..Default::default()
};
let mut server = TechniqueLanguageServer::new(params);
let messages = RefCell::new(Vec::new());
let sender = collecting_sender(&messages);

let request = Request::new(
RequestId::from(3),
"workspace/symbol".to_string(),
to_value(WorkspaceSymbolParams {
query: "making_coffee".to_string(),
..Default::default()
})
.unwrap(),
);
server
.handle_request(request, &sender)
.unwrap();
drop(sender);

let messages = messages.into_inner();
assert_eq!(messages.len(), 1);
match &messages[0] {
Message::Response(response) => match &response.response_kind {
ResponseKind::Ok { result } => {
let symbols: Option<Vec<SymbolInformation>> =
from_value(result.clone()).unwrap();
let symbols = symbols.unwrap();
assert!(
symbols
.iter()
.any(|symbol| symbol
.name
.starts_with("making_coffee"))
);
}
ResponseKind::Err { .. } => panic!("expected an ok response"),
},
_ => panic!("expected a Response message"),
}
}

#[test]
fn test_calculate_str_offsets() {
let parent = "hello world, this is a test";
Expand Down
26 changes: 26 additions & 0 deletions src/parsing/checks/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2280,6 +2280,32 @@ fn tuple_binding_expression() {
);
}

#[test]
fn binding_followed_by_use_in_same_code_block() {
let mut input = Parser::new();
input.initialize("{ now() ~ t ; t }");

let result = input.read_code_block();
assert_eq!(
result,
Ok(vec![
Expression::Binding(
Box::new(Expression::Execution(
Function {
target: Identifier::new("now"),
parameters: vec![]
},
Span::default()
)),
vec![Identifier::new("t")],
Span::default()
),
Expression::Separator,
Expression::Variable(Identifier::new("t"), Span::default())
])
);
}

#[test]
fn test_repeat_expression() {
let mut input = Parser::new();
Expand Down
3 changes: 2 additions & 1 deletion src/parsing/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3320,7 +3320,8 @@ fn is_function(content: &str) -> bool {
}

fn is_binding(content: &str) -> bool {
let re = regex!(r"~\s+([a-z][a-z0-9_]*|\([a-z][a-z0-9_]*(?:\s*,\s*[a-z][a-z0-9_]*)*\))\s*$");
let re =
regex!(r"~\s+([a-z][a-z0-9_]*|\([a-z][a-z0-9_]*(?:\s*,\s*[a-z][a-z0-9_]*)*\))\s*(;|$)");

re.is_match(content)
}
Expand Down
1 change: 0 additions & 1 deletion src/runner/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,6 @@ impl Context {
/// Pass a complete, already known, text string through to the user. This
/// is just a convenience over `write()` for whole messages such as status
/// lines or announcements.
#[allow(dead_code)]
pub fn emit(&self, message: &str) -> io::Result<()> {
self.write(message.as_bytes())
}
Expand Down
2 changes: 1 addition & 1 deletion src/runner/driver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ pub enum Kind {
Prose, // descriptive text; unit value
Computable, // a value will be computed with no external side effect
// (invoke, bind, literal, Pure builtin)
System, // an external command the host runs, e.g exec()
System, // something the host runs, e.g. exec(), now()
Action, // an act a human performs e.g. click())
Choice, // a response the user must select
}
Expand Down
Loading