This commit is contained in:
Gregory Schier
2025-01-26 13:13:45 -08:00
committed by GitHub
parent 82b1ad35ff
commit f678593903
99 changed files with 3492 additions and 1583 deletions

View File

@@ -5,6 +5,7 @@
@apply w-full block text-base;
/* Regular cursor */
.cm-cursor {
@apply border-text !important;
/* Widen the cursor a bit */
@@ -12,6 +13,7 @@
}
/* Vim-mode cursor */
.cm-fat-cursor {
@apply outline-0 bg-text !important;
@apply text-surface !important;
@@ -181,7 +183,7 @@
@apply hidden !important;
}
&.cm-singleline .cm-line {
&.cm-singleline * {
@apply cursor-default;
}
}

View File

@@ -1,4 +1,4 @@
import { defaultKeymap, historyField } from '@codemirror/commands';
import { defaultKeymap, historyField, indentWithTab } from '@codemirror/commands';
import { foldState, forceParsing } from '@codemirror/language';
import type { EditorStateConfig, Extension } from '@codemirror/state';
import { Compartment, EditorState } from '@codemirror/state';
@@ -34,14 +34,22 @@ import { TemplateVariableDialog } from '../../TemplateVariableDialog';
import { IconButton } from '../IconButton';
import { HStack } from '../Stacks';
import './Editor.css';
import { baseExtensions, getLanguageExtension, multiLineExtensions } from './extensions';
import {
baseExtensions,
getLanguageExtension,
multiLineExtensions,
readonlyExtensions,
} from './extensions';
import type { GenericCompletionConfig } from './genericCompletion';
import { singleLineExtensions } from './singleLine';
// VSCode's Tab actions mess with the single-line editor tab actions, so remove it.
const vsCodeWithoutTab = vscodeKeymap.filter((k) => k.key !== 'Tab');
const keymapExtensions: Record<EditorKeymap, Extension> = {
vim: vim(),
emacs: emacs(),
vscode: keymap.of(vscodeKeymap),
vscode: keymap.of(vsCodeWithoutTab),
default: [],
};
@@ -68,6 +76,7 @@ export interface EditorProps {
onKeyDown?: (e: KeyboardEvent) => void;
singleLine?: boolean;
wrapLines?: boolean;
disableTabIndent?: boolean;
format?: (v: string) => Promise<string>;
autocomplete?: GenericCompletionConfig;
autocompleteVariables?: boolean;
@@ -85,9 +94,9 @@ const emptyExtension: Extension = [];
export const Editor = forwardRef<EditorView | undefined, EditorProps>(function Editor(
{
readOnly,
type = 'text',
type,
heightMode,
language = 'text',
language,
autoFocus,
autoSelect,
placeholder,
@@ -101,6 +110,7 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
onBlur,
onKeyDown,
className,
disabled,
singleLine,
format,
autocomplete,
@@ -108,6 +118,7 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
autocompleteVariables,
actions,
wrapLines,
disableTabIndent,
hideGutter,
stateKey,
}: EditorProps,
@@ -122,6 +133,20 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
wrapLines = settings.editorSoftWrap;
}
if (disabled) {
readOnly = true;
}
if (
singleLine ||
language == null ||
language === 'text' ||
language === 'url' ||
language === 'pairs'
) {
disableTabIndent = true;
}
const cm = useRef<{ view: EditorView; languageCompartment: Compartment } | null>(null);
useImperativeHandle(ref, () => cm.current?.view, []);
@@ -166,7 +191,7 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
useEffect(
function configurePlaceholder() {
if (cm.current === null) return;
const ext = placeholderExt(placeholderElFromText(placeholder ?? '', type));
const ext = placeholderExt(placeholderElFromText(placeholder, type));
const effects = placeholderCompartment.current.reconfigure(ext);
cm.current?.view.dispatch({ effects });
},
@@ -209,6 +234,23 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
[wrapLines],
);
// Update tab indent
const tabIndentCompartment = useRef(new Compartment());
useEffect(
function configureTabIndent() {
if (cm.current === null) return;
const current = tabIndentCompartment.current.get(cm.current.view.state) ?? emptyExtension;
// PERF: This is expensive with hundreds of editors on screen, so only do it when necessary
if (disableTabIndent && current !== emptyExtension) return; // Nothing to do
if (!disableTabIndent && current === emptyExtension) return; // Nothing to do
const ext = !disableTabIndent ? keymap.of([indentWithTab]) : emptyExtension;
const effects = tabIndentCompartment.current.reconfigure(ext);
cm.current?.view.dispatch({ effects });
},
[disableTabIndent],
);
const onClickFunction = useCallback(
async (fn: TemplateFunction, tagValue: string, startPos: number) => {
const initialTokens = await parseTemplate(tagValue);
@@ -342,9 +384,12 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
const extensions = [
languageCompartment.of(langExt),
placeholderCompartment.current.of(
placeholderExt(placeholderElFromText(placeholder ?? '', type)),
placeholderExt(placeholderElFromText(placeholder, type)),
),
wrapLinesCompartment.current.of(wrapLines ? EditorView.lineWrapping : emptyExtension),
tabIndentCompartment.current.of(
!disableTabIndent ? keymap.of([indentWithTab]) : emptyExtension,
),
keymapCompartment.current.of(
keymapExtensions[settings.editorKeymap] ?? keymapExtensions['default'],
),
@@ -475,6 +520,7 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
className={classNames(
className,
'cm-wrapper text-base',
disabled && 'opacity-disabled',
type === 'password' && 'cm-obscure-text',
heightMode === 'auto' ? 'cm-auto-height' : 'cm-full-height',
singleLine ? 'cm-singleline' : 'cm-multiline',
@@ -557,10 +603,8 @@ function getExtensions({
tooltips({ parent }),
keymap.of(singleLine ? defaultKeymap.filter((k) => k.key !== 'Enter') : defaultKeymap),
...(singleLine ? [singleLineExtensions()] : []),
...(!singleLine ? [multiLineExtensions({ hideGutter })] : []),
...(readOnly
? [EditorState.readOnly.of(true), EditorView.contentAttributes.of({ tabindex: '-1' })]
: []),
...(!singleLine ? multiLineExtensions({ hideGutter }) : []),
...(readOnly ? readonlyExtensions : []),
// ------------------------ //
// Things that must be last //
@@ -580,13 +624,15 @@ function getExtensions({
];
}
const placeholderElFromText = (text: string, type: EditorProps['type']) => {
const placeholderElFromText = (text: string | undefined, type: EditorProps['type']) => {
const el = document.createElement('div');
if (type === 'password') {
// Will be obscured (dots) so just needs to be something to take up space
el.innerHTML = 'something-cool';
el.setAttribute('aria-hidden', 'true');
} else {
// Default to <SPACE> because codemirror needs it for sizing. I'm not sure why, but probably something
// to do with how Yaak "hacks" it with CSS for single line input.
el.innerHTML = text ? text.replaceAll('\n', '<br/>') : ' ';
}
return el;
@@ -596,8 +642,8 @@ function saveCachedEditorState(stateKey: string | null, state: EditorState | nul
if (!stateKey || state == null) return;
const stateObj = state.toJSON(stateFields);
// Save state in sessionStorage by removing doc and saving the hash of it instead
// This will be checked on restore and put back in if it matches
// Save state in sessionStorage by removing doc and saving the hash of it instead.
// This will be checked on restore and put back in if it matches.
stateObj.docHash = md5(stateObj.doc);
delete stateObj.doc;

View File

@@ -4,7 +4,7 @@ import {
closeBracketsKeymap,
completionKeymap,
} from '@codemirror/autocomplete';
import { history, historyKeymap, indentWithTab } from '@codemirror/commands';
import { history, historyKeymap } from '@codemirror/commands';
import { javascript } from '@codemirror/lang-javascript';
import { json } from '@codemirror/lang-json';
import { markdown } from '@codemirror/lang-markdown';
@@ -142,6 +142,11 @@ export const baseExtensions = [
keymap.of([...historyKeymap, ...completionKeymap]),
];
export const readonlyExtensions = [
EditorState.readOnly.of(true),
EditorView.contentAttributes.of({ tabindex: '-1' }),
];
export const multiLineExtensions = ({ hideGutter }: { hideGutter?: boolean }) => [
hideGutter
? []
@@ -208,5 +213,5 @@ export const multiLineExtensions = ({ hideGutter }: { hideGutter?: boolean }) =>
rectangularSelection(),
crosshairCursor(),
highlightActiveLineGutter(),
keymap.of([indentWithTab, ...closeBracketsKeymap, ...searchKeymap, ...foldKeymap, ...lintKeymap]),
keymap.of([...closeBracketsKeymap, ...searchKeymap, ...foldKeymap, ...lintKeymap]),
];

View File

@@ -1,16 +1,5 @@
import type { CompletionContext } from '@codemirror/autocomplete';
export interface GenericCompletionOption {
label: string;
type: 'constant' | 'variable';
detail?: string;
info?: string;
/** When given, should be a number from -99 to 99 that adjusts
* how this completion is ranked compared to other completions
* that match the input as well as this one. A negative number
* moves it down the list, a positive number moves it up. */
boost?: number;
}
import type { GenericCompletionOption } from '@yaakapp-internal/plugins';
export interface GenericCompletionConfig {
minMatch?: number;