Compare commits

...

52 Commits

Author SHA1 Message Date
Antonio Scandurra
d43c676dd0 Checkpoint 2024-11-29 19:43:13 +01:00
Thorsten Ball
3ddfb77fce Make test assertion looser after upgrading to cue-v6 2024-11-29 13:27:12 +01:00
Antonio Scandurra
93415672c4 WIP 2024-11-28 15:06:24 +01:00
Antonio Scandurra
c16315f4b3 WIP 2024-11-28 12:01:20 +01:00
Antonio Scandurra
75bfe51694 Checkpoint 2024-11-28 10:33:48 +01:00
Antonio Scandurra
f0d49a0cd5 Checkpoint 2024-11-28 10:15:36 +01:00
Antonio Scandurra
f239f88063 WIP 2024-11-27 19:20:31 +01:00
Antonio Scandurra
a03eb7d5bb WIP 2024-11-26 19:18:46 +01:00
Thorsten Ball
7cee1d3046 Simplify prompt again 2024-11-25 15:27:04 +01:00
Thorsten Ball
46f2c99634 Add missing word to fixture 2024-11-25 15:03:13 +01:00
Thorsten Ball
2c40009288 Add interpreter test case 2024-11-25 14:41:11 +01:00
Antonio Scandurra
0aeba45a69 WIP 2024-11-25 12:49:32 +01:00
Antonio Scandurra
69b7e1f26d WIP 2024-11-25 12:44:10 +01:00
Antonio Scandurra
a609c22daf Add failing test for visibility completion
Co-Authored-By: Bennet <bennet@zed.dev>
Co-Authored-By: Thorsten <thorsten@zed.dev>
2024-11-25 10:52:00 +01:00
Antonio Scandurra
79a70b72b3 🎨 2024-11-22 16:33:31 +01:00
Thorsten Ball
1bc342ff5a Fix Ollama API endpoint
Co-authored-by: Antonio <antonio@zed.dev>
2024-11-22 15:59:57 +01:00
Thorsten Ball
4d306d7cdb Use speculative decoding when sending inline completions
Co-authored-by: Antonio <antonio@zed.dev>
2024-11-22 15:35:13 +01:00
Thorsten Ball
25518042bc Get model to rewrite the whole code excerpt
Co-authored-by: Antonio <antonio@zed.dev>
2024-11-22 15:13:54 +01:00
Thorsten Ball
2dd31047c7 Tweak prompt and extend test to do multiple completions
Co-authored-by: Antonio <antonio@zed.dev>
2024-11-22 14:08:09 +01:00
Thorsten Ball
2b26a17455 Do not refresh inline completions if editor isn't focused
Co-authored-by: Antonio <antonio@zed.dev>
2024-11-22 10:48:30 +01:00
Thorsten Ball
e75c1dc86d vim: Accept inline completions with tab if in normal mode 2024-11-22 10:20:57 +01:00
Antonio Scandurra
c08da28b34 Merge remote-tracking branch 'origin/main' into zeta
# Conflicts:
#	Cargo.lock
2024-11-21 15:38:32 +01:00
Thorsten Ball
00b41352a2 Fix finding completion old_text in excerpt
Co-authored-by: Antonio <antonio@zed.dev>
2024-11-21 15:23:02 +01:00
Thorsten Ball
e5b114bb73 Add another test for the full-file completion test
Co-authored-by: Antonio <antonio@zed.dev>
2024-11-21 12:33:43 +01:00
Thorsten Ball
afef42085b Add full-file test and change test grading prompt
Co-authored-by: Antonio <antonio@zed.dev>
2024-11-21 12:28:00 +01:00
Antonio Scandurra
cfd1be20d7 Add another test 2024-11-21 10:18:10 +01:00
Antonio Scandurra
9b9dabc33a Reduce the amount of events we maintain 2024-11-21 10:12:42 +01:00
Thorsten Ball
6b01782425 Only put "rejected completions" into prompt
Co-authored-by: Antonio <antonio@zed.dev>
2024-11-21 09:34:52 +01:00
Thorsten Ball
ed16289574 Clean up dead code 2024-11-21 09:17:58 +01:00
Thorsten Ball
ded4bcedef Make prompt clearer while keeping all tests passing 2024-11-21 09:11:56 +01:00
Thorsten Ball
b2c4b350ba Fix compile errors and enable incremental tests again 2024-11-21 09:08:14 +01:00
Antonio Scandurra
502a989131 WIP 2024-11-21 08:57:07 +01:00
Antonio Scandurra
b6dff7e0e0 Implement debouncing to avoid spamming completion requests 2024-11-21 08:26:25 +01:00
Bennet Bo Fenner
3672f63014 check path when filtering out edit event 2024-11-20 18:30:07 +01:00
Thorsten Ball
f8ebf37529 All tests passing, incremental-edits in tests disabled 2024-11-20 18:01:20 +01:00
Thorsten Ball
01d19ee911 Checkpoint: incremental tests disabled, events reduced 2024-11-20 17:23:50 +01:00
Antonio Scandurra
cdb5cc6cfe Checkpoint 2024-11-20 12:39:58 +01:00
Antonio Scandurra
3ab088939b Checkpoint
Co-Authored-By: Thorsten <thorsten@zed.dev>
2024-11-20 12:09:58 +01:00
Thorsten Ball
9cb86564a2 Tweak eval prompt 2024-11-20 10:06:55 +01:00
Thorsten Ball
11c9ffd69a Add failing test for command line args refactor 2024-11-20 09:54:02 +01:00
Thorsten Ball
20b3fa4e5c Include formatting directive in prompt 2024-11-20 09:38:51 +01:00
Bennet Bo Fenner
776404c264 tweak prompt 2024-11-19 18:29:01 +01:00
Bennet Bo Fenner
893b6ede62 Resolve insertions correctly when computing diff 2024-11-19 18:17:27 +01:00
Thorsten Ball
b1d787dbd9 WIP: Create diff 2024-11-19 18:02:48 +01:00
Thorsten Ball
c500c6f429 Fix the indoc strings 2024-11-19 17:27:03 +01:00
Thorsten Ball
9fb2d86b16 Add two more test cases 2024-11-19 17:11:51 +01:00
Thorsten Ball
399316cd98 Fix test by loosening assertion 2024-11-19 16:36:09 +01:00
Thorsten Ball
a30f0d5f05 Change prompts and test assertion framework
Co-authored-by: Antonio <antonio@zed.dev>
Co-authored-by: Bennet <bennet@zed.dev>
2024-11-19 16:30:29 +01:00
Thorsten Ball
7c18df05bd Add missing newline 2024-11-19 15:04:49 +01:00
Antonio Scandurra
c1bd129466 WIP 2024-11-19 13:11:41 +01:00
Antonio Scandurra
f354425f88 WIP 2024-11-19 12:37:00 +01:00
Antonio Scandurra
f59227a6bf WIP: start on zeta 2024-11-18 19:16:06 +01:00
24 changed files with 7085 additions and 2 deletions

25
Cargo.lock generated
View File

@@ -15565,6 +15565,7 @@ dependencies = [
"winresource",
"workspace",
"zed_actions",
"zeta",
]
[[package]]
@@ -15875,6 +15876,30 @@ dependencies = [
"syn 2.0.87",
]
[[package]]
name = "zeta"
version = "0.1.0"
dependencies = [
"anyhow",
"collections",
"ctor",
"env_logger 0.11.5",
"futures 0.3.31",
"gpui",
"http_client",
"indoc",
"inline_completion",
"language",
"log",
"open_ai",
"reqwest_client",
"rpc",
"similar",
"tree-sitter-go",
"tree-sitter-rust",
"worktree",
]
[[package]]
name = "zstd"
version = "0.11.2+zstd.1.5.2"

View File

@@ -133,6 +133,7 @@ members = [
"crates/worktree",
"crates/zed",
"crates/zed_actions",
"crates/zeta",
#
# Extensions
@@ -307,6 +308,7 @@ workspace = { path = "crates/workspace" }
worktree = { path = "crates/worktree" }
zed = { path = "crates/zed" }
zed_actions = { path = "crates/zed_actions" }
zeta = { path = "crates/zeta" }
#
# External crates

View File

@@ -5304,7 +5304,8 @@ impl Editor {
if !user_requested
&& (!self.enable_inline_completions
|| !self.should_show_inline_completions(&buffer, cursor_buffer_position, cx))
|| !self.should_show_inline_completions(&buffer, cursor_buffer_position, cx)
|| !self.is_focused(cx))
{
self.discard_inline_completion(false, cx);
return None;

View File

@@ -193,6 +193,8 @@ impl Render for InlineCompletionButton {
),
);
}
InlineCompletionProvider::Zeta => div().child("Zeta"),
}
}
}

View File

@@ -575,7 +575,7 @@ impl<'a, 'b> DerefMut for ChunkRendererContext<'a, 'b> {
pub struct Diff {
pub(crate) base_version: clock::Global,
line_ending: LineEnding,
edits: Vec<(Range<usize>, Arc<str>)>,
pub edits: Vec<(Range<usize>, Arc<str>)>,
}
#[derive(Clone, Copy)]

View File

@@ -197,6 +197,7 @@ pub enum InlineCompletionProvider {
#[default]
Copilot,
Supermaven,
Zeta,
}
/// The settings for inline completions, such as [GitHub Copilot](https://github.com/features/copilot)

View File

@@ -266,6 +266,7 @@ impl LanguageModelRequest {
max_tokens: max_output_tokens,
tools: Vec::new(),
tool_choice: None,
prediction: None,
}
}

View File

