Skip to content

Commit

Permalink
Add "Find", "Find and replace", and "Jump to line" keyboard shortcuts (
Browse files Browse the repository at this point in the history
…#343)

Co-authored-by: Michael Taranto <michael.taranto@gmail.com>
Co-authored-by: Adam Skoufis <askoufis@users.noreply.github.com>
  • Loading branch information
3 people committed Mar 7, 2024
1 parent 9208310 commit 94c75f8
Show file tree
Hide file tree
Showing 8 changed files with 362 additions and 10 deletions.
11 changes: 11 additions & 0 deletions .changeset/good-starfishes-share.md
@@ -0,0 +1,11 @@
---
'playroom': minor
---

Add "Find", "Find and replace", and "Jump to line" functionality.

Keybindings for these new commands are:

- `Cmd + F` / `Ctrl + F` - Find
- `Cmd + Option + F` / `Ctrl + Alt + F` - Find and replace
- `Cmd + G` / `Ctrl + G` - Jump to line
169 changes: 163 additions & 6 deletions cypress/e2e/keymaps.cy.js
Expand Up @@ -3,6 +3,7 @@ import {
typeCode,
assertCodePaneContains,
loadPlayroom,
cmdPlus,
selectNextWords,
selectNextLines,
selectNextCharacters,
Expand All @@ -11,14 +12,13 @@ import {
moveToEndOfLine,
moveBy,
moveByWords,
assertCodePaneSearchMatchesCount,
findInCode,
replaceInCode,
jumpToLine,
} from '../support/utils';
import { isMac } from '../../src/utils/formatting';

const cmdPlus = (keyCombo) => {
const platformSpecificKey = isMac() ? 'cmd' : 'ctrl';
return `${platformSpecificKey}+${keyCombo}`;
};

describe('Keymaps', () => {
describe('swapLine', () => {
beforeEach(() => {
Expand Down Expand Up @@ -350,6 +350,163 @@ describe('Keymaps', () => {
});
});

describe('find and replace', () => {
beforeEach(() => {
loadPlayroom(`
<div>First line</div>
<div>Second line</div>
<div>Third line</div>
`);
});

it('should find all occurrences of search term', () => {
findInCode('div');

assertCodePaneSearchMatchesCount(6);

cy.focused().type('{esc}');

typeCode('c');

assertCodePaneContains(dedent`
<c>First line</div>
<div>Second line</div>
<div>Third line</div>
`);
});

it('should replace and skip occurrences of search term correctly', () => {
replaceInCode('div', 'span');

// replace occurrence
cy.get('.CodeMirror-dialog button').contains('Yes').click();

assertCodePaneContains(dedent`
<span>First line</div>
<div>Second line</div>
<div>Third line</div>
`);

// ignore occurrence
cy.get('.CodeMirror-dialog button').contains('No').click();

assertCodePaneContains(dedent`
<span>First line</div>
<div>Second line</div>
<div>Third line</div>
`);

// replace occurrence
cy.get('.CodeMirror-dialog button').contains('Yes').click();

assertCodePaneContains(dedent`
<span>First line</div>
<span>Second line</div>
<div>Third line</div>
`);

// replace all remaining occurrences
cy.get('.CodeMirror-dialog button').contains('All').click();

assertCodePaneContains(dedent`
<span>First line</span>
<span>Second line</span>
<span>Third line</span>
`);

typeCode('c');

assertCodePaneContains(dedent`
<span>First line</span>
<span>Second line</spanc>
<span>Third line</span>
`);
});

it('should back out of replace correctly', () => {
replaceInCode('div');

typeCode('{esc}');

assertCodePaneContains(dedent`
<div>First line</div>
<div>Second line</div>
<div>Third line</div>
`);

typeCode('c');

assertCodePaneContains(dedent`
c<div>First line</div>
<div>Second line</div>
<div>Third line</div>
`);
});
});

describe('jump to line', () => {
beforeEach(() => {
loadPlayroom(`
<div>First line</div>
<div>Second line</div>
<div>Third line</div>
<div>Forth line</div>
<div>Fifth line</div>
<div>Sixth line</div>
<div>Seventh line</div>
`);
});

it('should jump to line number correctly', () => {
const line = 6;
jumpToLine(line);

typeCode('c');

assertCodePaneContains(dedent`
<div>First line</div>
<div>Second line</div>
<div>Third line</div>
<div>Forth line</div>
<div>Fifth line</div>
c<div>Sixth line</div>
<div>Seventh line</div>
`);

typeCode('{backspace}');

const nextLine = 2;
jumpToLine(nextLine);

typeCode('c');

assertCodePaneContains(dedent`
<div>First line</div>
c<div>Second line</div>
<div>Third line</div>
<div>Forth line</div>
<div>Fifth line</div>
<div>Sixth line</div>
<div>Seventh line</div>
`);
});

it('should jump to line and column number correctly', () => {
jumpToLine('6:10');
typeCode('a');

assertCodePaneContains(dedent`
<div>First line</div>
<div>Second line</div>
<div>Third line</div>
<div>Forth line</div>
<div>Fifth line</div>
<div>Sixtha line</div>
<div>Seventh line</div>
`);
});
});

describe('toggleComment', () => {
const blockStarter = `
<div>First line</div>
Expand Down Expand Up @@ -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(`
<div>
Expand Down
56 changes: 56 additions & 0 deletions cypress/support/utils.js
Expand Up @@ -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));

Expand Down Expand Up @@ -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)
);
};
100 changes: 99 additions & 1 deletion 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';

Expand Down Expand Up @@ -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,
});

0 comments on commit 94c75f8

Please sign in to comment.