Vim/emacs/vscode keybindings

This commit is contained in:
Gregory Schier
2025-01-07 22:27:43 -08:00
parent 3cf372c01e
commit 81005165f3
14 changed files with 217 additions and 256 deletions

44
package-lock.json generated
View File

@@ -1782,6 +1782,47 @@
"integrity": "sha512-/RVXdLvJxLg4QKvMoM5WlwNR9ViO9z8B/qPcc+C0Sa/teJY7QG7kJ441DwzOjMYEY7GmU4dj5EcGHIkKZiQZCA==",
"license": "MIT"
},
"node_modules/@replit/codemirror-emacs": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/@replit/codemirror-emacs/-/codemirror-emacs-6.1.0.tgz",
"integrity": "sha512-74DITnht6Cs6sHg02PQ169IKb1XgtyhI9sLD0JeOFco6Ds18PT+dkD8+DgXBDokne9UIFKsBbKPnpFRAz60/Lw==",
"license": "MIT",
"peerDependencies": {
"@codemirror/autocomplete": "^6.0.2",
"@codemirror/commands": "^6.0.0",
"@codemirror/search": "^6.0.0",
"@codemirror/state": "^6.0.1",
"@codemirror/view": "^6.3.0"
}
},
"node_modules/@replit/codemirror-vim": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/@replit/codemirror-vim/-/codemirror-vim-6.2.1.tgz",
"integrity": "sha512-qDAcGSHBYU5RrdO//qCmD8K9t6vbP327iCj/iqrkVnjbrpFhrjOt92weGXGHmTNRh16cUtkUZ7Xq7rZf+8HVow==",
"license": "MIT",
"peerDependencies": {
"@codemirror/commands": "^6.0.0",
"@codemirror/language": "^6.1.0",
"@codemirror/search": "^6.2.0",
"@codemirror/state": "^6.0.1",
"@codemirror/view": "^6.0.3"
}
},
"node_modules/@replit/codemirror-vscode-keymap": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/@replit/codemirror-vscode-keymap/-/codemirror-vscode-keymap-6.0.2.tgz",
"integrity": "sha512-j45qTwGxzpsv82lMD/NreGDORFKSctMDVkGRopaP+OrzSzv+pXDQuU3LnFvKpasyjVT0lf+PKG1v2DSCn/vxxg==",
"license": "MIT",
"peerDependencies": {
"@codemirror/autocomplete": "^6.0.0",
"@codemirror/commands": "^6.0.0",
"@codemirror/language": "^6.0.0",
"@codemirror/lint": "^6.0.0",
"@codemirror/search": "^6.0.0",
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.0.0"
}
},
"node_modules/@rollup/plugin-virtual": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/@rollup/plugin-virtual/-/plugin-virtual-3.0.2.tgz",
@@ -15878,6 +15919,9 @@
"@gilbarbara/deep-equal": "^0.3.1",
"@lezer/highlight": "^1.1.3",
"@lezer/lr": "^1.3.3",
"@replit/codemirror-emacs": "^6.1.0",
"@replit/codemirror-vim": "^6.2.1",
"@replit/codemirror-vscode-keymap": "^6.0.2",
"@tailwindcss/container-queries": "^0.1.1",
"@tanstack/react-query": "^5.62.16",
"@tanstack/react-router": "^1.95.1",

View File

@@ -0,0 +1,2 @@
ALTER TABLE settings
ADD COLUMN editor_keymap TEXT DEFAULT 'codemirror' NOT NULL;

View File