@@ -164,11 +164,19 @@ pub struct Request {
pub stop: Vec<String>,
pub temperature: f32,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub prediction: Option<Prediction>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tool_choice: Option<ToolChoice>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub tools: Vec<ToolDefinition>,
}
#[derive(Clone, Deserialize, Serialize, Debug)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum Prediction {
Content { content: String },
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(untagged)]
pub enum ToolChoice {
@@ -285,6 +293,21 @@ pub struct ResponseStreamEvent {
pub usage: Option<Usage>,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct CompletionResponse {
pub id: String,
pub object: String,
pub created: u64,
pub model: String,
pub choices: Vec<CompletionChoice>,
pub usage: Usage,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct CompletionChoice {
pub text: String,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct Response {
pub id: String,
@@ -355,6 +378,66 @@ pub async fn complete(
}
}
pub async fn complete_text(
client: &dyn HttpClient,
api_url: &str,
api_key: &str,
prompt: &str,
model: &str,
max_tokens: Option<u32>,
temperature: f32,
) -> Result<CompletionResponse> {
let uri = format!("{api_url}/completions");
let request_builder = HttpRequest::builder()
.method(Method::POST)
.uri(uri)
.header("Content-Type", "application/json")
.header("Authorization", format!("Bearer {}", api_key));
let request_body = serde_json::json!({
"model": model,
"prompt": prompt,
"max_tokens": max_tokens,
"temperature": temperature
});
let request = request_builder.body(AsyncBody::from(request_body.to_string()))?;
let mut response = client.send(request).await?;
if response.status().is_success() {
let mut body = String::new();
response.body_mut().read_to_string(&mut body).await?;
let response = serde_json::from_str(&body)?;
Ok(response)
} else {
let mut body = String::new();
response.body_mut().read_to_string(&mut body).await?;
#[derive(Deserialize)]
struct OpenAiResponse {
error: OpenAiError,
}
#[derive(Deserialize)]
struct OpenAiError {
message: String,
}
match serde_json::from_str::<OpenAiResponse>(&body) {
Ok(response) if !response.error.message.is_empty() => Err(anyhow!(
"Failed to connect to OpenAI API: {}",
response.error.message,
)),
_ => Err(anyhow!(
"Failed to connect to OpenAI API: {} {}",
response.status(),
body,
)),
}
}
}
fn adapt_response_to_stream(response: Response) -> ResponseStreamEvent {
ResponseStreamEvent {
created: response.created as u32,

View File

@@ -1104,6 +1104,15 @@ impl Vim {
if self.mode == Mode::Replace {
self.multi_replace(text, cx)
}
if self.mode == Mode::Normal {
self.update_editor(cx, |_, editor, cx| {
editor.accept_inline_completion(
&editor::actions::AcceptInlineCompletion {},
cx,
);
});
}
}
}
}

View File

@@ -120,6 +120,7 @@ vim.workspace = true
welcome.workspace = true
workspace.workspace = true
zed_actions.workspace = true
zeta.workspace = true
[target.'cfg(target_os = "windows")'.dependencies]
windows.workspace = true

View File

@@ -131,5 +131,17 @@ fn assign_inline_completion_provider(
editor.set_inline_completion_provider(Some(provider), cx);
}
}
language::language_settings::InlineCompletionProvider::Zeta => {
let zeta = zeta::Zeta::global(cx);
if let Some(buffer) = editor.buffer().read(cx).as_singleton() {
if buffer.read(cx).file().is_some() {
zeta.update(cx, |zeta, cx| {
zeta.register_buffer(&buffer, cx);
});
}
}
let provider = cx.new_model(|_| zeta::ZetaInlineCompletionProvider::new(zeta));
editor.set_inline_completion_provider(Some(provider), cx);
}
}
}

40
crates/zeta/Cargo.toml Normal file
View File

@@ -0,0 +1,40 @@
[package]
name = "zeta"
version = "0.1.0"
edition = "2021"
publish = false
license = "GPL-3.0-or-later"
exclude = ["fixtures"]
[lints]
workspace = true
[lib]
path = "src/zeta.rs"
doctest = false
[dependencies]
anyhow.workspace = true
collections.workspace = true
futures.workspace = true
gpui.workspace = true
http_client.workspace = true
inline_completion.workspace = true
language.workspace = true
log.workspace = true
open_ai.workspace = true
similar.workspace = true
[dev-dependencies]
collections = { workspace = true, features = ["test-support"] }
ctor.workspace = true
env_logger.workspace = true
gpui = { workspace = true, features = ["test-support"] }
http_client = { workspace = true, features = ["test-support"] }
indoc.workspace = true
language = { workspace = true, features = ["test-support"] }
reqwest_client = { workspace = true, features = ["test-support"] }
rpc = { workspace = true, features = ["test-support"] }
tree-sitter-go.workspace = true
tree-sitter-rust.workspace = true
worktree = { workspace = true, features = ["test-support"] }

View File

@@ -0,0 +1,375 @@
package ast
import (
"bytes"
"fmt"
"strings"
"github.com/mrnugget/monkey/token"
)
// The base Node interface
type Node interface {
TokenLiteral() string
String() string
}
// All statement nodes implement this
type Statement interface {
Node
statementNode()
}
// All expression nodes implement this
type Expression interface {
Node
expressionNode()
}
type Program struct {
Statements []Statement
}
func (p *Program) TokenLiteral() string {
if len(p.Statements) > 0 {
return p.Statements[0].TokenLiteral()
} else {
return ""
}
}
func (p *Program) String() string {
var out bytes.Buffer
for _, s := range p.Statements {
out.WriteString(s.String())
}
return out.String()
}
// Statements
type WhileStatement struct {
<|user_cursor_is_here|>
}
type LetStatement struct {
Token token.Token // the token.LET token
Name *Identifier
Value Expression
}
func (ls *LetStatement) statementNode() {}
func (ls *LetStatement) TokenLiteral() string { return ls.Token.Literal }
func (ls *LetStatement) String() string {
var out bytes.Buffer
out.WriteString(ls.TokenLiteral() + " ")
out.WriteString(ls.Name.String())
out.WriteString(" = ")
if ls.Value != nil {
out.WriteString(ls.Value.String())
}
out.WriteString(";")
return out.String()
}
type ReturnStatement struct {
Token token.Token // the 'return' token
ReturnValue Expression
}
func (rs *ReturnStatement) statementNode() {}
func (rs *ReturnStatement) TokenLiteral() string { return rs.Token.Literal }
func (rs *ReturnStatement) String() string {
var out bytes.Buffer
out.WriteString(rs.TokenLiteral() + " ")
if rs.ReturnValue != nil {
out.WriteString(rs.ReturnValue.String())
}
out.WriteString(";")
return out.String()
}
type ExpressionStatement struct {
Token token.Token // the first token of the expression
Expression Expression
}
func (es *ExpressionStatement) statementNode() {}
func (es *ExpressionStatement) TokenLiteral() string { return es.Token.Literal }
func (es *ExpressionStatement) String() string {
if es.Expression != nil {
return es.Expression.String()
}
return ""
}
type BlockStatement struct {
Token token.Token // the { token
Statements []Statement
}
func (bs *BlockStatement) statementNode() {}
func (bs *BlockStatement) TokenLiteral() string { return bs.Token.Literal }
func (bs *BlockStatement) String() string {
var out bytes.Buffer
for _, s := range bs.Statements {
out.WriteString(s.String())
}
return out.String()
}
// Expressions
type Identifier struct {
Token token.Token // the token.IDENT token
Value string
}
func (i *Identifier) expressionNode() {}
func (i *Identifier) TokenLiteral() string { return i.Token.Literal }
func (i *Identifier) String() string { return i.Value }
type Boolean struct {
Token token.Token
Value bool
}
func (b *Boolean) expressionNode() {}
func (b *Boolean) TokenLiteral() string { return b.Token.Literal }
func (b *Boolean) String() string { return b.Token.Literal }
type IntegerLiteral struct {
Token token.Token
Value int64
}
func (il *IntegerLiteral) expressionNode() {}
func (il *IntegerLiteral) TokenLiteral() string { return il.Token.Literal }
func (il *IntegerLiteral) String() string { return il.Token.Literal }
type PrefixExpression struct {
Token token.Token // The prefix token, e.g. !
Operator string
Right Expression
}
func (pe *PrefixExpression) expressionNode() {}
func (pe *PrefixExpression) TokenLiteral() string { return pe.Token.Literal }
func (pe *PrefixExpression) String() string {
var out bytes.Buffer
out.WriteString("(")
out.WriteString(pe.Operator)
out.WriteString(pe.Right.String())
out.WriteString(")")
return out.String()
}
type InfixExpression struct {
Token token.Token // The operator token, e.g. +
Left Expression
Operator string
Right Expression
}
func (oe *InfixExpression) expressionNode() {}
func (oe *InfixExpression) TokenLiteral() string { return oe.Token.Literal }
func (oe *InfixExpression) String() string {
var out bytes.Buffer
out.WriteString("(")
out.WriteString(oe.Left.String())
out.WriteString(" " + oe.Operator + " ")
out.WriteString(oe.Right.String())
out.WriteString(")")
return out.String()
}
type IfExpression struct {
Token token.Token // The 'if' token
Condition Expression
Consequence *BlockStatement
Alternative *BlockStatement
}
func (ie *IfExpression) expressionNode() {}
func (ie *IfExpression) TokenLiteral() string { return ie.Token.Literal }
func (ie *IfExpression) String() string {
var out bytes.Buffer
out.WriteString("if (")
out.WriteString(ie.Condition.String())
out.WriteString(") { ")
out.WriteString(ie.Consequence.String())
out.WriteString(" }")
if ie.Alternative != nil {
out.WriteString(" else { ")
out.WriteString(ie.Alternative.String())
out.WriteString(" }")
}
return out.String()
}
type FunctionLiteral struct {
Token token.Token // The 'fn' token
Parameters []*Identifier
Body *BlockStatement
Name string
}
func (fl *FunctionLiteral) expressionNode() {}
func (fl *FunctionLiteral) TokenLiteral() string { return fl.Token.Literal }
func (fl *FunctionLiteral) String() string {
var out bytes.Buffer
params := []string{}
for _, p := range fl.Parameters {
params = append(params, p.String())
}
out.WriteString(fl.TokenLiteral())
if fl.Name != "" {
out.WriteString(fmt.Sprintf("<%s>", fl.Name))
}
out.WriteString("(")
out.WriteString(strings.Join(params, ", "))
out.WriteString(") ")
out.WriteString(fl.Body.String())
return out.String()
}
type CallExpression struct {
Token token.Token // The '(' token
Function Expression // Identifier or FunctionLiteral
Arguments []Expression
}
func (ce *CallExpression) expressionNode() {}
func (ce *CallExpression) TokenLiteral() string { return ce.Token.Literal }
func (ce *CallExpression) String() string {
var out bytes.Buffer
args := []string{}
for _, a := range ce.Arguments {
args = append(args, a.String())
}
out.WriteString(ce.Function.String())
out.WriteString("(")
out.WriteString(strings.Join(args, ", "))
out.WriteString(")")
return out.String()
}
type StringLiteral struct {
Token token.Token
Value string
}
func (sl *StringLiteral) expressionNode() {}
func (sl *StringLiteral) TokenLiteral() string { return sl.Token.Literal }
func (sl *StringLiteral) String() string { return sl.Token.Literal }
type ArrayLiteral struct {
Token token.Token // the '[' token
Elements []Expression
}
func (al *ArrayLiteral) expressionNode() {}
func (al *ArrayLiteral) TokenLiteral() string { return al.Token.Literal }
func (al *ArrayLiteral) String() string {
var out bytes.Buffer
elements := []string{}
for _, el := range al.Elements {
elements = append(elements, el.String())
}
out.WriteString("[")
out.WriteString(strings.Join(elements, ", "))
out.WriteString("]")
return out.String()
}
type IndexExpression struct {
Token token.Token // The [ token
Left Expression
Index Expression
}
func (ie *IndexExpression) expressionNode() {}
func (ie *IndexExpression) TokenLiteral() string { return ie.Token.Literal }
func (ie *IndexExpression) String() string {
var out bytes.Buffer
out.WriteString("(")
out.WriteString(ie.Left.String())
out.WriteString("[")
out.WriteString(ie.Index.String())
out.WriteString("])")
return out.String()
}
type HashLiteral struct {
Token token.Token // the '{' token
Pairs map[Expression]Expression
}
func (hl *HashLiteral) expressionNode() {}
func (hl *HashLiteral) TokenLiteral() string { return hl.Token.Literal }
func (hl *HashLiteral) String() string {
var out bytes.Buffer
pairs := []string{}
for key, value := range hl.Pairs {
pairs = append(pairs, key.String()+":"+value.String())
}
out.WriteString("{")
out.WriteString(strings.Join(pairs, ", "))
out.WriteString("}")
return out.String()
}
type MacroLiteral struct {
Token token.Token // The 'macro' token
Parameters []*Identifier
Body *BlockStatement
}
func (ml *MacroLiteral) expressionNode() {}
func (ml *MacroLiteral) TokenLiteral() string { return ml.Token.Literal }
func (ml *MacroLiteral) String() string {
var out bytes.Buffer
params := []string{}
for _, p := range ml.Parameters {
params = append(params, p.String())
}
out.WriteString(ml.TokenLiteral())
out.WriteString("(")
out.WriteString(strings.Join(params, ", "))
out.WriteString(") ")
out.WriteString(ml.Body.String())
return out.String()
}

View File

@@ -0,0 +1,372 @@
package ast
import (
"bytes"
"fmt"
"strings"
"github.com/mrnugget/monkey/token"
)
// The base Node interface
type Node interface {
TokenLiteral() string
String() string
}
// All statement nodes implement this
type Statement interface {
Node
statementNode()
}
// All expression nodes implement this
type Expression interface {
Node
expressionNode()
}
type Program struct {
Statements []Statement
}
func (p *Program) TokenLiteral() string {
if len(p.Statements) > 0 {
return p.Statements[0].TokenLiteral()
} else {
return ""
}
}
func (p *Program) String() string {
var out bytes.Buffer
for _, s := range p.Statements {
out.WriteString(s.String())
}
return out.String()
}
// Statements
type LetStatement struct {
Token token.Token // the token.LET token
Name *Identifier
Value Expression
}
func (ls *LetStatement) statementNode() {}
func (ls *LetStatement) TokenLiteral() string { return ls.Token.Literal }
func (ls *LetStatement) String() string {
var out bytes.Buffer
out.WriteString(ls.TokenLiteral() + " ")
out.WriteString(ls.Name.String())
out.WriteString(" = ")
if ls.Value != nil {
out.WriteString(ls.Value.String())
}
out.WriteString(";")
return out.String()
}
type ReturnStatement struct {
Token token.Token // the 'return' token
ReturnValue Expression
}
func (rs *ReturnStatement) statementNode() {}
func (rs *ReturnStatement) TokenLiteral() string { return rs.Token.Literal }
func (rs *ReturnStatement) String() string {
var out bytes.Buffer
out.WriteString(rs.TokenLiteral() + " ")
if rs.ReturnValue != nil {
out.WriteString(rs.ReturnValue.String())
}
out.WriteString(";")
return out.String()
}
type ExpressionStatement struct {
Token token.Token // the first token of the expression
Expression Expression
}
func (es *ExpressionStatement) statementNode() {}
func (es *ExpressionStatement) TokenLiteral() string { return es.Token.Literal }
func (es *ExpressionStatement) String() string {
if es.Expression != nil {
return es.Expression.String()
}
return ""
}
type BlockStatement struct {
Token token.Token // the { token
Statements []Statement
}
func (bs *BlockStatement) statementNode() {}
func (bs *BlockStatement) TokenLiteral() string { return bs.Token.Literal }
func (bs *BlockStatement) String() string {
var out bytes.Buffer
for _, s := range bs.Statements {
out.WriteString(s.String())
}
return out.String()
}
// Expressions
type Identifier struct {
Token token.Token // the token.IDENT token
Value string
}
func (i *Identifier) expressionNode() {}
func (i *Identifier) TokenLiteral() string { return i.Token.Literal }
func (i *Identifier) String() string { return i.Value }
type Boolean struct {
Token token.Token
Value bool
}
func (b *Boolean) expressionNode() {}
func (b *Boolean) TokenLiteral() string { return b.Token.Literal }
func (b *Boolean) String() string { return b.Token.Literal }
type IntegerLiteral struct {
Token token.Token
Value int64
}
func (il *IntegerLiteral) expressionNode() {}
func (il *IntegerLiteral) TokenLiteral() string { return il.Token.Literal }
func (il *IntegerLiteral) String() string { return il.Token.Literal }
type PrefixExpression struct {
Token token.Token // The prefix token, e.g. !
Operator string
Right Expression
}
func (pe *PrefixExpression) expressionNode() {}
func (pe *PrefixExpression) TokenLiteral() string { return pe.Token.Literal }
func (pe *PrefixExpression) String() string {
var out bytes.Buffer
out.WriteString("(")
out.WriteString(pe.Operator)
out.WriteString(pe.Right.String())
out.WriteString(")")
return out.String()
}
type InfixExpression struct {
Token token.Token // The operator token, e.g. +
Left Expression
Operator string
Right Expression
}
func (oe *InfixExpression) expressionNode() {}
func (oe *InfixExpression) TokenLiteral() string { return oe.Token.Literal }
func (oe *InfixExpression) String() string {
var out bytes.Buffer
out.WriteString("(")
out.WriteString(oe.Left.String())
out.WriteString(" " + oe.Operator + " ")
out.WriteString(oe.Right.String())
out.WriteString(")")
return out.String()
}
type IfExpression struct {
Token token.Token // The 'if' token
Condition Expression
Consequence *BlockStatement
Alternative *BlockStatement
}
func (ie *IfExpression) expressionNode() {}
func (ie *IfExpression) TokenLiteral() string { return ie.Token.Literal }
func (ie *IfExpression) String() string {
var out bytes.Buffer
out.WriteString("if (")
out.WriteString(ie.Condition.String())
out.WriteString(") { ")
out.WriteString(ie.Consequence.String())
out.WriteString(" }")
if ie.Alternative != nil {
out.WriteString(" else { ")
out.WriteString(ie.Alternative.String())
out.WriteString(" }")
}
return out.String()
}
type FunctionLiteral struct {
Token token.Token // The 'fn' token
Parameters []*Identifier
Body *BlockStatement
Name string
}
func (fl *FunctionLiteral) expressionNode() {}
func (fl *FunctionLiteral) TokenLiteral() string { return fl.Token.Literal }
func (fl *FunctionLiteral) String() string {
var out bytes.Buffer
params := []string{}
for _, p := range fl.Parameters {
params = append(params, p.String())
}
out.WriteString(fl.TokenLiteral())
if fl.Name != "" {
out.WriteString(fmt.Sprintf("<%s>", fl.Name))
}
out.WriteString("(")
out.WriteString(strings.Join(params, ", "))
out.WriteString(") ")
out.WriteString(fl.Body.String())
return out.String()
}
type CallExpression struct {
Token token.Token // The '(' token
Function Expression // Identifier or FunctionLiteral
Arguments []Expression
}
func (ce *CallExpression) expressionNode() {}
func (ce *CallExpression) TokenLiteral() string { return ce.Token.Literal }
func (ce *CallExpression) String() string {
var out bytes.Buffer
args := []string{}
for _, a := range ce.Arguments {
args = append(args, a.String())
}
out.WriteString(ce.Function.String())
out.WriteString("(")
out.WriteString(strings.Join(args, ", "))
out.WriteString(")")
return out.String()
}
type StringLiteral struct {
Token token.Token
Value string
}
func (sl *StringLiteral) expressionNode() {}
func (sl *StringLiteral) TokenLiteral() string { return sl.Token.Literal }
func (sl *StringLiteral) String() string { return sl.Token.Literal }
type ArrayLiteral struct {
Token token.Token // the '[' token
Elements []Expression
}
func (al *ArrayLiteral) expressionNode() {}
func (al *ArrayLiteral) TokenLiteral() string { return al.Token.Literal }
func (al *ArrayLiteral) String() string {
var out bytes.Buffer
elements := []string{}
for _, el := range al.Elements {
elements = append(elements, el.String())
}
out.WriteString("[")
out.WriteString(strings.Join(elements, ", "))
out.WriteString("]")
return out.String()
}
type IndexExpression struct {
Token token.Token // The [ token
Left Expression
Index Expression
}
func (ie *IndexExpression) expressionNode() {}
func (ie *IndexExpression) TokenLiteral() string { return ie.Token.Literal }
func (ie *IndexExpression) String() string {
var out bytes.Buffer
out.WriteString("(")
out.WriteString(ie.Left.String())
out.WriteString("[")
out.WriteString(ie.Index.String())
out.WriteString("])")
return out.String()
}
type HashLiteral struct {
Token token.Token // the '{' token
Pairs map[Expression]Expression
}
func (hl *HashLiteral) expressionNode() {}
func (hl *HashLiteral) TokenLiteral() string { return hl.Token.Literal }
func (hl *HashLiteral) String() string {
var out bytes.Buffer
pairs := []string{}
for key, value := range hl.Pairs {
pairs = append(pairs, key.String()+":"+value.String())
}
out.WriteString("{")
out.WriteString(strings.Join(pairs, ", "))
out.WriteString("}")
return out.String()
}
type MacroLiteral struct {
Token token.Token // The 'macro' token
Parameters []*Identifier
Body *BlockStatement
}
func (ml *MacroLiteral) expressionNode() {}
func (ml *MacroLiteral) TokenLiteral() string { return ml.Token.Literal }
func (ml *MacroLiteral) String() string {
var out bytes.Buffer
params := []string{}
for _, p := range ml.Parameters {
params = append(params, p.String())
}
out.WriteString(ml.TokenLiteral())
out.WriteString("(")
out.WriteString(strings.Join(params, ", "))
out.WriteString(") ")
out.WriteString(ml.Body.String())
return out.String()
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,73 @@
package token
type TokenType string
const (
ILLEGAL = "ILLEGAL"
EOF = "EOF"
// Identifiers + literals
IDENT = "IDENT" // add, foobar, x, y, ...
INT = "INT" // 1343456
STRING = "STRING" // "foobar"
// Operators
ASSIGN = "="
PLUS = "+"
MINUS = "-"
BANG = "!"
ASTERISK = "*"
SLASH = "/"
LT = "<"
GT = ">"
EQ = "=="
NOT_EQ = "!="
// Delimiters
COMMA = ","
SEMICOLON = ";"
COLON = ":"
LPAREN = "("
RPAREN = ")"
LBRACE = "{"
RBRACE = "}"
LBRACKET = "["
RBRACKET = "]"
// Keywords
FUNCTION = "FUNCTION"
LET = "LET"
TRUE = "TRUE"
FALSE = "FALSE"
IF = "IF"
ELSE = "ELSE"
RETURN = "RETURN"
MACRO = "MACRO"
WHILE <|user_cursor_is_here|>
)
type Token struct {
Type TokenType
Literal string
}
var keywords = map[string]TokenType{
"fn": FUNCTION,
"let": LET,
"true": TRUE,
"false": FALSE,
"if": IF,
"else": ELSE,
"return": RETURN,
"macro": MACRO,
}
func LookupIdent(ident string) TokenType {
if tok, ok := keywords[ident]; ok {
return tok
}
return IDENT
}

View File

@@ -0,0 +1,72 @@
package token
type TokenType string
const (
ILLEGAL = "ILLEGAL"
EOF = "EOF"
// Identifiers + literals
IDENT = "IDENT" // add, foobar, x, y, ...
INT = "INT" // 1343456
STRING = "STRING" // "foobar"
// Operators
ASSIGN = "="
PLUS = "+"
MINUS = "-"
BANG = "!"
ASTERISK = "*"
SLASH = "/"
LT = "<"
GT = ">"
EQ = "=="
NOT_EQ = "!="
// Delimiters
COMMA = ","
SEMICOLON = ";"
COLON = ":"
LPAREN = "("
RPAREN = ")"
LBRACE = "{"
RBRACE = "}"
LBRACKET = "["
RBRACKET = "]"
// Keywords
FUNCTION = "FUNCTION"
LET = "LET"
TRUE = "TRUE"
FALSE = "FALSE"
IF = "IF"
ELSE = "ELSE"
RETURN = "RETURN"
MACRO = "MACRO"
)
type Token struct {
Type TokenType
Literal string
}
var keywords = map[string]TokenType{
"fn": FUNCTION,
"let": LET,
"true": TRUE,
"false": FALSE,
"if": IF,
"else": ELSE,
"return": RETURN,
"macro": MACRO,
}
func LookupIdent(ident string) TokenType {
if tok, ok := keywords[ident]; ok {
return tok
}
return IDENT
}

View File

@@ -0,0 +1,709 @@
#![cfg_attr(
any(target_os = "linux", target_os = "freebsd", target_os = "windows"),
allow(dead_code)
)]
use anyhow::{Context, Result};
use clap::Parser;
use cli::{ipc::IpcOneShotServer, CliRequest, CliResponse, IpcHandshake};
use collections::HashMap;
use parking_lot::Mutex;
use std::{
env, fs, io,
path::{Path, PathBuf},
process::ExitStatus,
sync::Arc,
thread::{self, JoinHandle},
};
use tempfile::NamedTempFile;
use util::paths::PathWithPosition;
struct Detect;
trait InstalledApp {
fn zed_version_string(&self) -> String;
fn launch(&self, ipc_url: String) -> anyhow::Result<()>;
fn run_foreground(&self, ipc_url: String) -> io::Result<ExitStatus>;
}
#[derive(Parser, Debug)]
#[command(
name = "zed",
disable_version_flag = true,
after_help = "To read from stdin, append '-' (e.g. 'ps axf | zed -')"
)]
struct Args {
/// Wait for all of the given paths to be opened/closed before exiting.
#[arg(short, long)]
wait: bool,
/// Add files to the currently open workspace
#[arg(short, long, overrides_with = "new")]
add: bool,
/// Create a new workspace
#[arg(short, long, overrides_with = "add")]
new: bool,
/// A sequence of space-separated paths that you want to open.
///
/// Use `path:line:row` syntax to open a file at a specific location.
/// Non-existing paths and directories will ignore `:line:row` suffix.
paths_with_position: Vec<String>,
/// Print Zed's version and the app path.
#[arg(short, long)]
version: bool,
/// Run zed in the foreground (useful for debugging)
#[arg(long)]
foreground: bool,
/// Custom path to Zed.app or the zed binary
#[arg(long)]
zed: Option<PathBuf>,
/// Run zed in dev-server mode
#[arg(long)]
dev_server_token: Option<String>,
/// Only update the Zed instance
<|user_cursor_is_here|>
}
fn parse_path_with_position(argument_str: &str) -> anyhow::Result<String> {
let canonicalized = match Path::new(argument_str).canonicalize() {
Ok(existing_path) => PathWithPosition::from_path(existing_path),
Err(_) => {
let path = PathWithPosition::parse_str(argument_str);
let curdir = env::current_dir().context("reteiving current directory")?;
path.map_path(|path| match fs::canonicalize(&path) {
Ok(path) => Ok(path),
Err(e) => {
if let Some(mut parent) = path.parent() {
if parent == Path::new("") {
parent = &curdir
}
match fs::canonicalize(parent) {
Ok(parent) => Ok(parent.join(path.file_name().unwrap())),
Err(_) => Err(e),
}
} else {
Err(e)
}
}
})
}
.with_context(|| format!("parsing as path with position {argument_str}"))?,
};
Ok(canonicalized.to_string(|path| path.to_string_lossy().to_string()))
}
fn main() -> Result<()> {
// Exit flatpak sandbox if needed
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
{
flatpak::try_restart_to_host();
flatpak::ld_extra_libs();
}
// Intercept version designators
#[cfg(target_os = "macos")]
if let Some(channel) = std::env::args().nth(1).filter(|arg| arg.starts_with("--")) {
// When the first argument is a name of a release channel, we're gonna spawn off a cli of that version, with trailing args passed along.
use std::str::FromStr as _;
if let Ok(channel) = release_channel::ReleaseChannel::from_str(&channel[2..]) {
return mac_os::spawn_channel_cli(channel, std::env::args().skip(2).collect());
}
}
let args = Args::parse();
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
let args = flatpak::set_bin_if_no_escape(args);
let app = Detect::detect(args.zed.as_deref()).context("Bundle detection")?;
if args.version {
println!("{}", app.zed_version_string());
return Ok(());
}
let (server, server_name) =
IpcOneShotServer::<IpcHandshake>::new().context("Handshake before Zed spawn")?;
let url = format!("zed-cli://{server_name}");
let open_new_workspace = if args.new {
Some(true)
} else if args.add {
Some(false)
} else {
None
};
let env = Some(std::env::vars().collect::<HashMap<_, _>>());
let exit_status = Arc::new(Mutex::new(None));
let mut paths = vec![];
let mut urls = vec![];
let mut stdin_tmp_file: Option<fs::File> = None;
for path in args.paths_with_position.iter() {
if path.starts_with("zed://")
|| path.starts_with("http://")
|| path.starts_with("https://")
|| path.starts_with("file://")
|| path.starts_with("ssh://")
{
urls.push(path.to_string());
} else if path == "-" && args.paths_with_position.len() == 1 {
let file = NamedTempFile::new()?;
paths.push(file.path().to_string_lossy().to_string());
let (file, _) = file.keep()?;
stdin_tmp_file = Some(file);
} else {
paths.push(parse_path_with_position(path)?)
}
}
if let Some(_) = args.dev_server_token {
return Err(anyhow::anyhow!(
"Dev servers were removed in v0.157.x please upgrade to SSH remoting: https://zed.dev/docs/remote-development"
))?;
}
let sender: JoinHandle<anyhow::Result<()>> = thread::spawn({
let exit_status = exit_status.clone();
move || {
let (_, handshake) = server.accept().context("Handshake after Zed spawn")?;
let (tx, rx) = (handshake.requests, handshake.responses);
tx.send(CliRequest::Open {
paths,
urls,
wait: args.wait,
open_new_workspace,
env,
})?;
while let Ok(response) = rx.recv() {
match response {
CliResponse::Ping => {}
CliResponse::Stdout { message } => println!("{message}"),
CliResponse::Stderr { message } => eprintln!("{message}"),
CliResponse::Exit { status } => {
exit_status.lock().replace(status);
return Ok(());
}
}
}
Ok(())
}
});
let pipe_handle: JoinHandle<anyhow::Result<()>> = thread::spawn(move || {
if let Some(mut tmp_file) = stdin_tmp_file {
let mut stdin = std::io::stdin().lock();
if io::IsTerminal::is_terminal(&stdin) {
return Ok(());
}
let mut buffer = [0; 8 * 1024];
loop {
let bytes_read = io::Read::read(&mut stdin, &mut buffer)?;
if bytes_read == 0 {
break;
}
io::Write::write(&mut tmp_file, &buffer[..bytes_read])?;
}
io::Write::flush(&mut tmp_file)?;
}
Ok(())
});
if args.foreground {
app.run_foreground(url)?;
} else {
app.launch(url)?;
sender.join().unwrap()?;
pipe_handle.join().unwrap()?;
}
if let Some(exit_status) = exit_status.lock().take() {
std::process::exit(exit_status);
}
Ok(())
}
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
mod linux {
use std::{
env,
ffi::OsString,
io,
os::unix::net::{SocketAddr, UnixDatagram},
path::{Path, PathBuf},
process::{self, ExitStatus},
thread,
time::Duration,
};
use anyhow::anyhow;
use cli::FORCE_CLI_MODE_ENV_VAR_NAME;
use fork::Fork;
use once_cell::sync::Lazy;
use crate::{Detect, InstalledApp};
static RELEASE_CHANNEL: Lazy<String> =
Lazy::new(|| include_str!("../../zed/RELEASE_CHANNEL").trim().to_string());
struct App(PathBuf);
impl Detect {
pub fn detect(path: Option<&Path>) -> anyhow::Result<impl InstalledApp> {
let path = if let Some(path) = path {
path.to_path_buf().canonicalize()?
} else {
let cli = env::current_exe()?;
let dir = cli
.parent()
.ok_or_else(|| anyhow!("no parent path for cli"))?;
// libexec is the standard, lib/zed is for Arch (and other non-libexec distros),
// ./zed is for the target directory in development builds.
let possible_locations =
["../libexec/zed-editor", "../lib/zed/zed-editor", "./zed"];
possible_locations
.iter()
.find_map(|p| dir.join(p).canonicalize().ok().filter(|path| path != &cli))
.ok_or_else(|| {
anyhow!("could not find any of: {}", possible_locations.join(", "))
})?
};
Ok(App(path))
}
}
impl InstalledApp for App {
fn zed_version_string(&self) -> String {
format!(
"Zed {}{} {}",
if *RELEASE_CHANNEL == "stable" {
"".to_string()
} else {
format!(" {} ", *RELEASE_CHANNEL)
},
option_env!("RELEASE_VERSION").unwrap_or_default(),
self.0.display(),
)
}
fn launch(&self, ipc_url: String) -> anyhow::Result<()> {
let sock_path = paths::support_dir().join(format!("zed-{}.sock", *RELEASE_CHANNEL));
let sock = UnixDatagram::unbound()?;
if sock.connect(&sock_path).is_err() {
self.boot_background(ipc_url)?;
} else {
sock.send(ipc_url.as_bytes())?;
}
Ok(())
}
fn run_foreground(&self, ipc_url: String) -> io::Result<ExitStatus> {
std::process::Command::new(self.0.clone())
.arg(ipc_url)
.status()
}
}
impl App {
fn boot_background(&self, ipc_url: String) -> anyhow::Result<()> {
let path = &self.0;
match fork::fork() {
Ok(Fork::Parent(_)) => Ok(()),
Ok(Fork::Child) => {
std::env::set_var(FORCE_CLI_MODE_ENV_VAR_NAME, "");
if let Err(_) = fork::setsid() {
eprintln!("failed to setsid: {}", std::io::Error::last_os_error());
process::exit(1);
}
if let Err(_) = fork::close_fd() {
eprintln!("failed to close_fd: {}", std::io::Error::last_os_error());
}
let error =
exec::execvp(path.clone(), &[path.as_os_str(), &OsString::from(ipc_url)]);
// if exec succeeded, we never get here.
eprintln!("failed to exec {:?}: {}", path, error);
process::exit(1)
}
Err(_) => Err(anyhow!(io::Error::last_os_error())),
}
}
fn wait_for_socket(
&self,
sock_addr: &SocketAddr,
sock: &mut UnixDatagram,
) -> Result<(), std::io::Error> {
for _ in 0..100 {
thread::sleep(Duration::from_millis(10));
if sock.connect_addr(&sock_addr).is_ok() {
return Ok(());
}
}
sock.connect_addr(&sock_addr)
}
}
}
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
mod flatpak {
use std::ffi::OsString;
use std::path::PathBuf;
use std::process::Command;
use std::{env, process};
const EXTRA_LIB_ENV_NAME: &'static str = "ZED_FLATPAK_LIB_PATH";
const NO_ESCAPE_ENV_NAME: &'static str = "ZED_FLATPAK_NO_ESCAPE";
/// Adds bundled libraries to LD_LIBRARY_PATH if running under flatpak
pub fn ld_extra_libs() {
let mut paths = if let Ok(paths) = env::var("LD_LIBRARY_PATH") {
env::split_paths(&paths).collect()
} else {
Vec::new()
};
if let Ok(extra_path) = env::var(EXTRA_LIB_ENV_NAME) {
paths.push(extra_path.into());
}
env::set_var("LD_LIBRARY_PATH", env::join_paths(paths).unwrap());
}
/// Restarts outside of the sandbox if currently running within it
pub fn try_restart_to_host() {
if let Some(flatpak_dir) = get_flatpak_dir() {
let mut args = vec!["/usr/bin/flatpak-spawn".into(), "--host".into()];
args.append(&mut get_xdg_env_args());
args.push("--env=ZED_UPDATE_EXPLANATION=Please use flatpak to update zed".into());
args.push(
format!(
"--env={EXTRA_LIB_ENV_NAME}={}",
flatpak_dir.join("lib").to_str().unwrap()
)
.into(),
);
args.push(flatpak_dir.join("bin").join("zed").into());
let mut is_app_location_set = false;
for arg in &env::args_os().collect::<Vec<_>>()[1..] {
args.push(arg.clone());
is_app_location_set |= arg == "--zed";
}
if !is_app_location_set {
args.push("--zed".into());
args.push(flatpak_dir.join("libexec").join("zed-editor").into());
}
let error = exec::execvp("/usr/bin/flatpak-spawn", args);
eprintln!("failed restart cli on host: {:?}", error);
process::exit(1);
}
}
pub fn set_bin_if_no_escape(mut args: super::Args) -> super::Args {
if env::var(NO_ESCAPE_ENV_NAME).is_ok()
&& env::var("FLATPAK_ID").map_or(false, |id| id.starts_with("dev.zed.Zed"))
{
if args.zed.is_none() {
args.zed = Some("/app/libexec/zed-editor".into());
env::set_var("ZED_UPDATE_EXPLANATION", "Please use flatpak to update zed");
}
}
args
}
fn get_flatpak_dir() -> Option<PathBuf> {
if env::var(NO_ESCAPE_ENV_NAME).is_ok() {
return None;
}
if let Ok(flatpak_id) = env::var("FLATPAK_ID") {
if !flatpak_id.starts_with("dev.zed.Zed") {
return None;
}
let install_dir = Command::new("/usr/bin/flatpak-spawn")
.arg("--host")
.arg("flatpak")
.arg("info")
.arg("--show-location")
.arg(flatpak_id)
.output()
.unwrap();
let install_dir = PathBuf::from(String::from_utf8(install_dir.stdout).unwrap().trim());
Some(install_dir.join("files"))
} else {
None
}
}
fn get_xdg_env_args() -> Vec<OsString> {
let xdg_keys = [
"XDG_DATA_HOME",
"XDG_CONFIG_HOME",
"XDG_CACHE_HOME",
"XDG_STATE_HOME",
];
env::vars()
.filter(|(key, _)| xdg_keys.contains(&key.as_str()))
.map(|(key, val)| format!("--env=FLATPAK_{}={}", key, val).into())
.collect()
}
}
// todo("windows")
#[cfg(target_os = "windows")]
mod windows {
use crate::{Detect, InstalledApp};
use std::io;
use std::path::Path;
use std::process::ExitStatus;
struct App;
impl InstalledApp for App {
fn zed_version_string(&self) -> String {
unimplemented!()
}
fn launch(&self, _ipc_url: String) -> anyhow::Result<()> {
unimplemented!()
}
fn run_foreground(&self, _ipc_url: String) -> io::Result<ExitStatus> {
unimplemented!()
}
}
impl Detect {
pub fn detect(_path: Option<&Path>) -> anyhow::Result<impl InstalledApp> {
Ok(App)
}
}
}
#[cfg(target_os = "macos")]
mod mac_os {
use anyhow::{anyhow, Context, Result};
use core_foundation::{
array::{CFArray, CFIndex},
string::kCFStringEncodingUTF8,
url::{CFURLCreateWithBytes, CFURL},
};
use core_services::{kLSLaunchDefaults, LSLaunchURLSpec, LSOpenFromURLSpec, TCFType};
use serde::Deserialize;
use std::{
ffi::OsStr,
fs, io,
path::{Path, PathBuf},
process::{Command, ExitStatus},
ptr,
};
use cli::FORCE_CLI_MODE_ENV_VAR_NAME;
use crate::{Detect, InstalledApp};
#[derive(Debug, Deserialize)]
struct InfoPlist {
#[serde(rename = "CFBundleShortVersionString")]
bundle_short_version_string: String,
}
enum Bundle {
App {
app_bundle: PathBuf,
plist: InfoPlist,
},
LocalPath {
executable: PathBuf,
plist: InfoPlist,
},
}
fn locate_bundle() -> Result<PathBuf> {
let cli_path = std::env::current_exe()?.canonicalize()?;
let mut app_path = cli_path.clone();
while app_path.extension() != Some(OsStr::new("app")) {
if !app_path.pop() {
return Err(anyhow!("cannot find app bundle containing {:?}", cli_path));
}
}
Ok(app_path)
}
impl Detect {
pub fn detect(path: Option<&Path>) -> anyhow::Result<impl InstalledApp> {
let bundle_path = if let Some(bundle_path) = path {
bundle_path
.canonicalize()
.with_context(|| format!("Args bundle path {bundle_path:?} canonicalization"))?
} else {
locate_bundle().context("bundle autodiscovery")?
};
match bundle_path.extension().and_then(|ext| ext.to_str()) {
Some("app") => {
let plist_path = bundle_path.join("Contents/Info.plist");
let plist =
plist::from_file::<_, InfoPlist>(&plist_path).with_context(|| {
format!("Reading *.app bundle plist file at {plist_path:?}")
})?;
Ok(Bundle::App {
app_bundle: bundle_path,
plist,
})
}
_ => {
println!("Bundle path {bundle_path:?} has no *.app extension, attempting to locate a dev build");
let plist_path = bundle_path
.parent()
.with_context(|| format!("Bundle path {bundle_path:?} has no parent"))?
.join("WebRTC.framework/Resources/Info.plist");
let plist =
plist::from_file::<_, InfoPlist>(&plist_path).with_context(|| {
format!("Reading dev bundle plist file at {plist_path:?}")
})?;
Ok(Bundle::LocalPath {
executable: bundle_path,
plist,
})
}
}
}
}
impl InstalledApp for Bundle {
fn zed_version_string(&self) -> String {
let is_dev = matches!(self, Self::LocalPath { .. });
format!(
"Zed {}{} {}",
self.plist().bundle_short_version_string,
if is_dev { " (dev)" } else { "" },
self.path().display(),
)
}
fn launch(&self, url: String) -> anyhow::Result<()> {
match self {
Self::App { app_bundle, .. } => {
let app_path = app_bundle;
let status = unsafe {
let app_url = CFURL::from_path(app_path, true)
.with_context(|| format!("invalid app path {app_path:?}"))?;
let url_to_open = CFURL::wrap_under_create_rule(CFURLCreateWithBytes(
ptr::null(),
url.as_ptr(),
url.len() as CFIndex,
kCFStringEncodingUTF8,
ptr::null(),
));
// equivalent to: open zed-cli:... -a /Applications/Zed\ Preview.app
let urls_to_open =
CFArray::from_copyable(&[url_to_open.as_concrete_TypeRef()]);
LSOpenFromURLSpec(
&LSLaunchURLSpec {
appURL: app_url.as_concrete_TypeRef(),
itemURLs: urls_to_open.as_concrete_TypeRef(),
passThruParams: ptr::null(),
launchFlags: kLSLaunchDefaults,
asyncRefCon: ptr::null_mut(),
},
ptr::null_mut(),
)
};
anyhow::ensure!(
status == 0,
"cannot start app bundle {}",
self.zed_version_string()
);
}
Self::LocalPath { executable, .. } => {
let executable_parent = executable
.parent()
.with_context(|| format!("Executable {executable:?} path has no parent"))?;
let subprocess_stdout_file = fs::File::create(
executable_parent.join("zed_dev.log"),
)
.with_context(|| format!("Log file creation in {executable_parent:?}"))?;
let subprocess_stdin_file =
subprocess_stdout_file.try_clone().with_context(|| {
format!("Cloning descriptor for file {subprocess_stdout_file:?}")
})?;
let mut command = std::process::Command::new(executable);
let command = command
.env(FORCE_CLI_MODE_ENV_VAR_NAME, "")
.stderr(subprocess_stdout_file)
.stdout(subprocess_stdin_file)
.arg(url);
command
.spawn()
.with_context(|| format!("Spawning {command:?}"))?;
}
}
Ok(())
}
fn run_foreground(&self, ipc_url: String) -> io::Result<ExitStatus> {
let path = match self {
Bundle::App { app_bundle, .. } => app_bundle.join("Contents/MacOS/zed"),
Bundle::LocalPath { executable, .. } => executable.clone(),
};
std::process::Command::new(path).arg(ipc_url).status()
}
}
impl Bundle {
fn plist(&self) -> &InfoPlist {
match self {
Self::App { plist, .. } => plist,
Self::LocalPath { plist, .. } => plist,
}
}
fn path(&self) -> &Path {
match self {
Self::App { app_bundle, .. } => app_bundle,
Self::LocalPath { executable, .. } => executable,
}
}
}
pub(super) fn spawn_channel_cli(
channel: release_channel::ReleaseChannel,
leftover_args: Vec<String>,
) -> Result<()> {
use anyhow::bail;
let app_id_prompt = format!("id of app \"{}\"", channel.display_name());
let app_id_output = Command::new("osascript")
.arg("-e")
.arg(&app_id_prompt)
.output()?;
if !app_id_output.status.success() {
bail!("Could not determine app id for {}", channel.display_name());
}
let app_name = String::from_utf8(app_id_output.stdout)?.trim().to_owned();
let app_path_prompt = format!("kMDItemCFBundleIdentifier == '{app_name}'");
let app_path_output = Command::new("mdfind").arg(app_path_prompt).output()?;
if !app_path_output.status.success() {
bail!(
"Could not determine app path for {}",
channel.display_name()
);
}
let app_path = String::from_utf8(app_path_output.stdout)?.trim().to_owned();
let cli_path = format!("{app_path}/Contents/MacOS/cli");
Command::new(cli_path).args(leftover_args).spawn()?;
Ok(())
}
}

