Compare commits

..

10 Commits

Author SHA1 Message Date
Gregory Schier
db02dbcaa4 Hotfix for window focusing 2024-08-27 16:56:04 -07:00
Gregory Schier
badcbc7aef Special case Response()->response() 2024-08-26 15:26:43 -07:00
Gregory Schier
4b91601b98 Clean up code 2024-08-26 15:10:29 -07:00
Gregory Schier
93e0202b86 Default template fn args 2024-08-26 13:10:22 -07:00
Gregory Schier
e75d6abe33 Option to disable telemetry 2024-08-26 12:06:56 -07:00
Gregory Schier
24a4e3494e Node syntaxTree to parse template tags 2024-08-26 11:30:10 -07:00
Gregory Schier
124fb35dcd Force codemirror to parse more to be able to show code folding 2024-08-23 14:09:28 -07:00
Gregory Schier
1aa2839c51 Publish plugin-runtime-types 2024-08-23 13:37:47 -07:00
Gregory Schier
8d3260f394 Fix recursive plugin call locking 2024-08-23 13:20:48 -07:00
Gregory Schier
7e194b9148 Surface plugin error on import 2024-08-23 11:53:40 -07:00
38 changed files with 254 additions and 184 deletions

8
package-lock.json generated
View File

@@ -27,7 +27,7 @@
"@tauri-apps/plugin-log": "^2.0.0-rc.0",
"@tauri-apps/plugin-os": "^2.0.0-rc.0",
"@tauri-apps/plugin-shell": "^2.0.0-rc.0",
"@yaakapp/api": "^0.1.11",
"@yaakapp/api": "^0.1.13",
"buffer": "^6.0.3",
"classnames": "^2.3.2",
"cm6-graphql": "^0.0.9",
@@ -2990,9 +2990,9 @@
"integrity": "sha512-N8tkAACJx2ww8vFMneJmaAgmjAG1tnVBZJRLRcx061tmsLRZHSEZSLuGWnwPtunsSLvSqXQ2wfp7Mgqg1I+2dQ=="
},
"node_modules/@yaakapp/api": {
"version": "0.1.11",
"resolved": "https://registry.npmjs.org/@yaakapp/api/-/api-0.1.11.tgz",
"integrity": "sha512-dRZAXrQpftWygy9nJXiIYPzLA9om6reO/RiEacMe9RKqMjNG6FRF8cGmj7BcdyreizJOkH/DmcOpxn09kDD0XA==",
"version": "0.1.13",
"resolved": "https://registry.npmjs.org/@yaakapp/api/-/api-0.1.13.tgz",
"integrity": "sha512-FSYPHZV0mP967w63VXi9zYP81hPo3vjSW3/UElJLuF/8ig6WmG4p1q2oYos4Ik267Z3qSQAGN5dPMfuk3DAnBA==",
"dependencies": {
"@types/node": "^22.0.0"
}

View File

@@ -42,7 +42,7 @@
"@tauri-apps/plugin-log": "^2.0.0-rc.0",
"@tauri-apps/plugin-os": "^2.0.0-rc.0",
"@tauri-apps/plugin-shell": "^2.0.0-rc.0",
"@yaakapp/api": "^0.1.11",
"@yaakapp/api": "^0.1.13",
"buffer": "^6.0.3",
"classnames": "^2.3.2",
"cm6-graphql": "^0.0.9",

View File

@@ -1,6 +1,6 @@
{
"name": "@yaakapp/api",
"version": "0.1.11",
"version": "0.1.13",
"main": "lib/index.js",
"typings": "./lib/index.d.ts",
"files": [

View File

@@ -1,4 +1,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { CallTemplateFunctionPurpose } from "./CallTemplateFunctionPurpose";
import type { RenderPurpose } from "./RenderPurpose";
export type CallTemplateFunctionArgs = { purpose: CallTemplateFunctionPurpose, values: { [key: string]: string }, };
export type CallTemplateFunctionArgs = { purpose: RenderPurpose, values: { [key: string]: string }, };

View File

@@ -1,4 +1,5 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { HttpRequest } from "./HttpRequest";
import type { RenderPurpose } from "./RenderPurpose";
export type RenderHttpRequestRequest = { httpRequest: HttpRequest, };
export type RenderHttpRequestRequest = { httpRequest: HttpRequest, purpose: RenderPurpose, };

View File

@@ -1,3 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type CallTemplateFunctionPurpose = "send" | "preview";
export type RenderPurpose = "send" | "preview";

View File

@@ -1,3 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type Settings = { id: string, model: "settings", createdAt: string, updatedAt: string, theme: string, appearance: string, themeDark: string, themeLight: string, updateChannel: string, interfaceFontSize: number, interfaceScale: number, editorFontSize: number, editorSoftWrap: boolean, openWorkspaceNewWindow: boolean | null, };
export type Settings = { id: string, model: "settings", createdAt: string, updatedAt: string, theme: string, appearance: string, themeDark: string, themeLight: string, updateChannel: string, interfaceFontSize: number, interfaceScale: number, editorFontSize: number, editorSoftWrap: boolean, telemetry: boolean, openWorkspaceNewWindow: boolean | null, };

View File

@@ -5,7 +5,6 @@ export type * from './themes';
export * from './gen/BootRequest';
export * from './gen/BootResponse';
export * from './gen/CallHttpRequestActionArgs';
export * from './gen/CallTemplateFunctionPurpose';
export * from './gen/CallHttpRequestActionRequest';
export * from './gen/CallTemplateFunctionRequest';
export * from './gen/CallTemplateFunctionResponse';
@@ -48,6 +47,7 @@ export * from './gen/KeyValue';
export * from './gen/Model';
export * from './gen/RenderHttpRequestRequest';
export * from './gen/RenderHttpRequestResponse';
export * from './gen/RenderPurpose';
export * from './gen/SendHttpRequestRequest';
export * from './gen/SendHttpRequestResponse';
export * from './gen/SendHttpRequestResponse';

View File

@@ -64,6 +64,9 @@ new Promise<void>(async (resolve, reject) => {
}
function sendEvent(event: InternalEvent) {
if (event.payload.type !== 'empty_response') {
console.log('Sending event to app', event.id, event.payload.type);
}
parentPort!.postMessage(event);
}
@@ -77,8 +80,8 @@ new Promise<void>(async (resolve, reject) => {
const promise = new Promise<InternalEventPayload>(async (resolve) => {
const cb = (event: InternalEvent) => {
if (event.replyId === eventToSend.id) {
resolve(event.payload); // Not type-safe but oh well
parentPort!.off('message', cb); // Unlisten, now that we're done
resolve(event.payload); // Not type-safe but oh well
}
};
parentPort!.on('message', cb);
@@ -110,18 +113,18 @@ new Promise<void>(async (resolve, reject) => {
},
},
httpRequest: {
async getById({ id }) {
const payload = { type: 'get_http_request_by_id_request', id } as const;
async getById(args) {
const payload = { type: 'get_http_request_by_id_request', ...args } as const;
const { httpRequest } = await sendAndWaitForReply<GetHttpRequestByIdResponse>(payload);
return httpRequest;
},
async send({ httpRequest }) {
const payload = { type: 'send_http_request_request', httpRequest } as const;
async send(args) {
const payload = { type: 'send_http_request_request', ...args } as const;
const { httpResponse } = await sendAndWaitForReply<SendHttpRequestResponse>(payload);
return httpResponse;
},
async render({ httpRequest }) {
const payload = { type: 'render_http_request_request', httpRequest } as const;
async render(args) {
const payload = { type: 'render_http_request_request', ...args } as const;
const result = await sendAndWaitForReply<RenderHttpRequestResponse>(payload);
return result.httpRequest;
},
@@ -130,8 +133,6 @@ new Promise<void>(async (resolve, reject) => {
// Message comes into the plugin to be processed
parentPort!.on('message', async ({ payload, id: replyId }: InternalEvent) => {
console.log(`Received ${payload.type}`);
try {
if (payload.type === 'boot_request') {
const payload: InternalEventPayload = {

View File

@@ -0,0 +1 @@
ALTER TABLE settings ADD COLUMN telemetry BOOLEAN DEFAULT TRUE;

View File

@@ -5,9 +5,7 @@ use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
use tauri::{Manager, Runtime, WebviewWindow};
use yaak_models::queries::{
generate_id, get_key_value_int, get_key_value_string, set_key_value_int, set_key_value_string,
};
use yaak_models::queries::{generate_id, get_key_value_int, get_key_value_string, get_or_create_settings, set_key_value_int, set_key_value_string};
use crate::is_dev;
@@ -157,6 +155,7 @@ pub async fn track_event<R: Runtime>(
action: AnalyticsAction,
attributes: Option<Value>,
) {
let id = get_id(w).await;
let event = format!("{}.{}", resource, action);
let attributes_json = attributes.unwrap_or("{}".to_string().into()).to_string();
@@ -186,9 +185,15 @@ pub async fn track_event<R: Runtime>(
.get(format!("{base_url}/t/e"))
.query(&params);
let settings = get_or_create_settings(w).await;
if !settings.telemetry {
info!("Track event (disabled): {}", event);
return
}
// Disable analytics actual sending in dev
if is_dev() {
debug!("track: {} {}", event, attributes_json);
debug!("Track event: {} {}", event, attributes_json);
return;
}

View File

@@ -1942,24 +1942,21 @@ fn monitor_plugin_events<R: Runtime>(app_handle: &AppHandle<R>) {
let plugin_manager: State<'_, PluginManager> = app_handle.state();
let (_rx_id, mut rx) = plugin_manager.subscribe().await;
let app_handle = app_handle.clone();
while let Some(event) = rx.recv().await {
let payload = match handle_plugin_event(&app_handle, &event).await {
Some(e) => e,
None => continue,
};
if let Err(e) = plugin_manager.reply(&event, &payload).await {
warn!("Failed to reply to plugin manager: {}", e)
}
let app_handle = app_handle.clone();
// We might have recursive back-and-forth calls between app and plugin, so we don't
// want to block here
tauri::async_runtime::spawn(async move {
handle_plugin_event(&app_handle, &event).await;
});
}
});
}
async fn handle_plugin_event<R: Runtime>(
app_handle: &AppHandle<R>,
event: &InternalEvent,
) -> Option<InternalEventPayload> {
let event = match event.clone().payload {
async fn handle_plugin_event<R: Runtime>(app_handle: &AppHandle<R>, event: &InternalEvent) {
info!("Got event to app {}", event.id);
let response_event: Option<InternalEventPayload> = match event.clone().payload {
InternalEventPayload::CopyTextRequest(req) => {
app_handle
.clipboard()
@@ -1992,7 +1989,7 @@ async fn handle_plugin_event<R: Runtime>(
))
}
InternalEventPayload::RenderHttpRequestRequest(req) => {
let w = get_focused_window_no_lock(app_handle)?;
let w = get_focused_window_no_lock(app_handle).expect("No focused window");
let workspace = get_workspace(app_handle, req.http_request.workspace_id.as_str())
.await
.expect("Failed to get workspace for request");
@@ -2007,13 +2004,8 @@ async fn handle_plugin_event<R: Runtime>(
Some(id) => get_environment(&w, id.as_str()).await.ok(),
};
let cb = &*app_handle.state::<PluginTemplateCallback>();
let rendered_http_request = render_http_request(
&req.http_request,
&workspace,
environment.as_ref(),
cb,
)
.await;
let rendered_http_request =
render_http_request(&req.http_request, &workspace, environment.as_ref(), cb).await;
Some(InternalEventPayload::RenderHttpRequestResponse(
RenderHttpRequestResponse {
http_request: rendered_http_request,
@@ -2021,7 +2013,7 @@ async fn handle_plugin_event<R: Runtime>(
))
}
InternalEventPayload::SendHttpRequestRequest(req) => {
let w = get_focused_window_no_lock(app_handle)?;
let w = get_focused_window_no_lock(app_handle).expect("No focused window");
let url = w.url().unwrap();
let mut query_pairs = url.query_pairs();
@@ -2057,7 +2049,7 @@ async fn handle_plugin_event<R: Runtime>(
let http_response = match result {
Ok(r) => r,
Err(_e) => return None,
Err(_e) => return,
};
Some(InternalEventPayload::SendHttpRequestResponse(
@@ -2067,18 +2059,31 @@ async fn handle_plugin_event<R: Runtime>(
_ => None,
};
event
if let Some(e) = response_event {
let plugin_manager: State<'_, PluginManager> = app_handle.state();
if let Err(e) = plugin_manager.reply(&event, &e).await {
warn!("Failed to reply to plugin manager: {:?}", e)
}
}
}
// app_handle.get_focused_window locks, so this one is a non-locking version, safe for use in async context
fn get_focused_window_no_lock<R: Runtime>(app_handle: &AppHandle<R>) -> Option<WebviewWindow<R>> {
// TODO: Getting the focused window doesn't seem to work on Windows, so
// we'll need to pass the window label into plugin events instead.
if app_handle.webview_windows().len() == 1 {
debug!("Returning only webview window");
let w = app_handle
.webview_windows()
.iter()
.next()
.map(|w| w.1.clone());
return w;
}
app_handle
.windows()
.iter()
.find(|w| w.1.is_focused().unwrap_or(false))
.map(|w| w.1.clone())?
.webview_windows()
.iter()
.next()
.map(|(_, w)| w.to_owned())
.find(|w| w.1.is_focused().unwrap_or(false))
.map(|w| w.1.clone())
}

View File

@@ -1,32 +1,67 @@
use std::collections::HashMap;
use tauri::{AppHandle, Manager};
use yaak_plugin_runtime::events::CallTemplateFunctionPurpose;
use yaak_plugin_runtime::events::{RenderPurpose, TemplateFunctionArg};
use yaak_plugin_runtime::manager::PluginManager;
use yaak_templates::TemplateCallback;
#[derive(Clone)]
pub struct PluginTemplateCallback {
app_handle: AppHandle,
purpose: CallTemplateFunctionPurpose,
purpose: RenderPurpose,
}
impl PluginTemplateCallback {
pub fn new(app_handle: AppHandle) -> PluginTemplateCallback {
PluginTemplateCallback { app_handle, purpose: CallTemplateFunctionPurpose::Preview }
PluginTemplateCallback {
app_handle,
purpose: RenderPurpose::Preview,
}
}
pub fn for_send(&self) -> PluginTemplateCallback {
let mut v = self.clone();
v.purpose = CallTemplateFunctionPurpose::Send;
v.purpose = RenderPurpose::Send;
v
}
}
impl TemplateCallback for PluginTemplateCallback {
async fn run(&self, fn_name: &str, args: HashMap<String, String>) -> Result<String, String> {
// The beta named the function `Response` but was changed in stable.
// Keep this here for a while because there's no easy way to migrate
let fn_name = if fn_name == "Response" {
"response"
} else {
fn_name
};
let plugin_manager = self.app_handle.state::<PluginManager>();
let function = plugin_manager
.get_template_functions()
.await
.map_err(|e| e.to_string())?
.iter()
.flat_map(|f| f.functions.clone())
.find(|f| f.name == fn_name)
.ok_or("")?;
let mut args_with_defaults = args.clone();
// Fill in default values for all args
for a_def in function.args {
let base = match a_def {
TemplateFunctionArg::Text(a) => a.base,
TemplateFunctionArg::Select(a) => a.base,
TemplateFunctionArg::Checkbox(a) => a.base,
TemplateFunctionArg::HttpRequest(a) => a.base,
};
if let None = args_with_defaults.get(base.name.as_str()) {
args_with_defaults.insert(base.name, base.default_value.unwrap_or_default());
}
}
let resp = plugin_manager
.call_template_function(fn_name, args, self.purpose.clone())
.call_template_function(fn_name, args_with_defaults, self.purpose.clone())
.await
.map_err(|e| e.to_string())?;
Ok(resp.unwrap_or_default())

View File

@@ -23,6 +23,7 @@ pub struct Settings {
pub interface_scale: i32,
pub editor_font_size: i32,
pub editor_soft_wrap: bool,
pub telemetry: bool,
pub open_workspace_new_window: Option<bool>,
}
@@ -43,6 +44,7 @@ pub enum SettingsIden {
InterfaceScale,
EditorFontSize,
EditorSoftWrap,
Telemetry,
OpenWorkspaceNewWindow,
}
@@ -64,6 +66,7 @@ impl<'s> TryFrom<&Row<'s>> for Settings {
interface_scale: r.get("interface_scale")?,
editor_font_size: r.get("editor_font_size")?,
editor_soft_wrap: r.get("editor_soft_wrap")?,
telemetry: r.get("telemetry")?,
open_workspace_new_window: r.get("open_workspace_new_window")?,
})
}

View File

@@ -799,6 +799,10 @@ pub async fn update_settings<R: Runtime>(
SettingsIden::EditorSoftWrap,
settings.editor_soft_wrap.into(),
),
(
SettingsIden::Telemetry,
settings.telemetry.into(),
),
(
SettingsIden::OpenWorkspaceNewWindow,
settings.open_workspace_new_window.into(),

View File

@@ -17,18 +17,10 @@ pub enum Error {
GrpcSendErr(#[from] SendError<tonic::Result<EventStreamEvent>>),
#[error("JSON error")]
JsonErr(#[from] serde_json::Error),
#[error("Plugin not found error")]
#[error("Plugin not found: {0}")]
PluginNotFoundErr(String),
#[error("unknown error")]
MissingCallbackIdErr(String),
#[error("Missing callback ID error")]
MissingCallbackErr(String),
#[error("No plugins found")]
NoPluginsErr(String),
#[error("Plugin error")]
#[error("Plugin error: {0}")]
PluginErr(String),
#[error("Unknown error")]
UnknownErr(String),
}
impl Into<String> for Error {

View File

@@ -153,6 +153,7 @@ pub struct CopyTextRequest {
#[ts(export)]
pub struct RenderHttpRequestRequest {
pub http_request: HttpRequest,
pub purpose: RenderPurpose,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
@@ -289,21 +290,21 @@ pub struct CallTemplateFunctionResponse {
#[serde(default, rename_all = "camelCase")]
#[ts(export)]
pub struct CallTemplateFunctionArgs {
pub purpose: CallTemplateFunctionPurpose,
pub purpose: RenderPurpose,
pub values: HashMap<String, String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[serde(rename_all = "snake_case")]
#[ts(export)]
pub enum CallTemplateFunctionPurpose {
pub enum RenderPurpose {
Send,
Preview,
}
impl Default for CallTemplateFunctionPurpose {
impl Default for RenderPurpose {
fn default() -> Self {
CallTemplateFunctionPurpose::Preview
RenderPurpose::Preview
}
}

View File

@@ -1,6 +1,6 @@
use crate::error::Result;
use crate::events::{
CallHttpRequestActionRequest, CallTemplateFunctionArgs, CallTemplateFunctionPurpose,
CallHttpRequestActionRequest, CallTemplateFunctionArgs, RenderPurpose,
CallTemplateFunctionRequest, CallTemplateFunctionResponse, FilterRequest, FilterResponse,
GetHttpRequestActionsRequest, GetHttpRequestActionsResponse, GetTemplateFunctionsResponse,
ImportRequest, ImportResponse, InternalEvent, InternalEventPayload,
@@ -115,7 +115,7 @@ impl PluginManager {
&self,
fn_name: &str,
args: HashMap<String, String>,
purpose: CallTemplateFunctionPurpose,
purpose: RenderPurpose,
) -> Result<Option<String>> {
let req = CallTemplateFunctionRequest {
name: fn_name.to_string(),
@@ -155,7 +155,7 @@ impl PluginManager {
});
match result {
None => Err(PluginErr("No import responses found".to_string())),
None => Err(PluginErr("No importers found for file contents".to_string())),
Some((resp, ref_id)) => {
let plugin = self.server.plugin_by_ref_id(ref_id.as_str()).await?;
let plugin_name = plugin.name().await;

View File

@@ -9,7 +9,7 @@ use tonic::codegen::tokio_stream::wrappers::ReceiverStream;
use tonic::codegen::tokio_stream::{Stream, StreamExt};
use tonic::{Request, Response, Status, Streaming};
use crate::error::Error::{NoPluginsErr, PluginNotFoundErr};
use crate::error::Error::PluginNotFoundErr;
use crate::error::Result;
use crate::events::{BootRequest, BootResponse, InternalEvent, InternalEventPayload};
use crate::server::plugin_runtime::plugin_runtime_server::PluginRuntime;
@@ -53,7 +53,11 @@ impl PluginHandle {
}
pub async fn send(&self, event: &InternalEvent) -> Result<()> {
info!("Sending event {} {:?}", event.id, self.name().await);
info!(
"Sending event to plugin {} {:?}",
event.id,
self.name().await
);
self.to_plugin_tx
.lock()
.await
@@ -90,9 +94,9 @@ impl PluginRuntimeGrpcServer {
pub async fn subscribe(&self) -> (String, Receiver<InternalEvent>) {
let (tx, rx) = mpsc::channel(128);
let id = generate_id();
self.subscribers.lock().await.insert(id.clone(), tx);
(id, rx)
let rx_id = generate_id();
self.subscribers.lock().await.insert(rx_id.clone(), tx);
(rx_id, rx)
}
pub async fn unsubscribe(&self, rx_id: &str) {
@@ -187,14 +191,11 @@ impl PluginRuntimeGrpcServer {
pub async fn plugin_by_ref_id(&self, ref_id: &str) -> Result<PluginHandle> {
let plugins = self.plugin_ref_to_plugin.lock().await;
if plugins.is_empty() {
return Err(NoPluginsErr("Send failed because no plugins exist".into()));
return Err(PluginNotFoundErr(ref_id.into()));
}
match plugins.get(ref_id) {
None => {
let msg = format!("Failed to find plugin for id {ref_id}");
Err(PluginNotFoundErr(msg))
}
None => Err(PluginNotFoundErr(ref_id.into())),
Some(p) => Ok(p.to_owned()),
}
}
@@ -202,7 +203,7 @@ impl PluginRuntimeGrpcServer {
pub async fn plugin_by_name(&self, plugin_name: &str) -> Result<PluginHandle> {
let plugins = self.plugin_ref_to_plugin.lock().await;
if plugins.is_empty() {
return Err(NoPluginsErr("Send failed because no plugins exist".into()));
return Err(PluginNotFoundErr(plugin_name.into()));
}
for p in plugins.values() {
@@ -211,17 +212,20 @@ impl PluginRuntimeGrpcServer {
}
}
let msg = format!("Failed to find plugin for {plugin_name}");
Err(PluginNotFoundErr(msg))
Err(PluginNotFoundErr(plugin_name.into()))
}
pub async fn send(&self, payload: &InternalEventPayload, plugin_ref_id: &str, reply_id: Option<String>)-> Result<()> {
pub async fn send(
&self,
payload: &InternalEventPayload,
plugin_ref_id: &str,
reply_id: Option<String>,
) -> Result<()> {
let plugin = self.plugin_by_ref_id(plugin_ref_id).await?;
let event = plugin.build_event_to_send(payload, reply_id);
plugin.send(&event).await
}
pub async fn send_to_plugin(
&self,
plugin_name: &str,
@@ -229,7 +233,7 @@ impl PluginRuntimeGrpcServer {
) -> Result<InternalEvent> {
let plugins = self.plugin_ref_to_plugin.lock().await;
if plugins.is_empty() {
return Err(NoPluginsErr("Send failed because no plugins exist".into()));
return Err(PluginNotFoundErr(plugin_name.into()));
}
let mut plugin = None;
@@ -246,10 +250,7 @@ impl PluginRuntimeGrpcServer {
plugin.send(&event).await?;
Ok(event)
}
None => {
let msg = format!("Failed to find plugin for {plugin_name}");
Err(PluginNotFoundErr(msg))
}
None => Err(PluginNotFoundErr(plugin_name.into())),
}
}
@@ -397,7 +398,7 @@ impl PluginRuntime for PluginRuntimeGrpcServer {
for tx in subscribers.values() {
// Emit event to the channel for server to handle
if let Err(e) = tx.try_send(event.clone()) {
println!("Failed to send to server channel. Receiver probably isn't listening: {:?}", e);
println!("Failed to send to server channel (n={}). Receiver probably isn't listening: {:?}", subscribers.len(), e);
}
}

View File

@@ -1,6 +1,6 @@
import type { Cookie } from '@yaakapp/api';
import { useCookieJars } from '../hooks/useCookieJars';
import { useUpdateCookieJar } from '../hooks/useUpdateCookieJar';
import type { Cookie } from '../lib/models/Cookie';
import { cookieDomain } from '../lib/models';
import { Banner } from './core/Banner';
import { IconButton } from './core/IconButton';

View File

@@ -77,6 +77,12 @@ export function SettingsGeneral() {
{ label: 'New Window', value: 'new' },
]}
/>
<Checkbox
checked={settings.telemetry}
title="Send Usage Statistics"
onChange={(telemetry) => updateSettings.mutate({ telemetry })}
/>
<Separator className="my-4" />
<Heading size={2}>

View File

@@ -1,4 +1,5 @@
import { defaultKeymap } from '@codemirror/commands';
import { forceParsing } from '@codemirror/language';
import { Compartment, EditorState, type Extension } from '@codemirror/state';
import { keymap, placeholder as placeholderExt, tooltips } from '@codemirror/view';
import type { EnvironmentVariable, TemplateFunction } from '@yaakapp/api';
@@ -297,6 +298,12 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
});
view = new EditorView({ state, parent: container });
// For large documents, the parser may parse the max number of lines and fail to add
// things like fold markers because of it.
// This forces it to parse more but keeps the timeout to the default of 100ms.
forceParsing(view, 9e6, 100);
cm.current = { view, languageCompartment };
if (autoFocus) {
view.focus();

View File

@@ -68,7 +68,7 @@ const syntaxExtensions: Record<string, LanguageSupport> = {
'application/graphql': graphqlLanguageSupport(),
'application/json': json(),
'application/javascript': javascript(),
'text/html': xml(), // HTML as xml because HTML is oddly slow
'text/html': xml(), // HTML as XML because HTML is oddly slow
'application/xml': xml(),
'text/xml': xml(),
url: url(),

View File

@@ -7,7 +7,7 @@ import { genericCompletion } from '../genericCompletion';
import { textLanguageName } from '../text/extension';
import type { TwigCompletionOption } from './completion';
import { twigCompletion } from './completion';
import { templateTags } from './templateTags';
import { templateTagsPlugin } from './templateTags';
import { parser as twigParser } from './twig';
export function twig({
@@ -62,7 +62,7 @@ export function twig({
return [
language,
base.support,
templateTags(options, onClickMissingVariable),
templateTagsPlugin(options, onClickMissingVariable),
language.data.of({ autocomplete: completions }),
base.language.data.of({ autocomplete: completions }),
language.data.of({ autocomplete: genericCompletion(autocomplete) }),

View File

@@ -1,6 +1,8 @@
import { syntaxTree } from '@codemirror/language';
import type { Range } from '@codemirror/state';
import type { DecorationSet, ViewUpdate } from '@codemirror/view';
import { Decoration, EditorView, ViewPlugin, WidgetType } from '@codemirror/view';
import { BetterMatchDecorator } from '../BetterMatchDecorator';
import { Decoration, ViewPlugin, WidgetType } from '@codemirror/view';
import { EditorView } from 'codemirror';
import type { TwigCompletionOption } from './completion';
class TemplateTagWidget extends WidgetType {
@@ -22,7 +24,8 @@ class TemplateTagWidget extends WidgetType {
this.option.name === other.option.name &&
this.option.type === other.option.type &&
this.option.value === other.option.value &&
this.rawTag === other.rawTag
this.rawTag === other.rawTag &&
this.startPos === other.startPos
);
}
@@ -55,69 +58,91 @@ class TemplateTagWidget extends WidgetType {
}
}
export function templateTags(
function templateTags(
view: EditorView,
options: TwigCompletionOption[],
onClickMissingVariable: (name: string, rawTag: string, startPos: number) => void,
) {
const templateTagMatcher = new BetterMatchDecorator({
regexp: /\$\{\[\s*(.+)(?!]})\s*]}/g,
decoration(match, view, matchStartPos) {
const matchEndPos = matchStartPos + match[0].length - 1;
): DecorationSet {
const widgets: Range<Decoration>[] = [];
for (const { from, to } of view.visibleRanges) {
syntaxTree(view.state).iterate({
from,
to,
enter(node) {
if (node.name == 'Tag') {
// Don't decorate if the cursor is inside the match
for (const r of view.state.selection.ranges) {
if (r.from > node.from && r.to < node.to) {
return;
}
}
// Don't decorate if the cursor is inside the match
for (const r of view.state.selection.ranges) {
if (r.from > matchStartPos && r.to <= matchEndPos) {
return Decoration.replace({});
const rawTag = view.state.doc.sliceString(node.from, node.to);
// TODO: Search `node.tree` instead of using Regex here
const inner = rawTag.replace(/^\$\{\[\s*/, '').replace(/\s*]}$/, '');
let name = inner.match(/(\w+)[(]/)?.[1] ?? inner;
// The beta named the function `Response` but was changed in stable.
// Keep this here for a while because there's no easy way to migrate
if (name === 'Response') {
name = 'response';
}
let option = options.find((v) => v.name === name);
if (option == null) {
option = {
invalid: true,
type: 'variable',
name: inner,
value: null,
label: inner,
onClick: () => onClickMissingVariable(name, rawTag, node.from),
};
}
const widget = new TemplateTagWidget(option, rawTag, node.from);
const deco = Decoration.replace({ widget, inclusive: true });
widgets.push(deco.range(node.from, node.to));
}
}
const innerTagMatch = match[1];
if (innerTagMatch == null) {
// Should never happen, but make TS happy
console.warn('Group match was empty', match);
return Decoration.replace({});
}
// TODO: Replace this hacky match with a proper template parser
const name = innerTagMatch.match(/\s*(\w+)[(\s]*/)?.[1] ?? innerTagMatch;
let option = options.find((v) => v.name === name);
if (option == null) {
option = {
invalid: true,
type: 'variable',
name: innerTagMatch,
value: null,
label: innerTagMatch,
onClick: () => onClickMissingVariable(name, match[0], matchStartPos),
};
}
return Decoration.replace({
inclusive: true,
widget: new TemplateTagWidget(option, match[0], matchStartPos),
});
},
});
},
});
}
return Decoration.set(widgets);
}
export function templateTagsPlugin(
options: TwigCompletionOption[],
onClickMissingVariable: (name: string, tagValue: string, startPos: number) => void,
) {
return ViewPlugin.fromClass(
class {
decorations: DecorationSet;
constructor(view: EditorView) {
this.decorations = templateTagMatcher.createDeco(view);
this.decorations = templateTags(view, options, onClickMissingVariable);
}
update(update: ViewUpdate) {
this.decorations = templateTagMatcher.updateDeco(update, this.decorations);
this.decorations = templateTags(update.view, options, onClickMissingVariable);
}
},
{
decorations: (instance) => instance.decorations,
provide: (plugin) =>
EditorView.atomicRanges.of((view) => {
decorations(v) {
return v.decorations;
},
provide(plugin) {
return EditorView.atomicRanges.of((view) => {
return view.plugin(plugin)?.decorations || Decoration.none;
}),
});
},
eventHandlers: {
mousedown(e) {
const target = e.target as HTMLElement;
if (target.classList.contains('template-tag')) console.log('CLICKED TEMPLATE TAG');
// return toggleBoolean(view, view.posAtDOM(target));
},
},
},
);
}

View File

@@ -1,5 +1,5 @@
import { useQuery } from '@tanstack/react-query';
import type { CookieJar } from '../lib/models';
import type { CookieJar } from '@yaakapp/api';
import { invokeCmd } from '../lib/tauri';
import { useActiveWorkspace } from './useActiveWorkspace';

View File

@@ -1,6 +1,6 @@
import { useMutation } from '@tanstack/react-query';
import type { CookieJar } from '@yaakapp/api';
import { trackEvent } from '../lib/analytics';
import type { CookieJar } from '../lib/models';
import { invokeCmd } from '../lib/tauri';
import { useActiveWorkspace } from './useActiveWorkspace';
import { usePrompt } from './usePrompt';

View File

@@ -1,7 +1,7 @@
import { useMutation } from '@tanstack/react-query';
import type { CookieJar } from '@yaakapp/api';
import { InlineCode } from '../components/core/InlineCode';
import { trackEvent } from '../lib/analytics';
import type { CookieJar } from '../lib/models';
import { invokeCmd } from '../lib/tauri';
import { useConfirm } from './useConfirm';

View File

@@ -1,6 +1,6 @@
import type { Settings } from '@yaakapp/api';
import { useAtomValue } from 'jotai';
import { atom } from 'jotai/index';
import type { Settings } from '../lib/models/Settings';
import { getSettings } from '../lib/store';
const settings = await getSettings();

View File

@@ -1,5 +1,5 @@
import { useMutation } from '@tanstack/react-query';
import type { CookieJar } from '../lib/models';
import type { CookieJar } from '@yaakapp/api';
import { getCookieJar } from '../lib/store';
import { invokeCmd } from '../lib/tauri';

View File

@@ -1,5 +1,5 @@
import { useMutation } from '@tanstack/react-query';
import type { Settings } from '../lib/models';
import type { Settings } from '@yaakapp/api';
import { getSettings } from '../lib/store';
import { invokeCmd } from '../lib/tauri';

View File

@@ -1,9 +1,4 @@
import type { GrpcConnection, HttpResponse, HttpResponseHeader, Model } from '@yaakapp/api';
import type { Cookie } from './models/Cookie';
import type { CookieJar } from './models/CookieJar';
import type { Settings } from './models/Settings';
export type { CookieJar, Cookie, Settings };
import type { Cookie, GrpcConnection, HttpResponse, HttpResponseHeader, Model } from '@yaakapp/api';
export const BODY_TYPE_NONE = null;
export const BODY_TYPE_GRAPHQL = 'graphql';

View File

@@ -1,5 +0,0 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { CookieDomain } from "./CookieDomain";
import type { CookieExpires } from "./CookieExpires";
export type Cookie = { raw_cookie: string, domain: CookieDomain, expires: CookieExpires, path: [string, boolean], };

View File

@@ -1,3 +0,0 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type CookieDomain = { "HostOnly": string } | { "Suffix": string } | "NotPresent" | "Empty";

View File

@@ -1,3 +0,0 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type CookieExpires = { "AtUtc": string } | "SessionEnd";

View File

@@ -1,4 +0,0 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { Cookie } from "./Cookie";
export type CookieJar = { id: string, model: "cookie_jar", createdAt: string, updatedAt: string, workspaceId: string, name: string, cookies: Array<Cookie>, };

View File

@@ -1,3 +0,0 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type Settings = { id: string, model: "settings", createdAt: string, updatedAt: string, theme: string, appearance: string, themeDark: string, themeLight: string, updateChannel: string, interfaceFontSize: number, interfaceScale: number, editorFontSize: number, editorSoftWrap: boolean, openWorkspaceNewWindow: boolean | null, };

View File

@@ -1,6 +1,12 @@
import type { Environment, Folder, GrpcRequest, HttpRequest, Workspace } from '@yaakapp/api';
import type { CookieJar } from './models/CookieJar';
import type { Settings } from './models/Settings';
import type {
CookieJar,
Environment,
Folder,
GrpcRequest,
HttpRequest,
Settings,
Workspace,
} from '@yaakapp/api';
import { invokeCmd } from './tauri';
export async function getSettings(): Promise<Settings> {