Second line
+ Third line
+ `);
+ });
+
+ it('should back out of replace correctly', () => {
+ replaceInCode('div');
+
+ typeCode('{esc}');
+
+ assertCodePaneContains(dedent`
+ First line
+ Second line
+ Third line
+ `);
+
+ typeCode('c');
+
+ assertCodePaneContains(dedent`
+ cFirst line
+ Second line
+ Third line
+ `);
+ });
+ });
+
+ describe('jump to line', () => {
+ beforeEach(() => {
+ loadPlayroom(`
+ First line
+ Second line
+ Third line
+ Forth line
+ Fifth line
+ Sixth line
+ Seventh line
+ `);
+ });
+
+ it('should jump to line number correctly', () => {
+ const line = 6;
+ jumpToLine(line);
+
+ typeCode('c');
+
+ assertCodePaneContains(dedent`
+ First line
+ Second line
+ Third line
+ Forth line
+ Fifth line
+ cSixth line
+ Seventh line
+ `);
+
+ typeCode('{backspace}');
+
+ const nextLine = 2;
+ jumpToLine(nextLine);
+
+ typeCode('c');
+
+ assertCodePaneContains(dedent`
+ First line
+ cSecond line
+ Third line
+ Forth line
+ Fifth line
+ Sixth line
+ Seventh line
+ `);
+ });
+
+ it('should jump to line and column number correctly', () => {
+ jumpToLine('6:10');
+ typeCode('a');
+
+ assertCodePaneContains(dedent`
+ First line
+ Second line
+ Third line
+ Forth line
+ Fifth line
+ Sixtha line
+ Seventh line
+ `);
+ });
+ });
+
describe('toggleComment', () => {
const blockStarter = `
First line
@@ -1461,7 +1618,7 @@ describe('Keymaps', () => {
});
});
- describe('for an selection beginning during opening comment syntax', () => {
+ describe('for a selection beginning during opening comment syntax', () => {
it('block', () => {
loadPlayroom(`
diff --git a/cypress/support/utils.js b/cypress/support/utils.js
index 18986518..091d3686 100644
--- a/cypress/support/utils.js
+++ b/cypress/support/utils.js
@@ -5,6 +5,11 @@ import dedent from 'dedent';
import { createUrl } from '../../utils';
import { isMac } from '../../src/utils/formatting';
+export const cmdPlus = (keyCombo) => {
+ const platformSpecificKey = isMac() ? 'cmd' : 'ctrl';
+ return `${platformSpecificKey}+${keyCombo}`;
+};
+
const getCodeEditor = () =>
cy.get('.CodeMirror-code').then((editor) => cy.wrap(editor));
@@ -182,3 +187,54 @@ export const loadPlayroom = (initialCode) => {
indexedDB.deleteDatabase(storageKey);
});
};
+
+const typeInSearchField = (text) =>
+ /*
+ force true is required because cypress incorrectly and intermittently
+ reports that search field is covered by another element
+ */
+ cy.get('.CodeMirror-search-field').type(text, { force: true });
+
+/**
+ * @param {string} term
+ */
+export const findInCode = (term) => {
+ // Wait necessary to ensure code pane is focussed
+ cy.wait(200); // eslint-disable-line @finsit/cypress/no-unnecessary-waiting
+ typeCode(`{${cmdPlus('f')}}`);
+
+ typeInSearchField(`${term}{enter}`);
+};
+
+/**
+ * @param {string} term
+ * @param {string} [replaceWith]
+ */
+export const replaceInCode = (term, replaceWith) => {
+ // Wait necessary to ensure code pane is focussed
+ cy.wait(200); // eslint-disable-line @finsit/cypress/no-unnecessary-waiting
+ typeCode(`{${cmdPlus('alt+f')}}`);
+ typeInSearchField(`${term}{enter}`);
+ if (replaceWith) {
+ typeInSearchField(`${replaceWith}{enter}`);
+ }
+};
+
+/**
+ * @param {number} line
+ */
+export const jumpToLine = (line) => {
+ // Wait necessary to ensure code pane is focussed
+ cy.wait(200); // eslint-disable-line @finsit/cypress/no-unnecessary-waiting
+ typeCode(`{${cmdPlus('g')}}`);
+ typeCode(`${line}{enter}`);
+};
+
+/**
+ * @param {number} lines
+ */
+export const assertCodePaneSearchMatchesCount = (lines) => {
+ getCodeEditor().within(() =>
+ cy.get('.cm-searching').should('have.length', lines)
+ );
+};
diff --git a/src/Playroom/CodeEditor/CodeEditor.css.ts b/src/Playroom/CodeEditor/CodeEditor.css.ts
index e67ed1bb..8078b632 100644
--- a/src/Playroom/CodeEditor/CodeEditor.css.ts
+++ b/src/Playroom/CodeEditor/CodeEditor.css.ts
@@ -1,5 +1,6 @@
-import { style, globalStyle, keyframes } from '@vanilla-extract/css';
+import { style, globalStyle, keyframes, createVar } from '@vanilla-extract/css';
import { vars, colorPaletteVars, sprinkles } from '../sprinkles.css';
+import { toolbarItemSize } from '../ToolbarItem/ToolbarItem.css';
const minimumLineNumberWidth = '50px';
@@ -224,3 +225,100 @@ globalStyle('.cm-s-neo .cm-variable', {
globalStyle('.cm-s-neo .cm-number', {
color: colorPaletteVars.code.number,
});
+
+globalStyle('.CodeMirror-dialog', {
+ paddingLeft: vars.space.xlarge,
+ paddingRight: vars.space.xlarge,
+ minHeight: toolbarItemSize,
+ borderBottom: `1px solid ${colorPaletteVars.border.standard}`,
+ display: 'flex',
+ alignItems: 'center',
+});
+
+const searchOffset = createVar();
+globalStyle('.CodeMirror-scroll', {
+ transform: `translateY(${searchOffset})`,
+ transition: vars.transition.fast,
+});
+
+globalStyle('.dialog-opened .CodeMirror-scroll', {
+ vars: {
+ [searchOffset]: `${toolbarItemSize}px`,
+ },
+});
+
+globalStyle('.dialog-opened .CodeMirror-lines', {
+ paddingBottom: searchOffset,
+});
+
+globalStyle('.CodeMirror-dialog input', {
+ font: vars.font.scale.large,
+ fontFamily: vars.font.family.code,
+ height: vars.touchableSize,
+ flexGrow: 1,
+});
+
+globalStyle('.CodeMirror-search-hint', {
+ display: 'none',
+});
+
+globalStyle('.CodeMirror-search-label', {
+ display: 'flex',
+ alignItems: 'center',
+ minHeight: vars.touchableSize,
+ font: vars.font.scale.large,
+ fontFamily: vars.font.family.code,
+});
+
+globalStyle('.CodeMirror-search-field', {
+ paddingLeft: vars.space.xlarge,
+});
+
+globalStyle('label.CodeMirror-search-label', {
+ flexGrow: 1,
+});
+
+globalStyle('.dialog-opened.cm-s-neo .CodeMirror-selected', {
+ background: colorPaletteVars.background.search,
+});
+
+globalStyle('.cm-overlay.cm-searching', {
+ paddingTop: 2,
+ paddingBottom: 2,
+ background: colorPaletteVars.background.selection,
+});
+
+globalStyle('.CodeMirror-dialog button:first-of-type', {
+ marginLeft: vars.space.xlarge,
+});
+
+globalStyle('.CodeMirror-dialog button', {
+ appearance: 'none',
+ font: vars.font.scale.standard,
+ fontFamily: vars.font.family.standard,
+ marginLeft: vars.space.medium,
+ paddingTop: vars.space.medium,
+ paddingBottom: vars.space.medium,
+ paddingLeft: vars.space.large,
+ paddingRight: vars.space.large,
+ alignSelf: 'center',
+ display: 'block',
+ background: 'none',
+ borderRadius: vars.radii.large,
+ cursor: 'pointer',
+ border: '1px solid currentColor',
+});
+
+globalStyle('.CodeMirror-dialog button:focus', {
+ color: colorPaletteVars.foreground.accent,
+ boxShadow: colorPaletteVars.shadows.focus,
+ outline: 'none',
+});
+
+globalStyle('.CodeMirror-dialog button:focus:hover', {
+ background: colorPaletteVars.background.selection,
+});
+
+globalStyle('.CodeMirror-dialog button:hover', {
+ background: colorPaletteVars.background.transparent,
+});
diff --git a/src/Playroom/CodeEditor/CodeEditor.tsx b/src/Playroom/CodeEditor/CodeEditor.tsx
index 3ef4b57e..3c869741 100644
--- a/src/Playroom/CodeEditor/CodeEditor.tsx
+++ b/src/Playroom/CodeEditor/CodeEditor.tsx
@@ -2,6 +2,7 @@ import { useRef, useContext, useEffect, useCallback } from 'react';
import { useDebouncedCallback } from 'use-debounce';
import type { Editor } from 'codemirror';
import 'codemirror/lib/codemirror.css';
+import 'codemirror/addon/dialog/dialog.css';
import 'codemirror/theme/neo.css';
import {
@@ -26,6 +27,10 @@ import 'codemirror/addon/edit/closetag';
import 'codemirror/addon/edit/closebrackets';
import 'codemirror/addon/hint/show-hint';
import 'codemirror/addon/hint/xml-hint';
+import 'codemirror/addon/dialog/dialog';
+import 'codemirror/addon/search/jump-to-line';
+import 'codemirror/addon/search/search';
+import 'codemirror/addon/search/searchcursor';
import 'codemirror/addon/selection/active-line';
import 'codemirror/addon/fold/foldcode';
import 'codemirror/addon/fold/foldgutter';
@@ -117,6 +122,17 @@ export const CodeEditor = ({ code, onChange, previewCode, hints }: Props) => {
e.preventDefault();
dispatch({ type: 'toggleToolbar', payload: { panel: 'snippets' } });
}
+
+ // Prevent browser keyboard shortcuts when the search/replace input is focused
+ if (
+ cmdOrCtrl &&
+ document.activeElement?.classList.contains(
+ 'CodeMirror-search-field'
+ ) &&
+ e.key === 'f'
+ ) {
+ e.preventDefault();
+ }
}
};
@@ -259,6 +275,14 @@ export const CodeEditor = ({ code, onChange, previewCode, hints }: Props) => {
[`${keymapModifierKey}-D`]: selectNextOccurrence,
[`Shift-${keymapModifierKey}-,`]: wrapInTag,
[`${keymapModifierKey}-/`]: toggleComment,
+ [`${keymapModifierKey}-F`]: 'findPersistent',
+ [`${keymapModifierKey}-Alt-F`]: 'replace',
+ [`${keymapModifierKey}-G`]: 'jumpToLine',
+ ['Alt-G']: false, // override default keybinding
+ ['Alt-F']: false, // override default keybinding
+ ['Shift-Ctrl-R']: false, // override default keybinding
+ ['Cmd-Option-F']: false, // override default keybinding
+ ['Shift-Cmd-Option-F']: false, // override default keybinding
[`Shift-${keymapModifierKey}-C`]: () => {
dispatch({
type: 'copyToClipboard',
diff --git a/src/Playroom/SettingsPanel/SettingsPanel.tsx b/src/Playroom/SettingsPanel/SettingsPanel.tsx
index ec81f9b5..3cd70809 100644
--- a/src/Playroom/SettingsPanel/SettingsPanel.tsx
+++ b/src/Playroom/SettingsPanel/SettingsPanel.tsx
@@ -25,12 +25,15 @@ const getKeyBindings = () => {
const shiftKeySymbol = isMac() ? '⇧' : 'Shift';
return {
+ Find: [metaKeySymbol, 'F'],
+ 'Find and replace': [metaKeySymbol, altKeySymbol, 'F'],
'Toggle comment': [metaKeySymbol, '/'],
'Wrap selection in tag': [metaKeySymbol, shiftKeySymbol, ','],
'Format code': [metaKeySymbol, 'S'],
'Insert snippet': [metaKeySymbol, 'K'],
'Copy Playroom link': [metaKeySymbol, shiftKeySymbol, 'C'],
'Select next occurrence': [metaKeySymbol, 'D'],
+ 'Jump to line number': [metaKeySymbol, 'G'],
'Swap line up': [altKeySymbol, '↑'],
'Swap line down': [altKeySymbol, '↓'],
'Duplicate line up': [shiftKeySymbol, altKeySymbol, '↑'],
diff --git a/src/Playroom/palettes.ts b/src/Playroom/palettes.ts
index 4f33ecef..98d4a9c1 100644
--- a/src/Playroom/palettes.ts
+++ b/src/Playroom/palettes.ts
@@ -49,7 +49,8 @@ export const light = {
neutral: originalPalette.gray2,
surface: originalPalette.white,
body: originalPalette.gray1,
- selection: originalPalette.blue0,
+ selection: transparentize(0.85, originalPalette.blue1),
+ search: darken(0.15, originalPalette.blue0),
},
border: {
standard: originalPalette.gray2,
@@ -143,14 +144,15 @@ export const dark = {
positive: seekPalette.mint[500],
},
background: {
- transparent: 'rgba(0, 0, 0, .15)',
+ transparent: 'rgba(255, 255, 255, .07)',
accent: seekPalette.blue[500],
positive: mix(0.6, seekPalette.grey[900], seekPalette.mint[500]),
critical: mix(0.7, seekPalette.grey[900], seekPalette.red[600]),
neutral: seekPalette.grey[800],
surface: seekPalette.grey[900],
body: darken(0.03, seekPalette.grey[900]),
- selection: transparentize(0.85, seekPalette.blue[600]),
+ selection: transparentize(0.75, seekPalette.blue[600]),
+ search: transparentize(0.25, seekPalette.blue[600]),
},
border: {
standard: seekPalette.grey[800],
diff --git a/src/Playroom/sprinkles.css.ts b/src/Playroom/sprinkles.css.ts
index 6914f517..8c8659b3 100644
--- a/src/Playroom/sprinkles.css.ts
+++ b/src/Playroom/sprinkles.css.ts
@@ -75,6 +75,7 @@ export const colorPaletteVars = createThemeContract({
surface: null,
body: null,
selection: null,
+ search: null,
},
border: {
standard: null,