View File

@@ -0,0 +1,712 @@
#![cfg_attr(
any(target_os = "linux", target_os = "freebsd", target_os = "windows"),
allow(dead_code)
)]
use anyhow::{Context, Result};
use clap::Parser;
use cli::{ipc::IpcOneShotServer, CliRequest, CliResponse, IpcHandshake};
use collections::HashMap;
use parking_lot::Mutex;
use std::{
env, fs, io,
path::{Path, PathBuf},
process::ExitStatus,
sync::Arc,
thread::{self, JoinHandle},
};
use tempfile::NamedTempFile;
use util::paths::PathWithPosition;
struct Detect;
trait InstalledApp {
fn zed_version_string(&self) -> String;
fn launch(&self, ipc_url: String) -> anyhow::Result<()>;
fn run_foreground(&self, ipc_url: String) -> io::Result<ExitStatus>;
}
#[derive(Parser, Debug)]
#[command(
name = "zed",
disable_version_flag = true,
after_help = "To read from stdin, append '-' (e.g. 'ps axf | zed -')"
)]
struct Args {
/// Wait for all of the given paths to be opened/closed before exiting.
#[arg(short, long)]
wait: bool,
/// Add files to the currently open workspace
#[arg(short, long, overrides_with = "new")]
add: bool,
/// Create a new workspace
#[arg(short, long, overrides_with = "add")]
new: bool,
/// A sequence of space-separated paths that you want to open.
///
/// Use `path:line:row` syntax to open a file at a specific location.
/// Non-existing paths and directories will ignore `:line:row` suffix.
paths_with_position: Vec<String>,
/// Print Zed's version and the app path.
#[arg(short, long)]
version: bool,
/// Run zed in the foreground (useful for debugging)
#[arg(long)]
foreground: bool,
/// Custom path to Zed.app or the zed binary
#[arg(long)]
zed: Option<PathBuf>,
/// Run zed in dev-server mode
#[arg(long)]
dev_server_token: Option<String>,
/// Only update the Zed instance
#[arg(long)]
update: bool,
}
fn parse_path_with_position(argument_str: &str) -> anyhow::Result<String> {
let canonicalized = match Path::new(argument_str).canonicalize() {
Ok(existing_path) => PathWithPosition::from_path(existing_path),
Err(_) => {
let path = PathWithPosition::parse_str(argument_str);
let curdir = env::current_dir().context("reteiving current directory")?;
path.map_path(|path| match fs::canonicalize(&path) {
Ok(path) => Ok(path),
Err(e) => {
if let Some(mut parent) = path.parent() {
if parent == Path::new("") {
parent = &curdir
}
match fs::canonicalize(parent) {
Ok(parent) => Ok(parent.join(path.file_name().unwrap())),
Err(_) => Err(e),
}
} else {
Err(e)
}
}
})
}
.with_context(|| format!("parsing as path with position {argument_str}"))?,
};
Ok(canonicalized.to_string(|path| path.to_string_lossy().to_string()))
}
fn main() -> Result<()> {
// Exit flatpak sandbox if needed
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
{
flatpak::try_restart_to_host();
flatpak::ld_extra_libs();
}
// Intercept version designators
#[cfg(target_os = "macos")]
if let Some(channel) = std::env::args().nth(1).filter(|arg| arg.starts_with("--")) {
// When the first argument is a name of a release channel, we're gonna spawn off a cli of that version, with trailing args passed along.
use std::str::FromStr as _;
if let Ok(channel) = release_channel::ReleaseChannel::from_str(&channel[2..]) {
return mac_os::spawn_channel_cli(channel, std::env::args().skip(2).collect());
}
}
let args = Args::parse();
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
let args = flatpak::set_bin_if_no_escape(args);
let app = Detect::detect(args.zed.as_deref()).context("Bundle detection")?;
if args.version {
println!("{}", app.zed_version_string());
return Ok(());
}
if<|user_cursor_is_here|>
let (server, server_name) =
IpcOneShotServer::<IpcHandshake>::new().context("Handshake before Zed spawn")?;
let url = format!("zed-cli://{server_name}");
let open_new_workspace = if args.new {
Some(true)
} else if args.add {
Some(false)
} else {
None
};
let env = Some(std::env::vars().collect::<HashMap<_, _>>());
let exit_status = Arc::new(Mutex::new(None));
let mut paths = vec![];
let mut urls = vec![];
let mut stdin_tmp_file: Option<fs::File> = None;
for path in args.paths_with_position.iter() {
if path.starts_with("zed://")
|| path.starts_with("http://")
|| path.starts_with("https://")
|| path.starts_with("file://")
|| path.starts_with("ssh://")
{
urls.push(path.to_string());
} else if path == "-" && args.paths_with_position.len() == 1 {
let file = NamedTempFile::new()?;
paths.push(file.path().to_string_lossy().to_string());
let (file, _) = file.keep()?;
stdin_tmp_file = Some(file);
} else {
paths.push(parse_path_with_position(path)?)
}
}
if let Some(_) = args.dev_server_token {
return Err(anyhow::anyhow!(
"Dev servers were removed in v0.157.x please upgrade to SSH remoting: https://zed.dev/docs/remote-development"
))?;
}
let sender: JoinHandle<anyhow::Result<()>> = thread::spawn({
let exit_status = exit_status.clone();
move || {
let (_, handshake) = server.accept().context("Handshake after Zed spawn")?;
let (tx, rx) = (handshake.requests, handshake.responses);
tx.send(CliRequest::Open {
paths,
urls,
wait: args.wait,
open_new_workspace,
env,
})?;
while let Ok(response) = rx.recv() {
match response {
CliResponse::Ping => {}
CliResponse::Stdout { message } => println!("{message}"),
CliResponse::Stderr { message } => eprintln!("{message}"),
CliResponse::Exit { status } => {
exit_status.lock().replace(status);
return Ok(());
}
}
}
Ok(())
}
});
let pipe_handle: JoinHandle<anyhow::Result<()>> = thread::spawn(move || {
if let Some(mut tmp_file) = stdin_tmp_file {
let mut stdin = std::io::stdin().lock();
if io::IsTerminal::is_terminal(&stdin) {
return Ok(());
}
let mut buffer = [0; 8 * 1024];
loop {
let bytes_read = io::Read::read(&mut stdin, &mut buffer)?;
if bytes_read == 0 {
break;
}
io::Write::write(&mut tmp_file, &buffer[..bytes_read])?;
}
io::Write::flush(&mut tmp_file)?;
}
Ok(())
});
if args.foreground {
app.run_foreground(url)?;
} else {
app.launch(url)?;
sender.join().unwrap()?;
pipe_handle.join().unwrap()?;
}
if let Some(exit_status) = exit_status.lock().take() {
std::process::exit(exit_status);
}
Ok(())
}
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
mod linux {
use std::{
env,
ffi::OsString,
io,
os::unix::net::{SocketAddr, UnixDatagram},
path::{Path, PathBuf},
process::{self, ExitStatus},
thread,
time::Duration,
};
use anyhow::anyhow;
use cli::FORCE_CLI_MODE_ENV_VAR_NAME;
use fork::Fork;
use once_cell::sync::Lazy;
use crate::{Detect, InstalledApp};
static RELEASE_CHANNEL: Lazy<String> =
Lazy::new(|| include_str!("../../zed/RELEASE_CHANNEL").trim().to_string());
struct App(PathBuf);
impl Detect {
pub fn detect(path: Option<&Path>) -> anyhow::Result<impl InstalledApp> {
let path = if let Some(path) = path {
path.to_path_buf().canonicalize()?
} else {
let cli = env::current_exe()?;
let dir = cli
.parent()
.ok_or_else(|| anyhow!("no parent path for cli"))?;
// libexec is the standard, lib/zed is for Arch (and other non-libexec distros),
// ./zed is for the target directory in development builds.
let possible_locations =
["../libexec/zed-editor", "../lib/zed/zed-editor", "./zed"];
possible_locations
.iter()
.find_map(|p| dir.join(p).canonicalize().ok().filter(|path| path != &cli))
.ok_or_else(|| {
anyhow!("could not find any of: {}", possible_locations.join(", "))
})?
};
Ok(App(path))
}
}
impl InstalledApp for App {
fn zed_version_string(&self) -> String {
format!(
"Zed {}{} {}",
if *RELEASE_CHANNEL == "stable" {
"".to_string()
} else {
format!(" {} ", *RELEASE_CHANNEL)
},
option_env!("RELEASE_VERSION").unwrap_or_default(),
self.0.display(),
)
}
fn launch(&self, ipc_url: String) -> anyhow::Result<()> {
let sock_path = paths::support_dir().join(format!("zed-{}.sock", *RELEASE_CHANNEL));
let sock = UnixDatagram::unbound()?;
if sock.connect(&sock_path).is_err() {
self.boot_background(ipc_url)?;
} else {
sock.send(ipc_url.as_bytes())?;
}
Ok(())
}
fn run_foreground(&self, ipc_url: String) -> io::Result<ExitStatus> {
std::process::Command::new(self.0.clone())
.arg(ipc_url)
.status()
}
}
impl App {
fn boot_background(&self, ipc_url: String) -> anyhow::Result<()> {
let path = &self.0;
match fork::fork() {
Ok(Fork::Parent(_)) => Ok(()),
Ok(Fork::Child) => {
std::env::set_var(FORCE_CLI_MODE_ENV_VAR_NAME, "");
if let Err(_) = fork::setsid() {
eprintln!("failed to setsid: {}", std::io::Error::last_os_error());
process::exit(1);
}
if let Err(_) = fork::close_fd() {
eprintln!("failed to close_fd: {}", std::io::Error::last_os_error());
}
let error =
exec::execvp(path.clone(), &[path.as_os_str(), &OsString::from(ipc_url)]);
// if exec succeeded, we never get here.
eprintln!("failed to exec {:?}: {}", path, error);
process::exit(1)
}
Err(_) => Err(anyhow!(io::Error::last_os_error())),
}
}
fn wait_for_socket(
&self,
sock_addr: &SocketAddr,
sock: &mut UnixDatagram,
) -> Result<(), std::io::Error> {
for _ in 0..100 {
thread::sleep(Duration::from_millis(10));
if sock.connect_addr(&sock_addr).is_ok() {
return Ok(());
}
}
sock.connect_addr(&sock_addr)
}
}
}
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
mod flatpak {
use std::ffi::OsString;
use std::path::PathBuf;
use std::process::Command;
use std::{env, process};
const EXTRA_LIB_ENV_NAME: &'static str = "ZED_FLATPAK_LIB_PATH";
const NO_ESCAPE_ENV_NAME: &'static str = "ZED_FLATPAK_NO_ESCAPE";
/// Adds bundled libraries to LD_LIBRARY_PATH if running under flatpak
pub fn ld_extra_libs() {
let mut paths = if let Ok(paths) = env::var("LD_LIBRARY_PATH") {
env::split_paths(&paths).collect()
} else {
Vec::new()
};
if let Ok(extra_path) = env::var(EXTRA_LIB_ENV_NAME) {
paths.push(extra_path.into());
}
env::set_var("LD_LIBRARY_PATH", env::join_paths(paths).unwrap());
}
/// Restarts outside of the sandbox if currently running within it
pub fn try_restart_to_host() {
if let Some(flatpak_dir) = get_flatpak_dir() {
let mut args = vec!["/usr/bin/flatpak-spawn".into(), "--host".into()];
args.append(&mut get_xdg_env_args());
args.push("--env=ZED_UPDATE_EXPLANATION=Please use flatpak to update zed".into());
args.push(
format!(
"--env={EXTRA_LIB_ENV_NAME}={}",
flatpak_dir.join("lib").to_str().unwrap()
)
.into(),
);
args.push(flatpak_dir.join("bin").join("zed").into());
let mut is_app_location_set = false;
for arg in &env::args_os().collect::<Vec<_>>()[1..] {
args.push(arg.clone());
is_app_location_set |= arg == "--zed";
}
if !is_app_location_set {
args.push("--zed".into());
args.push(flatpak_dir.join("libexec").join("zed-editor").into());
}
let error = exec::execvp("/usr/bin/flatpak-spawn", args);
eprintln!("failed restart cli on host: {:?}", error);
process::exit(1);
}
}
pub fn set_bin_if_no_escape(mut args: super::Args) -> super::Args {
if env::var(NO_ESCAPE_ENV_NAME).is_ok()
&& env::var("FLATPAK_ID").map_or(false, |id| id.starts_with("dev.zed.Zed"))
{
if args.zed.is_none() {
args.zed = Some("/app/libexec/zed-editor".into());
env::set_var("ZED_UPDATE_EXPLANATION", "Please use flatpak to update zed");
}
}
args
}
fn get_flatpak_dir() -> Option<PathBuf> {
if env::var(NO_ESCAPE_ENV_NAME).is_ok() {
return None;
}
if let Ok(flatpak_id) = env::var("FLATPAK_ID") {
if !flatpak_id.starts_with("dev.zed.Zed") {
return None;
}
let install_dir = Command::new("/usr/bin/flatpak-spawn")
.arg("--host")
.arg("flatpak")
.arg("info")
.arg("--show-location")
.arg(flatpak_id)
.output()
.unwrap();
let install_dir = PathBuf::from(String::from_utf8(install_dir.stdout).unwrap().trim());
Some(install_dir.join("files"))
} else {
None
}
}
fn get_xdg_env_args() -> Vec<OsString> {
let xdg_keys = [
"XDG_DATA_HOME",
"XDG_CONFIG_HOME",
"XDG_CACHE_HOME",
"XDG_STATE_HOME",
];
env::vars()
.filter(|(key, _)| xdg_keys.contains(&key.as_str()))
.map(|(key, val)| format!("--env=FLATPAK_{}={}", key, val).into())
.collect()
}
}
// todo("windows")
#[cfg(target_os = "windows")]
mod windows {
use crate::{Detect, InstalledApp};
use std::io;
use std::path::Path;
use std::process::ExitStatus;
struct App;
impl InstalledApp for App {
fn zed_version_string(&self) -> String {
unimplemented!()
}
fn launch(&self, _ipc_url: String) -> anyhow::Result<()> {
unimplemented!()
}
fn run_foreground(&self, _ipc_url: String) -> io::Result<ExitStatus> {
unimplemented!()
}
}
impl Detect {
pub fn detect(_path: Option<&Path>) -> anyhow::Result<impl InstalledApp> {
Ok(App)
}
}
}
#[cfg(target_os = "macos")]
mod mac_os {
use anyhow::{anyhow, Context, Result};
use core_foundation::{
array::{CFArray, CFIndex},
string::kCFStringEncodingUTF8,
url::{CFURLCreateWithBytes, CFURL},
};
use core_services::{kLSLaunchDefaults, LSLaunchURLSpec, LSOpenFromURLSpec, TCFType};
use serde::Deserialize;
use std::{
ffi::OsStr,
fs, io,
path::{Path, PathBuf},
process::{Command, ExitStatus},
ptr,
};
use cli::FORCE_CLI_MODE_ENV_VAR_NAME;
use crate::{Detect, InstalledApp};
#[derive(Debug, Deserialize)]
struct InfoPlist {
#[serde(rename = "CFBundleShortVersionString")]
bundle_short_version_string: String,
}
enum Bundle {
App {
app_bundle: PathBuf,
plist: InfoPlist,
},
LocalPath {
executable: PathBuf,
plist: InfoPlist,
},
}
fn locate_bundle() -> Result<PathBuf> {
let cli_path = std::env::current_exe()?.canonicalize()?;
let mut app_path = cli_path.clone();
while app_path.extension() != Some(OsStr::new("app")) {
if !app_path.pop() {
return Err(anyhow!("cannot find app bundle containing {:?}", cli_path));
}
}
Ok(app_path)
}
impl Detect {
pub fn detect(path: Option<&Path>) -> anyhow::Result<impl InstalledApp> {
let bundle_path = if let Some(bundle_path) = path {
bundle_path
.canonicalize()
.with_context(|| format!("Args bundle path {bundle_path:?} canonicalization"))?
} else {
locate_bundle().context("bundle autodiscovery")?
};
match bundle_path.extension().and_then(|ext| ext.to_str()) {
Some("app") => {
let plist_path = bundle_path.join("Contents/Info.plist");
let plist =
plist::from_file::<_, InfoPlist>(&plist_path).with_context(|| {
format!("Reading *.app bundle plist file at {plist_path:?}")
})?;
Ok(Bundle::App {
app_bundle: bundle_path,
plist,
})
}
_ => {
println!("Bundle path {bundle_path:?} has no *.app extension, attempting to locate a dev build");
let plist_path = bundle_path
.parent()
.with_context(|| format!("Bundle path {bundle_path:?} has no parent"))?
.join("WebRTC.framework/Resources/Info.plist");
let plist =
plist::from_file::<_, InfoPlist>(&plist_path).with_context(|| {
format!("Reading dev bundle plist file at {plist_path:?}")
})?;
Ok(Bundle::LocalPath {
executable: bundle_path,
plist,
})
}
}
}
}
impl InstalledApp for Bundle {
fn zed_version_string(&self) -> String {
let is_dev = matches!(self, Self::LocalPath { .. });
format!(
"Zed {}{} {}",
self.plist().bundle_short_version_string,
if is_dev { " (dev)" } else { "" },
self.path().display(),
)
}
fn launch(&self, url: String) -> anyhow::Result<()> {
match self {
Self::App { app_bundle, .. } => {
let app_path = app_bundle;
let status = unsafe {
let app_url = CFURL::from_path(app_path, true)
.with_context(|| format!("invalid app path {app_path:?}"))?;
let url_to_open = CFURL::wrap_under_create_rule(CFURLCreateWithBytes(
ptr::null(),
url.as_ptr(),
url.len() as CFIndex,
kCFStringEncodingUTF8,
ptr::null(),
));
// equivalent to: open zed-cli:... -a /Applications/Zed\ Preview.app
let urls_to_open =
CFArray::from_copyable(&[url_to_open.as_concrete_TypeRef()]);
LSOpenFromURLSpec(
&LSLaunchURLSpec {
appURL: app_url.as_concrete_TypeRef(),
itemURLs: urls_to_open.as_concrete_TypeRef(),
passThruParams: ptr::null(),
launchFlags: kLSLaunchDefaults,
asyncRefCon: ptr::null_mut(),
},
ptr::null_mut(),
)
};
anyhow::ensure!(
status == 0,
"cannot start app bundle {}",
self.zed_version_string()
);
}
Self::LocalPath { executable, .. } => {
let executable_parent = executable
.parent()
.with_context(|| format!("Executable {executable:?} path has no parent"))?;
let subprocess_stdout_file = fs::File::create(
executable_parent.join("zed_dev.log"),
)
.with_context(|| format!("Log file creation in {executable_parent:?}"))?;
let subprocess_stdin_file =
subprocess_stdout_file.try_clone().with_context(|| {
format!("Cloning descriptor for file {subprocess_stdout_file:?}")
})?;
let mut command = std::process::Command::new(executable);
let command = command
.env(FORCE_CLI_MODE_ENV_VAR_NAME, "")
.stderr(subprocess_stdout_file)
.stdout(subprocess_stdin_file)
.arg(url);
command
.spawn()
.with_context(|| format!("Spawning {command:?}"))?;
}
}
Ok(())
}
fn run_foreground(&self, ipc_url: String) -> io::Result<ExitStatus> {
let path = match self {
Bundle::App { app_bundle, .. } => app_bundle.join("Contents/MacOS/zed"),
Bundle::LocalPath { executable, .. } => executable.clone(),
};
std::process::Command::new(path).arg(ipc_url).status()
}
}
impl Bundle {
fn plist(&self) -> &InfoPlist {
match self {
Self::App { plist, .. } => plist,
Self::LocalPath { plist, .. } => plist,
}
}
fn path(&self) -> &Path {
match self {
Self::App { app_bundle, .. } => app_bundle,
Self::LocalPath { executable, .. } => executable,
}
}
}
pub(super) fn spawn_channel_cli(
channel: release_channel::ReleaseChannel,
leftover_args: Vec<String>,
) -> Result<()> {
use anyhow::bail;
let app_id_prompt = format!("id of app \"{}\"", channel.display_name());
let app_id_output = Command::new("osascript")
.arg("-e")
.arg(&app_id_prompt)
.output()?;
if !app_id_output.status.success() {
bail!("Could not determine app id for {}", channel.display_name());
}
let app_name = String::from_utf8(app_id_output.stdout)?.trim().to_owned();
let app_path_prompt = format!("kMDItemCFBundleIdentifier == '{app_name}'");
let app_path_output = Command::new("mdfind").arg(app_path_prompt).output()?;
if !app_path_output.status.success() {
bail!(
"Could not determine app path for {}",
channel.display_name()
);
}
let app_path = String::from_utf8(app_path_output.stdout)?.trim().to_owned();
let cli_path = format!("{app_path}/Contents/MacOS/cli");
Command::new(cli_path).args(leftover_args).spawn()?;
Ok(())
}
}