@@ -1,250 +1,61 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type AnyModel =
| CookieJar
| Environment
| Folder
| GrpcConnection
| GrpcEvent
| GrpcRequest
| HttpRequest
| HttpResponse
| Plugin
| Settings
| KeyValue
| Workspace;
export type AnyModel = CookieJar | Environment | Folder | GrpcConnection | GrpcEvent | GrpcRequest | HttpRequest | HttpResponse | Plugin | Settings | KeyValue | Workspace;
export type Cookie = {
raw_cookie: string;
domain: CookieDomain;
expires: CookieExpires;
path: [string, boolean];
};
export type Cookie = { raw_cookie: string, domain: CookieDomain, expires: CookieExpires, path: [string, boolean], };
export type CookieDomain = { HostOnly: string } | { Suffix: string } | 'NotPresent' | 'Empty';
export type CookieDomain = { "HostOnly": string } | { "Suffix": string } | "NotPresent" | "Empty";
export type CookieExpires = { AtUtc: string } | 'SessionEnd';
export type CookieExpires = { "AtUtc": string } | "SessionEnd";
export type CookieJar = {
model: 'cookie_jar';
id: string;
createdAt: string;
updatedAt: string;
workspaceId: string;
cookies: Array<Cookie>;
name: string;
};
export type CookieJar = { model: "cookie_jar", id: string, createdAt: string, updatedAt: string, workspaceId: string, cookies: Array<Cookie>, name: string, };
export type Environment = {
model: 'environment';
id: string;
workspaceId: string;
environmentId: string | null;
createdAt: string;
updatedAt: string;
name: string;
variables: Array<EnvironmentVariable>;
};
export type EditorKeymap = "default" | "vim" | "vscode" | "emacs";
export type EnvironmentVariable = { enabled?: boolean; name: string; value: string; id: string };
export type Environment = { model: "environment", id: string, workspaceId: string, environmentId: string | null, createdAt: string, updatedAt: string, name: string, variables: Array<EnvironmentVariable>, };
export type Folder = {
model: 'folder';
id: string;
createdAt: string;
updatedAt: string;
workspaceId: string;
folderId: string | null;
name: string;
description: string;
sortPriority: number;
};
export type EnvironmentVariable = { enabled?: boolean, name: string, value: string, id: string, };
export type GrpcConnection = {
model: 'grpc_connection';
id: string;
createdAt: string;
updatedAt: string;
workspaceId: string;
requestId: string;
elapsed: number;
error: string | null;
method: string;
service: string;
status: number;
state: GrpcConnectionState;
trailers: { [key in string]?: string };
url: string;
};
export type Folder = { model: "folder", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, name: string, description: string, sortPriority: number, };
export type GrpcConnectionState = 'initialized' | 'connected' | 'closed';
export type GrpcConnection = { model: "grpc_connection", id: string, createdAt: string, updatedAt: string, workspaceId: string, requestId: string, elapsed: number, error: string | null, method: string, service: string, status: number, state: GrpcConnectionState, trailers: { [key in string]?: string }, url: string, };
export type GrpcEvent = {
model: 'grpc_event';
id: string;
createdAt: string;
updatedAt: string;
workspaceId: string;
requestId: string;
connectionId: string;
content: string;
error: string | null;
eventType: GrpcEventType;
metadata: { [key in string]?: string };
status: number | null;
};
export type GrpcConnectionState = "initialized" | "connected" | "closed";
export type GrpcEventType =
| 'info'
| 'error'
| 'client_message'
| 'server_message'
| 'connection_start'
| 'connection_end';
export type GrpcEvent = { model: "grpc_event", id: string, createdAt: string, updatedAt: string, workspaceId: string, requestId: string, connectionId: string, content: string, error: string | null, eventType: GrpcEventType, metadata: { [key in string]?: string }, status: number | null, };
export type GrpcMetadataEntry = { enabled?: boolean; name: string; value: string; id: string };
export type GrpcEventType = "info" | "error" | "client_message" | "server_message" | "connection_start" | "connection_end";
export type GrpcRequest = {
model: 'grpc_request';
id: string;
createdAt: string;
updatedAt: string;
workspaceId: string;
folderId: string | null;
authenticationType: string | null;
authentication: Record<string, any>;
description: string;
message: string;
metadata: Array<GrpcMetadataEntry>;
method: string | null;
name: string;
service: string | null;
sortPriority: number;
url: string;
};
export type GrpcMetadataEntry = { enabled?: boolean, name: string, value: string, id: string, };
export type HttpRequest = {
model: 'http_request';
id: string;
createdAt: string;
updatedAt: string;
workspaceId: string;
folderId: string | null;
authentication: Record<string, any>;
authenticationType: string | null;
body: Record<string, any>;
bodyType: string | null;
description: string;
headers: Array<HttpRequestHeader>;
method: string;
name: string;
sortPriority: number;
url: string;
urlParameters: Array<HttpUrlParameter>;
};
export type GrpcRequest = { model: "grpc_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authenticationType: string | null, authentication: Record<string, any>, description: string, message: string, metadata: Array<GrpcMetadataEntry>, method: string | null, name: string, service: string | null, sortPriority: number, url: string, };
export type HttpRequestHeader = { enabled?: boolean; name: string; value: string; id: string };
export type HttpRequest = { model: "http_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authentication: Record<string, any>, authenticationType: string | null, body: Record<string, any>, bodyType: string | null, description: string, headers: Array<HttpRequestHeader>, method: string, name: string, sortPriority: number, url: string, urlParameters: Array<HttpUrlParameter>, };
export type HttpResponse = {
model: 'http_response';
id: string;
createdAt: string;
updatedAt: string;
workspaceId: string;
requestId: string;
bodyPath: string | null;
contentLength: number | null;
elapsed: number;
elapsedHeaders: number;
error: string | null;
headers: Array<HttpResponseHeader>;
remoteAddr: string | null;
status: number;
statusReason: string | null;
state: HttpResponseState;
url: string;
version: string | null;
};
export type HttpRequestHeader = { enabled?: boolean, name: string, value: string, id: string, };
export type HttpResponseHeader = { name: string; value: string };
export type HttpResponse = { model: "http_response", id: string, createdAt: string, updatedAt: string, workspaceId: string, requestId: string, bodyPath: string | null, contentLength: number | null, elapsed: number, elapsedHeaders: number, error: string | null, headers: Array<HttpResponseHeader>, remoteAddr: string | null, status: number, statusReason: string | null, state: HttpResponseState, url: string, version: string | null, };
export type HttpResponseState = 'initialized' | 'connected' | 'closed';
export type HttpResponseHeader = { name: string, value: string, };
export type HttpUrlParameter = { enabled?: boolean; name: string; value: string; id: string };
export type HttpResponseState = "initialized" | "connected" | "closed";
export type KeyValue = {
model: 'key_value';
createdAt: string;
updatedAt: string;
key: string;
namespace: string;
value: string;
};
export type HttpUrlParameter = { enabled?: boolean, name: string, value: string, id: string, };
export type ModelPayload = { model: AnyModel; windowLabel: string; updateSource: UpdateSource };
export type KeyValue = { model: "key_value", createdAt: string, updatedAt: string, key: string, namespace: string, value: string, };
export type Plugin = {
model: 'plugin';
id: string;
createdAt: string;
updatedAt: string;
checkedAt: string | null;
directory: string;
enabled: boolean;
url: string | null;
};
export type ModelPayload = { model: AnyModel, windowLabel: string, updateSource: UpdateSource, };
export type ProxySetting =
| { type: 'enabled'; http: string; https: string; auth: ProxySettingAuth | null }
| {
type: 'disabled';
};
export type Plugin = { model: "plugin", id: string, createdAt: string, updatedAt: string, checkedAt: string | null, directory: string, enabled: boolean, url: string | null, };
export type ProxySettingAuth = { user: string; password: string };
export type ProxySetting = { "type": "enabled", http: string, https: string, auth: ProxySettingAuth | null, } | { "type": "disabled" };
export type Settings = {
model: 'settings';
id: string;
createdAt: string;
updatedAt: string;
appearance: string;
editorFontSize: number;
editorSoftWrap: boolean;
interfaceFontSize: number;
interfaceScale: number;
openWorkspaceNewWindow: boolean | null;
telemetry: boolean;
theme: string;
themeDark: string;
themeLight: string;
updateChannel: string;
proxy: ProxySetting | null;
};
export type ProxySettingAuth = { user: string, password: string, };
export type SyncState = {
model: 'sync_state';
id: string;
workspaceId: string;
createdAt: string;
updatedAt: string;
flushedAt: string;
modelId: string;
checksum: string;
relPath: string;
syncDir: string;
};
export type Settings = { model: "settings", id: string, createdAt: string, updatedAt: string, appearance: string, editorFontSize: number, editorSoftWrap: boolean, interfaceFontSize: number, interfaceScale: number, openWorkspaceNewWindow: boolean | null, proxy: ProxySetting | null, telemetry: boolean, theme: string, themeDark: string, themeLight: string, updateChannel: string, editorKeymap: EditorKeymap, };
export type UpdateSource = 'sync' | 'window' | 'plugin' | 'background';
export type SyncState = { model: "sync_state", id: string, workspaceId: string, createdAt: string, updatedAt: string, flushedAt: string, modelId: string, checksum: string, relPath: string, syncDir: string, };
export type Workspace = {
model: 'workspace';
id: string;
createdAt: string;
updatedAt: string;
name: string;
description: string;
settingValidateCertificates: boolean;
settingFollowRedirects: boolean;
settingRequestTimeout: number;
settingSyncDir: string | null;
};
export type UpdateSource = "sync" | "window" | "plugin" | "background";
export type Workspace = { model: "workspace", id: string, createdAt: string, updatedAt: string, name: string, description: string, settingValidateCertificates: boolean, settingFollowRedirects: boolean, settingRequestTimeout: number, settingSyncDir: string | null, };

View File

@@ -4,6 +4,8 @@ use sea_query::Iden;
use serde::{Deserialize, Deserializer, Serialize};
use serde_json::Value;
use std::collections::BTreeMap;
use std::fmt::Display;
use std::str::FromStr;
use ts_rs::TS;
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
@@ -26,6 +28,48 @@ pub struct ProxySettingAuth {
pub password: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[serde(rename_all = "snake_case")]
#[ts(export, export_to = "models.ts")]
pub enum EditorKeymap {
Default,
Vim,
Vscode,
Emacs,
}
impl FromStr for EditorKeymap {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"default" => Ok(Self::Default),
"vscode" => Ok(Self::Vscode),
"vim" => Ok(Self::Vim),
"emacs" => Ok(Self::Emacs),
_ => Ok(Self::default()),
}
}
}
impl Display for EditorKeymap {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let str = match self {
EditorKeymap::Default => "default".to_string(),
EditorKeymap::Vscode => "vscode".to_string(),
EditorKeymap::Vim => "vim".to_string(),
EditorKeymap::Emacs => "emacs".to_string(),
};
write!(f, "{}", str)
}
}
impl Default for EditorKeymap {
fn default() -> Self {
Self::Default
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "models.ts")]
@@ -42,12 +86,13 @@ pub struct Settings {
pub interface_font_size: i32,
pub interface_scale: f32,
pub open_workspace_new_window: Option<bool>,
pub proxy: Option<ProxySetting>,
pub telemetry: bool,
pub theme: String,
pub theme_dark: String,
pub theme_light: String,
pub update_channel: String,
pub proxy: Option<ProxySetting>,
pub editor_keymap: EditorKeymap,
}
#[derive(Iden)]
@@ -61,6 +106,7 @@ pub enum SettingsIden {
Appearance,
EditorFontSize,
EditorKeymap,
EditorSoftWrap,
InterfaceFontSize,
InterfaceScale,
@@ -78,6 +124,7 @@ impl<'s> TryFrom<&Row<'s>> for Settings {
fn try_from(r: &Row<'s>) -> Result<Self, Self::Error> {
let proxy: Option<String> = r.get("proxy")?;
let editor_keymap: String = r.get("editor_keymap")?;
Ok(Settings {
id: r.get("id")?,
model: r.get("model")?,
@@ -85,6 +132,7 @@ impl<'s> TryFrom<&Row<'s>> for Settings {
updated_at: r.get("updated_at")?,
appearance: r.get("appearance")?,
editor_font_size: r.get("editor_font_size")?,
editor_keymap: EditorKeymap::from_str(editor_keymap.as_str()).unwrap(),
editor_soft_wrap: r.get("editor_soft_wrap")?,
interface_font_size: r.get("interface_font_size")?,
interface_scale: r.get("interface_scale")?,
@@ -1043,9 +1091,7 @@ impl<'de> Deserialize<'de> for AnyModel {
Some(m) if m == "environment" => {
AnyModel::Environment(serde_json::from_value(value).unwrap())
}
Some(m) if m == "folder" => {
AnyModel::Folder(serde_json::from_value(value).unwrap())
}
Some(m) if m == "folder" => AnyModel::Folder(serde_json::from_value(value).unwrap()),
Some(m) if m == "key_value" => {
AnyModel::KeyValue(serde_json::from_value(value).unwrap())
}
@@ -1058,9 +1104,7 @@ impl<'de> Deserialize<'de> for AnyModel {
Some(m) if m == "cookie_jar" => {
AnyModel::CookieJar(serde_json::from_value(value).unwrap())
}
Some(m) if m == "plugin" => {
AnyModel::Plugin(serde_json::from_value(value).unwrap())
}
Some(m) if m == "plugin" => AnyModel::Plugin(serde_json::from_value(value).unwrap()),
Some(m) => {
return Err(serde::de::Error::custom(format!("Unknown model {}", m)));
}

View File

@@ -914,6 +914,7 @@ pub async fn update_settings<R: Runtime>(
(SettingsIden::InterfaceFontSize, settings.interface_font_size.into()),
(SettingsIden::InterfaceScale, settings.interface_scale.into()),
(SettingsIden::EditorFontSize, settings.editor_font_size.into()),
(SettingsIden::EditorKeymap, settings.editor_keymap.to_string().into()),
(SettingsIden::EditorSoftWrap, settings.editor_soft_wrap.into()),
(SettingsIden::Telemetry, settings.telemetry.into()),
(SettingsIden::OpenWorkspaceNewWindow, settings.open_workspace_new_window.into()),

View File

@@ -1,6 +1,6 @@
import { useFolders } from '../hooks/useFolders';
import { useUpdateAnyFolder } from '../hooks/useUpdateAnyFolder';
import { PlainInput } from './core/PlainInput';
import { Input } from './core/Input';
import { VStack } from './core/Stacks';
import { MarkdownEditor } from './MarkdownEditor';
@@ -17,13 +17,14 @@ export function FolderSettingsDialog({ folderId }: Props) {
return (
<VStack space={3} className="pb-3">
<PlainInput
<Input
label="Folder Name"
defaultValue={folder.name}
onChange={(name) => {
if (folderId == null) return;
updateFolder({ id: folderId, update: (folder) => ({ ...folder, name }) });
}}
stateKey={`name.${folder.id}`}
/>
<MarkdownEditor

View File

@@ -45,8 +45,8 @@ import type {
GenericCompletionOption,
} from './core/Editor/genericCompletion';
import { InlineCode } from './core/InlineCode';
import { Input } from './core/Input';
import type { Pair } from './core/PairEditor';
import { PlainInput } from './core/PlainInput';
import type { TabItem } from './core/Tabs/Tabs';
import { TabContent, Tabs } from './core/Tabs/Tabs';
import { EmptyStateText } from './EmptyStateText';
@@ -481,7 +481,7 @@ export const RequestPane = memo(function RequestPane({
</TabContent>
<TabContent value={TAB_DESCRIPTION}>
<div className="grid grid-rows-[auto_minmax(0,1fr)] h-full">
<PlainInput
<Input
label="Request Name"
hideLabel
forceUpdateKey={forceUpdateKey}
@@ -490,6 +490,7 @@ export const RequestPane = memo(function RequestPane({
containerClassName="border-0"
placeholder="Request Name"
onChange={(name) => updateRequest({ id: activeRequestId, update: { name } })}
stateKey={`name.${activeRequest.id}`}
/>
<MarkdownEditor
name="request-description"

View File

@@ -1,3 +1,4 @@
import type { EditorKeymap } from '@yaakapp-internal/models';
import React from 'react';
import { useActiveWorkspace } from '../../hooks/useActiveWorkspace';
import { useResolvedAppearance } from '../../hooks/useResolvedAppearance';
@@ -9,7 +10,7 @@ import { getThemes } from '../../lib/theme/themes';
import { isThemeDark } from '../../lib/theme/window';
import type { ButtonProps } from '../core/Button';
import { Checkbox } from '../core/Checkbox';
import {Editor} from "../core/Editor/Editor";
import { Editor } from '../core/Editor/Editor';
import type { IconProps } from '../core/Icon';
import { Icon } from '../core/Icon';
import { IconButton } from '../core/IconButton';
@@ -18,10 +19,13 @@ import { Select } from '../core/Select';
import { Separator } from '../core/Separator';
import { HStack, VStack } from '../core/Stacks';
const fontSizes = [
const fontSizeOptions = [
8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30,
].map((n) => ({ label: `${n}`, value: `${n}` }));
const keymaps: EditorKeymap[] = ['default', 'vim', 'vscode', 'emacs'];
const keymapOptions = keymaps.map((n) => ({ label: n, value: n }));
const buttonColors: ButtonProps['color'][] = [
'primary',
'info',
@@ -86,9 +90,9 @@ export function SettingsAppearance() {
label="Font Size"
labelPosition="left"
value={`${settings.interfaceFontSize}`}
options={fontSizes}
options={fontSizeOptions}
onChange={(v) => updateSettings.mutate({ interfaceFontSize: parseInt(v) })}
event="font-size.interface"
event="ui-font-size"
/>
<Select
size="sm"
@@ -96,15 +100,25 @@ export function SettingsAppearance() {
label="Editor Font Size"
labelPosition="left"
value={`${settings.editorFontSize}`}
options={fontSizes}
options={fontSizeOptions}
onChange={(v) => updateSettings.mutate({ editorFontSize: clamp(parseInt(v) || 14, 8, 30) })}
event="font-size.editor"
event="editor-font-size"
/>
<Checkbox
checked={settings.editorSoftWrap}
title="Wrap Editor Lines"
onChange={(editorSoftWrap) => updateSettings.mutate({ editorSoftWrap })}
event="wrap-lines"
event="editor-wrap-lines"
/>
<Select
size="sm"
name="editorKeymap"
label="Editor Key Map"
labelPosition="left"
value={`${settings.editorKeymap}`}
options={keymapOptions}
onChange={(v) => updateSettings.mutate({ editorKeymap: v })}
event="editor-keymap"
/>
<Separator className="my-4" />

View File

@@ -2,7 +2,7 @@ import { useDeleteActiveWorkspace } from '../hooks/useDeleteActiveWorkspace';
import { useUpdateWorkspace } from '../hooks/useUpdateWorkspace';
import { useWorkspaces } from '../hooks/useWorkspaces';
import { Button } from './core/Button';
import { PlainInput } from './core/PlainInput';
import { Input } from './core/Input';
import { VStack } from './core/Stacks';
import { MarkdownEditor } from './MarkdownEditor';
import { SelectFile } from './SelectFile';
@@ -22,10 +22,11 @@ export function WorkspaceSettingsDialog({ workspaceId, hide }: Props) {
return (
<VStack space={3} alignItems="start" className="pb-3 max-h-[50vh]">
<PlainInput
<Input
label="Workspace Name"
defaultValue={workspace.name}
onChange={(name) => updateWorkspace({ name })}
stateKey={`name.${workspace.id}`}
/>
<MarkdownEditor

View File

@@ -4,16 +4,28 @@
.cm-editor {
@apply w-full block text-base;
/* Regular cursor */
.cm-cursor {
@apply border-text !important;
/* Widen the cursor */
/* Widen the cursor a bit */
@apply border-l-[2px];
}
/* Vim-mode cursor */
.cm-fat-cursor {
@apply bg-text opacity-60;
}
&.cm-focused {
outline: none !important;
}
&:not(.cm-focused) {
.cm-cursor, .cm-fat-cursor {
display: none;
}
}
.cm-content {
@apply py-0;
}

View File

@@ -3,7 +3,10 @@ import { foldState, forceParsing } from '@codemirror/language';
import type { EditorStateConfig, Extension } from '@codemirror/state';
import { Compartment, EditorState } from '@codemirror/state';
import { keymap, placeholder as placeholderExt, tooltips } from '@codemirror/view';
import type { EnvironmentVariable } from '@yaakapp-internal/models';
import { emacs } from '@replit/codemirror-emacs';
import { vim } from '@replit/codemirror-vim';
import { vscodeKeymap } from '@replit/codemirror-vscode-keymap';
import type { EditorKeymap, EnvironmentVariable } from '@yaakapp-internal/models';
import type { TemplateFunction } from '@yaakapp-internal/plugins';
import classNames from 'classnames';
import { EditorView } from 'codemirror';
@@ -33,15 +36,17 @@ import { TemplateVariableDialog } from '../../TemplateVariableDialog';
import { IconButton } from '../IconButton';
import { HStack } from '../Stacks';
import './Editor.css';
import {
baseExtensions,
emptyExtension,
getLanguageExtension,
multiLineExtensions,
} from './extensions';
import { baseExtensions, getLanguageExtension, multiLineExtensions } from './extensions';
import type { GenericCompletionConfig } from './genericCompletion';
import { singleLineExtensions } from './singleLine';
const keymapExtensions: Record<EditorKeymap, Extension> = {
vim: vim(),
emacs: emacs(),
vscode: keymap.of(vscodeKeymap),
default: [],
};
export interface EditorProps {
id?: string;
readOnly?: boolean;
@@ -86,6 +91,7 @@ export interface EditorProps {
const stateFields = { history: historyField, folds: foldState };
const emptyVariables: EnvironmentVariable[] = [];
const emptyExtension: Extension = [];
export const Editor = forwardRef<EditorView | undefined, EditorProps>(function Editor(
{
@@ -119,6 +125,7 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
ref,
) {
const settings = useSettings();
const templateFunctions = useTemplateFunctions();
const allEnvironmentVariables = useActiveEnvironmentVariables();
const environmentVariables = autocompleteVariables ? allEnvironmentVariables : emptyVariables;
@@ -178,6 +185,25 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
[placeholder],
);
// Update vim
const keymapCompartment = useRef(new Compartment());
useEffect(
function configureKeymap() {
if (cm.current === null) return;
const current = keymapCompartment.current.get(cm.current.view.state) ?? [];
// PERF: This is expensive with hundreds of editors on screen, so only do it when necessary
if (settings.editorKeymap === 'default' && current === keymapExtensions['default']) return; // Nothing to do
if (settings.editorKeymap === 'vim' && current === keymapExtensions['vim']) return; // Nothing to do
if (settings.editorKeymap === 'vscode' && current === keymapExtensions['vscode']) return; // Nothing to do
if (settings.editorKeymap === 'emacs' && current === keymapExtensions['emacs']) return; // Nothing to do
const ext = keymapExtensions[settings.editorKeymap] ?? keymapExtensions['default'];
const effect = keymapCompartment.current.reconfigure(ext);
cm.current.view.dispatch({ effects: effect });
},
[settings.editorKeymap],
);
// Update wrap lines
const wrapLinesCompartment = useRef(new Compartment());
useEffect(
@@ -188,7 +214,7 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
if (wrapLines && current !== emptyExtension) return; // Nothing to do
if (!wrapLines && current === emptyExtension) return; // Nothing to do
const ext = wrapLines ? EditorView.lineWrapping : emptyExtension;
const ext = wrapLines ? EditorView.lineWrapping : [];
const effect = wrapLinesCompartment.current.reconfigure(ext);
cm.current?.view.dispatch({ effects: effect });
},
@@ -331,7 +357,10 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
placeholderCompartment.current.of(
placeholderExt(placeholderElFromText(placeholder ?? '')),
),
wrapLinesCompartment.current.of(wrapLines ? EditorView.lineWrapping : emptyExtension),
wrapLinesCompartment.current.of(wrapLines ? EditorView.lineWrapping : []),
keymapCompartment.current.of(
keymapExtensions[settings.editorKeymap] ?? keymapExtensions['default'],
),
...getExtensions({
container,
readOnly,

View File

@@ -21,7 +21,6 @@ import {
import { lintKeymap } from '@codemirror/lint';
import { searchKeymap } from '@codemirror/search';
import type { Extension } from '@codemirror/state';
import { EditorState } from '@codemirror/state';
import {
crosshairCursor,
@@ -86,8 +85,6 @@ const syntaxExtensions: Record<NonNullable<EditorProps['language']>, LanguageSup
markdown: markdown(),
};
export const emptyExtension: Extension = [];
export function getLanguageExtension({
language,
useTemplating = false,

View File

@@ -1,9 +1,9 @@
import type { PromptTextRequest } from '@yaakapp-internal/plugins';
import type { FormEvent, ReactNode } from 'react';
import { useCallback, useState } from 'react';
import {PlainInput} from "./PlainInput";
import { HStack } from './Stacks';
import { Button } from './Button';
import { Input } from './Input';
import { HStack } from './Stacks';
export type PromptProps = Omit<PromptTextRequest, 'id' | 'title' | 'description'> & {
description?: ReactNode;
@@ -35,7 +35,7 @@ export function Prompt({
className="grid grid-rows-[auto_auto] grid-cols-[minmax(0,1fr)] gap-4 mb-4"
onSubmit={handleSubmit}
>
<PlainInput
<Input
hideLabel
autoSelect
require={require}
@@ -43,6 +43,7 @@ export function Prompt({
label={label}
defaultValue={defaultValue}
onChange={setValue}
stateKey={null}
/>
<HStack space={2} justifyContent="end">
<Button onClick={onCancel} variant="border" color="secondary">

View File

@@ -19,6 +19,9 @@
"@gilbarbara/deep-equal": "^0.3.1",
"@lezer/highlight": "^1.1.3",
"@lezer/lr": "^1.3.3",
"@replit/codemirror-emacs": "^6.1.0",
"@replit/codemirror-vim": "^6.2.1",
"@replit/codemirror-vscode-keymap": "^6.0.2",
"@tailwindcss/container-queries": "^0.1.1",
"@tanstack/react-query": "^5.62.16",
"@tanstack/react-router": "^1.95.1",