View File

@@ -0,0 +1,707 @@
#![cfg_attr(
any(target_os = "linux", target_os = "freebsd", target_os = "windows"),
allow(dead_code)
)]
use anyhow::{Context, Result};
use clap::Parser;
use cli::{ipc::IpcOneShotServer, CliRequest, CliResponse, IpcHandshake};
use collections::HashMap;
use parking_lot::Mutex;
use std::{
env, fs, io,
path::{Path, PathBuf},
process::ExitStatus,
sync::Arc,
thread::{self, JoinHandle},
};
use tempfile::NamedTempFile;
use util::paths::PathWithPosition;
struct Detect;
trait InstalledApp {
fn zed_version_string(&self) -> String;
fn launch(&self, ipc_url: String) -> anyhow::Result<()>;
fn run_foreground(&self, ipc_url: String) -> io::Result<ExitStatus>;
}
#[derive(Parser, Debug)]
#[command(
name = "zed",
disable_version_flag = true,
after_help = "To read from stdin, append '-' (e.g. 'ps axf | zed -')"
)]
struct Args {
/// Wait for all of the given paths to be opened/closed before exiting.
#[arg(short, long)]
wait: bool,
/// Add files to the currently open workspace
#[arg(short, long, overrides_with = "new")]
add: bool,
/// Create a new workspace
#[arg(short, long, overrides_with = "add")]
new: bool,
/// A sequence of space-separated paths that you want to open.
///
/// Use `path:line:row` syntax to open a file at a specific location.
/// Non-existing paths and directories will ignore `:line:row` suffix.
paths_with_position: Vec<String>,
/// Print Zed's version and the app path.
#[arg(short, long)]
version: bool,
/// Run zed in the foreground (useful for debugging)
#[arg(long)]
foreground: bool,
/// Custom path to Zed.app or the zed binary
#[arg(long)]
zed: Option<PathBuf>,
/// Run zed in dev-server mode
#[arg(long)]
dev_server_token: Option<String>,
}
fn parse_path_with_position(argument_str: &str) -> anyhow::Result<String> {
let canonicalized = match Path::new(argument_str).canonicalize() {
Ok(existing_path) => PathWithPosition::from_path(existing_path),
Err(_) => {
let path = PathWithPosition::parse_str(argument_str);
let curdir = env::current_dir().context("reteiving current directory")?;
path.map_path(|path| match fs::canonicalize(&path) {
Ok(path) => Ok(path),
Err(e) => {
if let Some(mut parent) = path.parent() {
if parent == Path::new("") {
parent = &curdir
}
match fs::canonicalize(parent) {
Ok(parent) => Ok(parent.join(path.file_name().unwrap())),
Err(_) => Err(e),
}
} else {
Err(e)
}
}
})
}
.with_context(|| format!("parsing as path with position {argument_str}"))?,
};
Ok(canonicalized.to_string(|path| path.to_string_lossy().to_string()))
}
fn main() -> Result<()> {
// Exit flatpak sandbox if needed
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
{
flatpak::try_restart_to_host();
flatpak::ld_extra_libs();
}
// Intercept version designators
#[cfg(target_os = "macos")]
if let Some(channel) = std::env::args().nth(1).filter(|arg| arg.starts_with("--")) {
// When the first argument is a name of a release channel, we're gonna spawn off a cli of that version, with trailing args passed along.
use std::str::FromStr as _;
if let Ok(channel) = release_channel::ReleaseChannel::from_str(&channel[2..]) {
return mac_os::spawn_channel_cli(channel, std::env::args().skip(2).collect());
}
}
let args = Args::parse();
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
let args = flatpak::set_bin_if_no_escape(args);
let app = Detect::detect(args.zed.as_deref()).context("Bundle detection")?;
if args.version {
println!("{}", app.zed_version_string());
return Ok(());
}
let (server, server_name) =
IpcOneShotServer::<IpcHandshake>::new().context("Handshake before Zed spawn")?;
let url = format!("zed-cli://{server_name}");
let open_new_workspace = if args.new {
Some(true)
} else if args.add {
Some(false)
} else {
None
};
let env = Some(std::env::vars().collect::<HashMap<_, _>>());
let exit_status = Arc::new(Mutex::new(None));
let mut paths = vec![];
let mut urls = vec![];
let mut stdin_tmp_file: Option<fs::File> = None;
for path in args.paths_with_position.iter() {
if path.starts_with("zed://")
|| path.starts_with("http://")
|| path.starts_with("https://")
|| path.starts_with("file://")
|| path.starts_with("ssh://")
{
urls.push(path.to_string());
} else if path == "-" && args.paths_with_position.len() == 1 {
let file = NamedTempFile::new()?;
paths.push(file.path().to_string_lossy().to_string());
let (file, _) = file.keep()?;
stdin_tmp_file = Some(file);
} else {
paths.push(parse_path_with_position(path)?)
}
}
if let Some(_) = args.dev_server_token {
return Err(anyhow::anyhow!(
"Dev servers were removed in v0.157.x please upgrade to SSH remoting: https://zed.dev/docs/remote-development"
))?;
}
let sender: JoinHandle<anyhow::Result<()>> = thread::spawn({
let exit_status = exit_status.clone();
move || {
let (_, handshake) = server.accept().context("Handshake after Zed spawn")?;
let (tx, rx) = (handshake.requests, handshake.responses);
tx.send(CliRequest::Open {
paths,
urls,
wait: args.wait,
open_new_workspace,
env,
})?;
while let Ok(response) = rx.recv() {
match response {
CliResponse::Ping => {}
CliResponse::Stdout { message } => println!("{message}"),
CliResponse::Stderr { message } => eprintln!("{message}"),
CliResponse::Exit { status } => {
exit_status.lock().replace(status);
return Ok(());
}
}
}
Ok(())
}
});
let pipe_handle: JoinHandle<anyhow::Result<()>> = thread::spawn(move || {
if let Some(mut tmp_file) = stdin_tmp_file {
let mut stdin = std::io::stdin().lock();
if io::IsTerminal::is_terminal(&stdin) {
return Ok(());
}
let mut buffer = [0; 8 * 1024];
loop {
let bytes_read = io::Read::read(&mut stdin, &mut buffer)?;
if bytes_read == 0 {
break;
}
io::Write::write(&mut tmp_file, &buffer[..bytes_read])?;
}
io::Write::flush(&mut tmp_file)?;
}
Ok(())
});
if args.foreground {
app.run_foreground(url)?;
} else {
app.launch(url)?;
sender.join().unwrap()?;
pipe_handle.join().unwrap()?;
}
if let Some(exit_status) = exit_status.lock().take() {
std::process::exit(exit_status);
}
Ok(())
}
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
mod linux {
use std::{
env,
ffi::OsString,
io,
os::unix::net::{SocketAddr, UnixDatagram},
path::{Path, PathBuf},
process::{self, ExitStatus},
thread,
time::Duration,
};
use anyhow::anyhow;
use cli::FORCE_CLI_MODE_ENV_VAR_NAME;
use fork::Fork;
use once_cell::sync::Lazy;
use crate::{Detect, InstalledApp};
static RELEASE_CHANNEL: Lazy<String> =
Lazy::new(|| include_str!("../../zed/RELEASE_CHANNEL").trim().to_string());
struct App(PathBuf);
impl Detect {
pub fn detect(path: Option<&Path>) -> anyhow::Result<impl InstalledApp> {
let path = if let Some(path) = path {
path.to_path_buf().canonicalize()?
} else {
let cli = env::current_exe()?;
let dir = cli
.parent()
.ok_or_else(|| anyhow!("no parent path for cli"))?;
// libexec is the standard, lib/zed is for Arch (and other non-libexec distros),
// ./zed is for the target directory in development builds.
let possible_locations =
["../libexec/zed-editor", "../lib/zed/zed-editor", "./zed"];
possible_locations
.iter()
.find_map(|p| dir.join(p).canonicalize().ok().filter(|path| path != &cli))
.ok_or_else(|| {
anyhow!("could not find any of: {}", possible_locations.join(", "))
})?
};
Ok(App(path))
}
}
impl InstalledApp for App {
fn zed_version_string(&self) -> String {
format!(
"Zed {}{} {}",
if *RELEASE_CHANNEL == "stable" {
"".to_string()
} else {
format!(" {} ", *RELEASE_CHANNEL)
},
option_env!("RELEASE_VERSION").unwrap_or_default(),
self.0.display(),
)
}
fn launch(&self, ipc_url: String) -> anyhow::Result<()> {
let sock_path = paths::support_dir().join(format!("zed-{}.sock", *RELEASE_CHANNEL));
let sock = UnixDatagram::unbound()?;
if sock.connect(&sock_path).is_err() {
self.boot_background(ipc_url)?;
} else {
sock.send(ipc_url.as_bytes())?;
}
Ok(())
}
fn run_foreground(&self, ipc_url: String) -> io::Result<ExitStatus> {
std::process::Command::new(self.0.clone())
.arg(ipc_url)
.status()
}
}
impl App {
fn boot_background(&self, ipc_url: String) -> anyhow::Result<()> {
let path = &self.0;
match fork::fork() {
Ok(Fork::Parent(_)) => Ok(()),
Ok(Fork::Child) => {
std::env::set_var(FORCE_CLI_MODE_ENV_VAR_NAME, "");
if let Err(_) = fork::setsid() {
eprintln!("failed to setsid: {}", std::io::Error::last_os_error());
process::exit(1);
}
if let Err(_) = fork::close_fd() {
eprintln!("failed to close_fd: {}", std::io::Error::last_os_error());
}
let error =
exec::execvp(path.clone(), &[path.as_os_str(), &OsString::from(ipc_url)]);
// if exec succeeded, we never get here.
eprintln!("failed to exec {:?}: {}", path, error);
process::exit(1)
}
Err(_) => Err(anyhow!(io::Error::last_os_error())),
}
}
fn wait_for_socket(
&self,
sock_addr: &SocketAddr,
sock: &mut UnixDatagram,
) -> Result<(), std::io::Error> {
for _ in 0..100 {
thread::sleep(Duration::from_millis(10));
if sock.connect_addr(&sock_addr).is_ok() {
return Ok(());
}
}
sock.connect_addr(&sock_addr)
}
}
}
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
mod flatpak {
use std::ffi::OsString;
use std::path::PathBuf;
use std::process::Command;
use std::{env, process};
const EXTRA_LIB_ENV_NAME: &'static str = "ZED_FLATPAK_LIB_PATH";
const NO_ESCAPE_ENV_NAME: &'static str = "ZED_FLATPAK_NO_ESCAPE";
/// Adds bundled libraries to LD_LIBRARY_PATH if running under flatpak
pub fn ld_extra_libs() {
let mut paths = if let Ok(paths) = env::var("LD_LIBRARY_PATH") {
env::split_paths(&paths).collect()
} else {
Vec::new()
};
if let Ok(extra_path) = env::var(EXTRA_LIB_ENV_NAME) {
paths.push(extra_path.into());
}
env::set_var("LD_LIBRARY_PATH", env::join_paths(paths).unwrap());
}
/// Restarts outside of the sandbox if currently running within it
pub fn try_restart_to_host() {
if let Some(flatpak_dir) = get_flatpak_dir() {
let mut args = vec!["/usr/bin/flatpak-spawn".into(), "--host".into()];
args.append(&mut get_xdg_env_args());
args.push("--env=ZED_UPDATE_EXPLANATION=Please use flatpak to update zed".into());
args.push(
format!(
"--env={EXTRA_LIB_ENV_NAME}={}",
flatpak_dir.join("lib").to_str().unwrap()
)
.into(),
);
args.push(flatpak_dir.join("bin").join("zed").into());
let mut is_app_location_set = false;
for arg in &env::args_os().collect::<Vec<_>>()[1..] {
args.push(arg.clone());
is_app_location_set |= arg == "--zed";
}
if !is_app_location_set {
args.push("--zed".into());
args.push(flatpak_dir.join("libexec").join("zed-editor").into());
}
let error = exec::execvp("/usr/bin/flatpak-spawn", args);
eprintln!("failed restart cli on host: {:?}", error);
process::exit(1);
}
}
pub fn set_bin_if_no_escape(mut args: super::Args) -> super::Args {
if env::var(NO_ESCAPE_ENV_NAME).is_ok()
&& env::var("FLATPAK_ID").map_or(false, |id| id.starts_with("dev.zed.Zed"))
{
if args.zed.is_none() {
args.zed = Some("/app/libexec/zed-editor".into());
env::set_var("ZED_UPDATE_EXPLANATION", "Please use flatpak to update zed");
}
}
args
}
fn get_flatpak_dir() -> Option<PathBuf> {
if env::var(NO_ESCAPE_ENV_NAME).is_ok() {
return None;
}
if let Ok(flatpak_id) = env::var("FLATPAK_ID") {
if !flatpak_id.starts_with("dev.zed.Zed") {
return None;
}
let install_dir = Command::new("/usr/bin/flatpak-spawn")
.arg("--host")
.arg("flatpak")
.arg("info")
.arg("--show-location")
.arg(flatpak_id)
.output()
.unwrap();
let install_dir = PathBuf::from(String::from_utf8(install_dir.stdout).unwrap().trim());
Some(install_dir.join("files"))
} else {
None
}
}
fn get_xdg_env_args() -> Vec<OsString> {
let xdg_keys = [
"XDG_DATA_HOME",
"XDG_CONFIG_HOME",
"XDG_CACHE_HOME",
"XDG_STATE_HOME",
];
env::vars()
.filter(|(key, _)| xdg_keys.contains(&key.as_str()))
.map(|(key, val)| format!("--env=FLATPAK_{}={}", key, val).into())
.collect()
}
}
// todo("windows")
#[cfg(target_os = "windows")]
mod windows {
use crate::{Detect, InstalledApp};
use std::io;
use std::path::Path;
use std::process::ExitStatus;
struct App;
impl InstalledApp for App {
fn zed_version_string(&self) -> String {
unimplemented!()
}
fn launch(&self, _ipc_url: String) -> anyhow::Result<()> {
unimplemented!()
}
fn run_foreground(&self, _ipc_url: String) -> io::Result<ExitStatus> {
unimplemented!()
}
}
impl Detect {
pub fn detect(_path: Option<&Path>) -> anyhow::Result<impl InstalledApp> {
Ok(App)
}
}
}
#[cfg(target_os = "macos")]
mod mac_os {
use anyhow::{anyhow, Context, Result};
use core_foundation::{
array::{CFArray, CFIndex},
string::kCFStringEncodingUTF8,
url::{CFURLCreateWithBytes, CFURL},
};
use core_services::{kLSLaunchDefaults, LSLaunchURLSpec, LSOpenFromURLSpec, TCFType};
use serde::Deserialize;
use std::{
ffi::OsStr,
fs, io,
path::{Path, PathBuf},
process::{Command, ExitStatus},
ptr,
};
use cli::FORCE_CLI_MODE_ENV_VAR_NAME;
use crate::{Detect, InstalledApp};
#[derive(Debug, Deserialize)]
struct InfoPlist {
#[serde(rename = "CFBundleShortVersionString")]
bundle_short_version_string: String,
}
enum Bundle {
App {
app_bundle: PathBuf,
plist: InfoPlist,
},
LocalPath {
executable: PathBuf,
plist: InfoPlist,
},
}
fn locate_bundle() -> Result<PathBuf> {
let cli_path = std::env::current_exe()?.canonicalize()?;
let mut app_path = cli_path.clone();
while app_path.extension() != Some(OsStr::new("app")) {
if !app_path.pop() {
return Err(anyhow!("cannot find app bundle containing {:?}", cli_path));
}
}
Ok(app_path)
}
impl Detect {
pub fn detect(path: Option<&Path>) -> anyhow::Result<impl InstalledApp> {
let bundle_path = if let Some(bundle_path) = path {
bundle_path
.canonicalize()
.with_context(|| format!("Args bundle path {bundle_path:?} canonicalization"))?
} else {
locate_bundle().context("bundle autodiscovery")?
};
match bundle_path.extension().and_then(|ext| ext.to_str()) {
Some("app") => {
let plist_path = bundle_path.join("Contents/Info.plist");
let plist =
plist::from_file::<_, InfoPlist>(&plist_path).with_context(|| {
format!("Reading *.app bundle plist file at {plist_path:?}")
})?;
Ok(Bundle::App {
app_bundle: bundle_path,
plist,
})
}
_ => {
println!("Bundle path {bundle_path:?} has no *.app extension, attempting to locate a dev build");
let plist_path = bundle_path
.parent()
.with_context(|| format!("Bundle path {bundle_path:?} has no parent"))?
.join("WebRTC.framework/Resources/Info.plist");
let plist =
plist::from_file::<_, InfoPlist>(&plist_path).with_context(|| {
format!("Reading dev bundle plist file at {plist_path:?}")
})?;
Ok(Bundle::LocalPath {
executable: bundle_path,
plist,
})
}
}
}
}
impl InstalledApp for Bundle {
fn zed_version_string(&self) -> String {
let is_dev = matches!(self, Self::LocalPath { .. });
format!(
"Zed {}{} {}",
self.plist().bundle_short_version_string,
if is_dev { " (dev)" } else { "" },
self.path().display(),
)
}
fn launch(&self, url: String) -> anyhow::Result<()> {
match self {
Self::App { app_bundle, .. } => {
let app_path = app_bundle;
let status = unsafe {
let app_url = CFURL::from_path(app_path, true)
.with_context(|| format!("invalid app path {app_path:?}"))?;
let url_to_open = CFURL::wrap_under_create_rule(CFURLCreateWithBytes(
ptr::null(),
url.as_ptr(),
url.len() as CFIndex,
kCFStringEncodingUTF8,
ptr::null(),
));
// equivalent to: open zed-cli:... -a /Applications/Zed\ Preview.app
let urls_to_open =
CFArray::from_copyable(&[url_to_open.as_concrete_TypeRef()]);
LSOpenFromURLSpec(
&LSLaunchURLSpec {
appURL: app_url.as_concrete_TypeRef(),
itemURLs: urls_to_open.as_concrete_TypeRef(),
passThruParams: ptr::null(),
launchFlags: kLSLaunchDefaults,
asyncRefCon: ptr::null_mut(),
},
ptr::null_mut(),
)
};
anyhow::ensure!(
status == 0,
"cannot start app bundle {}",
self.zed_version_string()
);
}
Self::LocalPath { executable, .. } => {
let executable_parent = executable
.parent()
.with_context(|| format!("Executable {executable:?} path has no parent"))?;
let subprocess_stdout_file = fs::File::create(
executable_parent.join("zed_dev.log"),
)
.with_context(|| format!("Log file creation in {executable_parent:?}"))?;
let subprocess_stdin_file =
subprocess_stdout_file.try_clone().with_context(|| {
format!("Cloning descriptor for file {subprocess_stdout_file:?}")
})?;
let mut command = std::process::Command::new(executable);
let command = command
.env(FORCE_CLI_MODE_ENV_VAR_NAME, "")
.stderr(subprocess_stdout_file)
.stdout(subprocess_stdin_file)
.arg(url);
command
.spawn()
.with_context(|| format!("Spawning {command:?}"))?;
}
}
Ok(())
}
fn run_foreground(&self, ipc_url: String) -> io::Result<ExitStatus> {
let path = match self {
Bundle::App { app_bundle, .. } => app_bundle.join("Contents/MacOS/zed"),
Bundle::LocalPath { executable, .. } => executable.clone(),
};
std::process::Command::new(path).arg(ipc_url).status()
}
}
impl Bundle {
fn plist(&self) -> &InfoPlist {
match self {
Self::App { plist, .. } => plist,
Self::LocalPath { plist, .. } => plist,
}
}
fn path(&self) -> &Path {
match self {
Self::App { app_bundle, .. } => app_bundle,
Self::LocalPath { executable, .. } => executable,
}
}
}
pub(super) fn spawn_channel_cli(
channel: release_channel::ReleaseChannel,
leftover_args: Vec<String>,
) -> Result<()> {
use anyhow::bail;
let app_id_prompt = format!("id of app \"{}\"", channel.display_name());
let app_id_output = Command::new("osascript")
.arg("-e")
.arg(&app_id_prompt)
.output()?;
if !app_id_output.status.success() {
bail!("Could not determine app id for {}", channel.display_name());
}
let app_name = String::from_utf8(app_id_output.stdout)?.trim().to_owned();
let app_path_prompt = format!("kMDItemCFBundleIdentifier == '{app_name}'");
let app_path_output = Command::new("mdfind").arg(app_path_prompt).output()?;
if !app_path_output.status.success() {
bail!(
"Could not determine app path for {}",
channel.display_name()
);
}
let app_path = String::from_utf8(app_path_output.stdout)?.trim().to_owned();
let cli_path = format!("{app_path}/Contents/MacOS/cli");
Command::new(cli_path).args(leftover_args).spawn()?;
Ok(())
}
}

View File

@@ -0,0 +1,12 @@
Below is an instruction that describes a task, paired with an input that provides further context. Write a response that appropriately completes the request.
### Instruction:
You are a code completion assistant and your task is to analyze user edits and then rewrite an excerpt that the user provides, suggesting the appropriate edits within the excerpt, taking into account the cursor location.
### Events:
<events>
### Input:
<excerpt>
### Response:

View File

@@ -0,0 +1,20 @@
Your task is to help me grade code test output.
Here is the test output:
```
<actual>
```
Now, help me score the test output using the following criteria:
<assertions>
Based on these criteria, give the test output a score between 0 and 5.
- 0 means: test output doesn't meet any criteria
- 5 means: test output meets all criteria
- If the test output does not contain any of the text I said it "must" include, grade it 0.
- First, perform a short, succinct analysis of how the test output meets the criteria.
- On the last line of your response, write the grade as a single number. Nothing else.
- **Always** end with a grade. The last line **must** be a number between 0 and 5.

1528
crates/zeta/src/zeta.rs Normal file

File diff suppressed because it is too large Load Diff