diff --git a/package-lock.json b/package-lock.json index 7751925f6..00abd94e8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8087,9 +8087,9 @@ } }, "node_modules/@mongodb-js/compass-components": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@mongodb-js/compass-components/-/compass-components-1.7.0.tgz", - "integrity": "sha512-QLQNVmVI2PKEVfYEQq6QuLsSx5xm55sZOZhujeAvXLJJ58qxfRHQG+PoT66tuwGh9TvV2wnLVUiImurl+10VyA==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@mongodb-js/compass-components/-/compass-components-1.8.0.tgz", + "integrity": "sha512-Ech1WELSdcY2mG628AFxSSP7MmODfcaXO+dx1JwD3cYSz8g/O1e9cRhBr5DV5fSzuA9GG5ljBnEX3tCg6zrPuw==", "dev": true, "dependencies": { "@dnd-kit/core": "^6.0.7", @@ -8138,8 +8138,8 @@ "@react-stately/tooltip": "^3.0.5", "bson": "^5.0.1", "focus-trap-react": "^8.4.2", - "hadron-document": "^8.1.2", - "hadron-type-checker": "^7.0.1", + "hadron-document": "^8.2.0", + "hadron-type-checker": "^7.0.2", "is-electron-renderer": "^2.0.1", "lodash": "^4.17.21", "polished": "^4.2.2", @@ -8154,9 +8154,9 @@ } }, "node_modules/@mongodb-js/compass-editor": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/@mongodb-js/compass-editor/-/compass-editor-0.6.0.tgz", - "integrity": "sha512-s0JbFPhpaVhbX4wyohHlvbtAedXxmc+Y/ED1XdPZNXbiOebw2IGRSl7iL0ru/5VbRFq/cWjjav3i9krSVYCYCQ==", + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@mongodb-js/compass-editor/-/compass-editor-0.7.0.tgz", + "integrity": "sha512-wCY8MUlmVYGvwARPaV3DhhMlUTzxNsX4rAFGQQVqp3mZpO/gJjcpoLLICt0lLLTqLGT4VMKvGuZs9mS9ngVRoA==", "dev": true, "dependencies": { "@codemirror/autocomplete": "^6.4.0", @@ -8168,7 +8168,7 @@ "@codemirror/state": "^6.1.4", "@codemirror/view": "^6.7.1", "@lezer/highlight": "^1.1.3", - "@mongodb-js/compass-components": "^1.7.0", + "@mongodb-js/compass-components": "^1.8.0", "@mongodb-js/mongodb-constants": "^0.2.1", "ace-builds": "^1.11.2", "polished": "^4.2.2", @@ -17438,15 +17438,15 @@ } }, "node_modules/hadron-document": { - "version": "8.1.2", - "resolved": "https://registry.npmjs.org/hadron-document/-/hadron-document-8.1.2.tgz", - "integrity": "sha512-Yv/trlfDp3zgEATAsqFiI00Trx+0M+uSRzg89OPYU4qy94YJMxDC2fY+Y9cvaJIY1ykgcwStLp5HKAJQlr3oLA==", + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/hadron-document/-/hadron-document-8.2.0.tgz", + "integrity": "sha512-RmbhQBQlxB5XVbeXhX4gEZmLbrRbo9y8bNX8tBTUMIUmsiIrpNxOj4GtlT8lA3xe+nyjY7AGrtplwpx+CpmVGA==", "dev": true, "dependencies": { "bson": "^5.0.1", "debug": "^4.2.0", "eventemitter3": "^4.0.0", - "hadron-type-checker": "^7.0.1", + "hadron-type-checker": "^7.0.2", "lodash.foreach": "^4.5.0", "lodash.isarray": "^4.0.0", "lodash.isequal": "^4.5.0", @@ -17455,9 +17455,9 @@ } }, "node_modules/hadron-type-checker": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/hadron-type-checker/-/hadron-type-checker-7.0.1.tgz", - "integrity": "sha512-oKd0akmTuZHtlwiGRfowtJApO1v/zuoYWXd1Q2HZYOSbyTXzBrBUmIrA0Z3AtfI4HNTCBQEuSZLCFisRHPHb1Q==", + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/hadron-type-checker/-/hadron-type-checker-7.0.2.tgz", + "integrity": "sha512-TB8UJ4gwoAgfTZ9m72/4mMzcbdxRdWOXzYuaIUZ5H/GmN33Nu0Y431sLy8qFokuuf4HPaX68soTkt8xLGJnKGQ==", "dev": true, "dependencies": { "bson": "^5.0.1", @@ -30353,8 +30353,8 @@ "@babel/plugin-proposal-class-properties": "^7.8.3", "@babel/preset-react": "^7.18.6", "@babel/preset-typescript": "^7.18.6", - "@mongodb-js/compass-components": "*", - "@mongodb-js/compass-editor": "*", + "@mongodb-js/compass-components": "^1.8.0", + "@mongodb-js/compass-editor": "^0.7.0", "@pmmmwh/react-refresh-webpack-plugin": "^0.5.8", "@types/numeral": "^2.0.2", "@types/react": "^16.9.17", @@ -37934,9 +37934,9 @@ } }, "@mongodb-js/compass-components": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@mongodb-js/compass-components/-/compass-components-1.7.0.tgz", - "integrity": "sha512-QLQNVmVI2PKEVfYEQq6QuLsSx5xm55sZOZhujeAvXLJJ58qxfRHQG+PoT66tuwGh9TvV2wnLVUiImurl+10VyA==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@mongodb-js/compass-components/-/compass-components-1.8.0.tgz", + "integrity": "sha512-Ech1WELSdcY2mG628AFxSSP7MmODfcaXO+dx1JwD3cYSz8g/O1e9cRhBr5DV5fSzuA9GG5ljBnEX3tCg6zrPuw==", "dev": true, "requires": { "@dnd-kit/core": "^6.0.7", @@ -37985,8 +37985,8 @@ "@react-stately/tooltip": "^3.0.5", "bson": "^5.0.1", "focus-trap-react": "^8.4.2", - "hadron-document": "^8.1.2", - "hadron-type-checker": "^7.0.1", + "hadron-document": "^8.2.0", + "hadron-type-checker": "^7.0.2", "is-electron-renderer": "^2.0.1", "lodash": "^4.17.21", "polished": "^4.2.2", @@ -37997,9 +37997,9 @@ } }, "@mongodb-js/compass-editor": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/@mongodb-js/compass-editor/-/compass-editor-0.6.0.tgz", - "integrity": "sha512-s0JbFPhpaVhbX4wyohHlvbtAedXxmc+Y/ED1XdPZNXbiOebw2IGRSl7iL0ru/5VbRFq/cWjjav3i9krSVYCYCQ==", + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@mongodb-js/compass-editor/-/compass-editor-0.7.0.tgz", + "integrity": "sha512-wCY8MUlmVYGvwARPaV3DhhMlUTzxNsX4rAFGQQVqp3mZpO/gJjcpoLLICt0lLLTqLGT4VMKvGuZs9mS9ngVRoA==", "dev": true, "requires": { "@codemirror/autocomplete": "^6.4.0", @@ -38011,7 +38011,7 @@ "@codemirror/state": "^6.1.4", "@codemirror/view": "^6.7.1", "@lezer/highlight": "^1.1.3", - "@mongodb-js/compass-components": "^1.7.0", + "@mongodb-js/compass-components": "^1.8.0", "@mongodb-js/mongodb-constants": "^0.2.1", "ace-builds": "^1.11.2", "polished": "^4.2.2", @@ -38126,8 +38126,8 @@ "@babel/plugin-proposal-class-properties": "^7.8.3", "@babel/preset-react": "^7.18.6", "@babel/preset-typescript": "^7.18.6", - "@mongodb-js/compass-components": "*", - "@mongodb-js/compass-editor": "*", + "@mongodb-js/compass-components": "1.8.0", + "@mongodb-js/compass-editor": "0.7.0", "@mongosh/browser-runtime-core": "0.0.0-dev.0", "@mongosh/errors": "0.0.0-dev.0", "@mongosh/history": "0.0.0-dev.0", @@ -45918,15 +45918,15 @@ } }, "hadron-document": { - "version": "8.1.2", - "resolved": "https://registry.npmjs.org/hadron-document/-/hadron-document-8.1.2.tgz", - "integrity": "sha512-Yv/trlfDp3zgEATAsqFiI00Trx+0M+uSRzg89OPYU4qy94YJMxDC2fY+Y9cvaJIY1ykgcwStLp5HKAJQlr3oLA==", + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/hadron-document/-/hadron-document-8.2.0.tgz", + "integrity": "sha512-RmbhQBQlxB5XVbeXhX4gEZmLbrRbo9y8bNX8tBTUMIUmsiIrpNxOj4GtlT8lA3xe+nyjY7AGrtplwpx+CpmVGA==", "dev": true, "requires": { "bson": "^5.0.1", "debug": "^4.2.0", "eventemitter3": "^4.0.0", - "hadron-type-checker": "^7.0.1", + "hadron-type-checker": "^7.0.2", "lodash.foreach": "^4.5.0", "lodash.isarray": "^4.0.0", "lodash.isequal": "^4.5.0", @@ -45935,9 +45935,9 @@ } }, "hadron-type-checker": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/hadron-type-checker/-/hadron-type-checker-7.0.1.tgz", - "integrity": "sha512-oKd0akmTuZHtlwiGRfowtJApO1v/zuoYWXd1Q2HZYOSbyTXzBrBUmIrA0Z3AtfI4HNTCBQEuSZLCFisRHPHb1Q==", + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/hadron-type-checker/-/hadron-type-checker-7.0.2.tgz", + "integrity": "sha512-TB8UJ4gwoAgfTZ9m72/4mMzcbdxRdWOXzYuaIUZ5H/GmN33Nu0Y431sLy8qFokuuf4HPaX68soTkt8xLGJnKGQ==", "dev": true, "requires": { "bson": "^5.0.1", diff --git a/packages/browser-repl/src/components/ace-autocompleter-adapter.spec.ts b/packages/browser-repl/src/components/ace-autocompleter-adapter.spec.ts deleted file mode 100644 index e2fc1faf5..000000000 --- a/packages/browser-repl/src/components/ace-autocompleter-adapter.spec.ts +++ /dev/null @@ -1,87 +0,0 @@ -import sinon from 'sinon'; -import util from 'util'; -import { AceAutocompleterAdapter } from './ace-autocompleter-adapter'; -import { expect } from '../../testing/chai'; -import { Completion } from '@mongosh/browser-runtime-core'; - -async function testGetCompletions(adaptee, textBeforeCursor): Promise { - const completer = new AceAutocompleterAdapter(adaptee as any); - const getCompletions = util.promisify(completer.getCompletions.bind(completer)); - - const rows = textBeforeCursor.split('\n'); - const prefix = textBeforeCursor.split(/[\. ]/g).pop(); - - return await getCompletions( - null, - { - getLine: (i) => rows[i] - }, - { - row: rows.length - 1, - column: rows[rows.length - 1].length - }, - prefix - ); -} - -describe('AceAutocompleterAdapter', () => { - describe('getCompletions', () => { - it('calls adaptee.getCompletions with code', async() => { - const adaptee = { - getCompletions: sinon.spy(() => Promise.resolve([])) - }; - - await testGetCompletions(adaptee, 'text'); - - expect(adaptee.getCompletions).to.have.been.calledWith('text'); - }); - - it('calls adaptee.getCompletions with code till cursor', async() => { - const adaptee = { - getCompletions: sinon.spy(() => Promise.resolve([])) - }; - - await testGetCompletions(adaptee, 'some text'); - - expect(adaptee.getCompletions).to.have.been.calledWith('some text'); - }); - - it('passes dots', async() => { - const adaptee = { - getCompletions: sinon.spy(() => Promise.resolve([])) - }; - - await testGetCompletions(adaptee, 'some.text'); - - expect(adaptee.getCompletions).to.have.been.calledWith('some.text'); - }); - - it('only gets cursor line', async() => { - const adaptee = { - getCompletions: sinon.spy(() => Promise.resolve([])) - }; - - await testGetCompletions(adaptee, 'this is\nsome text'); - - expect(adaptee.getCompletions).to.have.been.calledWith('some text'); - }); - - it('converts the completions to the ace format', async() => { - const adaptee = { - getCompletions: sinon.spy(() => Promise.resolve([ - { - completion: 'something to.complete' - } - ])) - }; - - const completions = await testGetCompletions(adaptee, 'something to.compl'); - - expect(completions[0]).to.deep.equal({ - value: 'complete', - caption: 'complete' - }); - }); - }); -}); - diff --git a/packages/browser-repl/src/components/ace-autocompleter-adapter.ts b/packages/browser-repl/src/components/ace-autocompleter-adapter.ts deleted file mode 100644 index f426e0697..000000000 --- a/packages/browser-repl/src/components/ace-autocompleter-adapter.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { Autocompleter, Completion } from '@mongosh/browser-runtime-core'; - -interface AceCompletion { - caption: string; - value: string; -} - -/** - * @private - * - * Adapts an Autocompleter instance to comply with the ACE Editor - * interface. - */ -export class AceAutocompleterAdapter { - private adaptee: Autocompleter; - - constructor(adaptee: Autocompleter) { - this.adaptee = adaptee; - } - - getCompletions = ( - _editor: any, - session: any, - position: { row: number; column: number }, - prefix: string, - done: (err: Error | null, completions?: AceCompletion[]) => any): void => { - // ACE wont include '.' in the prefix, so we have to extract a new prefix - // including dots to be passed to the autocompleter. - const line = session.getLine(position.row) - .substring(0, position.column); - - - this.adaptee.getCompletions(line) - .then((completions) => { - done(null, completions.map( - this.adaptCompletion.bind(this, prefix, line) - )); - }) - .catch(done); - }; - - adaptCompletion = (prefix: string, line: string, completion: Completion): AceCompletion => { - // We convert the completion to the ACE editor format by taking only - // the last part. ie (db.coll1.find -> find) - const value = prefix + completion.completion.substring(line.length); - return { - caption: value, - value: value - }; - }; -} diff --git a/packages/browser-repl/src/components/editor.spec.tsx b/packages/browser-repl/src/components/editor.spec.tsx index 66f6ad489..c8f34ec16 100644 --- a/packages/browser-repl/src/components/editor.spec.tsx +++ b/packages/browser-repl/src/components/editor.spec.tsx @@ -1,199 +1,115 @@ import sinon from 'sinon'; -import React from 'react'; import { expect } from '../../testing/chai'; -import { mount } from '../../testing/enzyme'; -import { Editor } from './editor'; +import { createCommands } from './editor'; +import { Command } from '@mongodb-js/compass-editor'; describe('', () => { - const getAceEditorInstance = (wrapper): any => { - const aceEditor = wrapper.find(Editor); - return aceEditor.instance().editor as any; - }; - - const execCommandBoundTo = ( - aceEditor: any, - key: { win: string; mac: string } - ): void => { - const commands = Object.values(aceEditor.commands.commands); - const command: any = commands.find(({ name, bindKey }) => { - if (!bindKey) { - return false; - } - if (name === 'gotoline') { - // Ignore gotoline - our command overrides. - return false; + let commandSpies: Parameters[number]; + let commands: Record; + + const sandbox = sinon.createSandbox(); + + function mockContext( + selection: { from: number; to?: number; empty?: boolean } = { from: 0 }, + line?: { from?: number; to?: number }, + docLength?: number + ): any { + selection.to = selection.to || selection.from; + line = line || { from: selection.from }; + line.to = line.to || selection.to; + return { + state: { + selection: { + main: { + from: selection.from, + to: selection.to, + empty: selection.empty || selection.from === selection.to + } + }, + doc: { + length: docLength || line.to + } + }, + lineBlockAt() { + return line; } - - const { win, mac } = bindKey as { win: string; mac: string }; - return win === key.win && mac === key.mac; - }); - - if (!command) { - throw new Error(`No command bound to ${key}.`); - } - - aceEditor.execCommand(command.name); - }; - - it('allows to set the value', () => { - const wrapper = mount(); - - const aceEditor = getAceEditorInstance(wrapper); - expect(aceEditor.getValue()).to.equal('some value'); - }); - - it('is not readonly by default', () => { - const wrapper = mount(); - const aceEditor = getAceEditorInstance(wrapper); - - expect(aceEditor.getOption('readOnly')).to.equal(false); + }; + } + + beforeEach(function() { + commandSpies = { + onEnter: sinon.spy(), + onArrowUpOnFirstLine: sinon.stub().resolves(false), + onArrowDownOnLastLine: sinon.stub().resolves(false), + onClearCommand: sinon.spy(), + onSigInt: sinon.spy() + }; + commands = Object.fromEntries( + createCommands(commandSpies).map(({ key, run }) => { + return [key, run]; + }) + ); }); - it('allows to set the editor as readonly when operationInProgress is true', () => { - const wrapper = mount(); - const aceEditor = getAceEditorInstance(wrapper); - - expect(aceEditor.getOption('readOnly')).to.equal(true); - }); - - it('calls onChange when the content changes', () => { - const spy = sinon.spy(); - const wrapper = mount(); - - const aceEditor = getAceEditorInstance(wrapper); - expect(spy).not.to.have.been.called; - - aceEditor.setValue('value'); - expect(spy).to.have.been.calledWith('value'); + afterEach(function() { + sandbox.reset(); }); - it('calls onEnter when enter is pressed', () => { - const spy = sinon.spy(); - const wrapper = mount(); - - const aceEditor = getAceEditorInstance(wrapper); - expect(spy).not.to.have.been.called; - - execCommandBoundTo(aceEditor, { - win: 'Return', - mac: 'Return' + describe('commands', function() { + it('calls onEnter when enter is pressed', function() { + // eslint-disable-next-line new-cap + expect(commands.Enter?.({} as any)).to.eq(true); + expect(commandSpies.onEnter).to.have.been.calledOnce; }); - expect(spy).to.have.been.calledOnce; - }); - - it('calls onClearCommand when command/ctrl+L is pressed', () => { - const spy = sinon.spy(); - const wrapper = mount(); - const aceEditor = getAceEditorInstance(wrapper); - - expect(spy).not.to.have.been.called; - execCommandBoundTo(aceEditor, { - win: 'Ctrl-L', - mac: 'Command-L' + it('calls onClearCommand when command/ctrl+L is pressed', function() { + // eslint-disable-next-line new-cap + expect(commands['Mod-l']?.({} as any)).to.eq(true); + expect(commandSpies.onClearCommand).to.have.been.calledOnce; }); - expect(spy).to.have.been.calledOnce; - }); - it('calls onArrowUpOnFirstLine when arrow up is pressed and cursor on fisrt row', () => { - const spy = sinon.spy(); - const wrapper = mount(); - - const aceEditor = getAceEditorInstance(wrapper); - - expect(spy).not.to.have.been.called; - execCommandBoundTo(aceEditor, { - win: 'Up', - mac: 'Up' + it('calls onArrowUpOnFirstLine when arrow up is pressed and cursor on fisrt row', function() { + // eslint-disable-next-line new-cap + expect(commands.ArrowUp?.(mockContext())).to.eq(true); + expect(commandSpies.onArrowUpOnFirstLine).to.have.been.calledOnce; }); - expect(spy).to.have.been.calledOnce; - }); - it('does not call onArrowUpOnFirstLine when arrow up is pressed and row > 0', () => { - const spy = sinon.spy(); - const wrapper = mount(); - - const aceEditor = getAceEditorInstance(wrapper); - aceEditor.setValue('row 0\nrow 1'); - aceEditor.moveCursorToPosition({ row: 1, column: 0 }); - aceEditor.clearSelection(); - - execCommandBoundTo(aceEditor, { - win: 'Up', - mac: 'Up' + it('does not call onArrowUpOnFirstLine when arrow up is pressed and row > 0', function() { + // eslint-disable-next-line new-cap + expect(commands.ArrowUp?.(mockContext({ from: 6 }, { from: 3 }))).to.eq( + false + ); + expect(commandSpies.onArrowUpOnFirstLine).to.not.have.been.called; }); - expect(spy).not.to.have.been.called; - }); - it('calls onArrowDownOnLastLine when arrow down is pressed and cursor on last row', () => { - const spy = sinon.spy(); - const wrapper = mount(); - - const aceEditor = getAceEditorInstance(wrapper); - aceEditor.setValue('row 0\nrow 1'); - aceEditor.moveCursorToPosition({ row: 1, column: 0 }); - aceEditor.clearSelection(); - - expect(spy).not.to.have.been.called; - execCommandBoundTo(aceEditor, { - win: 'Down', - mac: 'Down' + it('calls onArrowDownOnLastLine when arrow down is pressed and cursor on last row', function() { + // eslint-disable-next-line new-cap + expect(commands.ArrowDown?.(mockContext())).to.eq(true); + expect(commandSpies.onArrowDownOnLastLine).to.have.been.called; }); - expect(spy).to.have.been.calledOnce; - }); - - it('does not call onArrowDownOnLastLine when arrow down is pressed and cursor not on last row', () => { - const spy = sinon.spy(); - const wrapper = mount(); - - const aceEditor = getAceEditorInstance(wrapper); - aceEditor.setValue('row 0\nrow 1'); - execCommandBoundTo(aceEditor, { - win: 'Down', - mac: 'Down' + it('does not call onArrowDownOnLastLine when arrow down is pressed and cursor not on last row', function() { + expect( + // eslint-disable-next-line new-cap + commands.ArrowDown?.(mockContext({ from: 0 }, { from: 0, to: 10 }, 20)) + ).to.eq(false); + expect(commandSpies.onArrowDownOnLastLine).to.not.have.been.called; }); - expect(spy).not.to.have.been.called; - }); - - it('does not call onArrowUpOnFirstLine if text is selected', () => { - const spy = sinon.spy(); - const wrapper = mount(); - - const aceEditor = getAceEditorInstance(wrapper); - aceEditor.setValue('text'); - aceEditor.selectAll(); - execCommandBoundTo(aceEditor, { - win: 'Up', - mac: 'Up' + it('does not call onArrowUpOnFirstLine if text is selected', function() { + expect( + // eslint-disable-next-line new-cap + commands.ArrowUp?.(mockContext({ from: 0, to: 1, empty: false })) + ).to.eq(false); + expect(commandSpies.onArrowUpOnFirstLine).to.not.have.been.called; }); - expect(spy).not.to.have.been.called; - }); - - it('does not call onArrowDownOnLastLine if text is selected', () => { - const spy = sinon.spy(); - const wrapper = mount(); - - const aceEditor = getAceEditorInstance(wrapper); - aceEditor.setValue('text'); - aceEditor.selectAll(); - execCommandBoundTo(aceEditor, { - win: 'Down', - mac: 'Down' + it('does not call onArrowDownOnLastLine if text is selected', function() { + expect( + // eslint-disable-next-line new-cap + commands.ArrowDown?.(mockContext({ from: 0, to: 1, empty: false })) + ).to.eq(false); + expect(commandSpies.onArrowDownOnLastLine).to.not.have.been.called; }); - expect(spy).not.to.have.been.called; - }); - - it('sets the input ref for the editor', () => { - const spy = sinon.spy(); - const wrapper = mount(); - - const aceEditor = getAceEditorInstance(wrapper); - - expect(spy).to.have.been.calledOnce; - expect(spy.args[0][0]).to.equal(aceEditor); }); }); - diff --git a/packages/browser-repl/src/components/editor.tsx b/packages/browser-repl/src/components/editor.tsx index 4aee33cdc..dd4c692c5 100644 --- a/packages/browser-repl/src/components/editor.tsx +++ b/packages/browser-repl/src/components/editor.tsx @@ -1,167 +1,194 @@ import React, { Component } from 'react'; import { css } from '@mongodb-js/compass-components'; -import { InlineEditor, AceEditor as IAceEditor } from '@mongodb-js/compass-editor'; +import { CodemirrorInlineEditor } from '@mongodb-js/compass-editor'; +import type { EditorRef, Command } from '@mongodb-js/compass-editor'; import { Autocompleter } from '@mongosh/browser-runtime-core'; -import { AceAutocompleterAdapter } from './ace-autocompleter-adapter'; + +// TODO: update compass editor and use exported type +type Completer = React.ComponentProps< + typeof CodemirrorInlineEditor +>['completer']; const noop = (): void => {}; -const editor = css({ - lineHeight: '24px !important', - marginLeft: '-4px', +export const editorStyles = css({ + '& .cm-content': { + paddingTop: 0, + paddingBottom: 0 + }, + '& .cm-line': { + paddingLeft: 1, + paddingRight: 1 + } }); +function cursorDocEnd({ state, dispatch }: any) { + dispatch( + state.update({ + selection: { anchor: state.doc.length }, + scrollIntoView: true, + userEvent: 'select' + }) + ); + return true; +} + interface EditorProps { autocompleter?: Autocompleter; - moveCursorToTheEndOfInput: boolean; onEnter(): void | Promise; - onArrowUpOnFirstLine(): void | Promise; - onArrowDownOnLastLine(): void | Promise; + onArrowUpOnFirstLine(): Promise; + onArrowDownOnLastLine(): Promise; onChange(value: string): void | Promise; onClearCommand(): void | Promise; onSigInt(): Promise; operationInProgress: boolean; value: string; - onEditorLoad?: (editor: IAceEditor) => void; + editorRef?: (editor: EditorRef | null) => void; +} + +export function createCommands( + callbacks: Pick< + EditorProps, + | 'onEnter' + | 'onArrowDownOnLastLine' + | 'onArrowUpOnFirstLine' + | 'onClearCommand' + | 'onSigInt' + > +): Command[] { + return [ + { + key: 'Enter', + run: () => { + void callbacks.onEnter(); + return true; + }, + preventDefault: true + }, + { + key: 'ArrowUp', + run: (context) => { + const selection = context.state.selection.main; + if (!selection.empty) { + return false; + } + const lineBlock = context.lineBlockAt(selection.from); + const isFirstLine = lineBlock.from === 0; + if (!isFirstLine) { + return false; + } + void callbacks.onArrowUpOnFirstLine().then((updated) => { + if (updated) { + cursorDocEnd(context); + } + }); + return true; + }, + preventDefault: true + }, + { + key: 'ArrowDown', + run: (context) => { + const selection = context.state.selection.main; + if (!selection.empty) { + return false; + } + const lineBlock = context.lineBlockAt(selection.from); + const isLastLine = lineBlock.to === context.state.doc.length; + if (!isLastLine) { + return false; + } + void callbacks.onArrowDownOnLastLine().then((updated) => { + if (updated) { + cursorDocEnd(context); + } + }); + return true; + }, + preventDefault: true + }, + { + key: 'Mod-l', + run: () => { + void callbacks.onClearCommand(); + return true; + }, + preventDefault: true + }, + { + key: 'Ctrl-c', + run: () => { + void callbacks.onSigInt(); + return true; + }, + preventDefault: true + } + ]; } export class Editor extends Component { static defaultProps = { onEnter: noop, - onArrowUpOnFirstLine: noop, - onArrowDownOnLastLine: noop, + onArrowUpOnFirstLine: () => Promise.resolve(false), + onArrowDownOnLastLine: () => Promise.resolve(false), onChange: noop, onClearCommand: noop, onSigInt: noop, operationInProgress: false, - value: '', - moveCursorToTheEndOfInput: false + value: '' }; - private editor: any; - private visibleCursorDisplayStyle = ''; - - private onEditorLoad = (editor: IAceEditor): void => { - this.editor = editor; - this.visibleCursorDisplayStyle = this.editor.renderer.$cursorLayer.element.style.display; - - editor.commands.on('afterExec', (e: any) => { - if ( - // Only suggest autocomplete if autocompleter was set - this.autocompleter && - e.command.name === 'insertstring' && - /^[\w.]$/.test(e.args) - ) { - this.editor.execCommand('startAutocomplete'); - } - }); - - // eslint-disable-next-line chai-friendly/no-unused-expressions - this.props.onEditorLoad?.(editor); - }; + private commands: Command[]; - private autocompleter: AceAutocompleterAdapter | null = null; + private autocompleter: Completer; private editorId: number = Date.now(); constructor(props: EditorProps) { super(props); - if (this.props.autocompleter) { - this.autocompleter = new AceAutocompleterAdapter( - this.props.autocompleter - ); - } - } - - componentDidUpdate(prevProps: EditorProps): void { - if (prevProps.operationInProgress !== this.props.operationInProgress) { - if (this.props.operationInProgress) { - this.hideCursor(); - } else { - this.showCursor(); + this.autocompleter = (context) => { + if (!this.props.autocompleter?.getCompletions) { + return null; } - } - } - componentWillUnmount(): void { - this.autocompleter = null; - } - - private hideCursor(): void { - this.editor.renderer.$cursorLayer.element.style.display = 'none'; - } - - private showCursor(): void { - this.editor.renderer.$cursorLayer.element.style.display = this.visibleCursorDisplayStyle; - } - - render(): JSX.Element { - return ( { - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.props.onEnter(); + const line = context.state.doc.lineAt(context.pos); + + return this.props.autocompleter.getCompletions(line.text).then( + (completions) => { + if (completions && completions.length > 0) { + return { + from: line.from, + options: completions.map(({ completion }) => { + return { label: completion }; + }) + }; } + return null; }, - { - name: 'arrowUpOnFirstLine', - bindKey: { win: 'Up', mac: 'Up' }, - exec: (): void => { - const selectionRange = this.editor.getSelectionRange(); - if (!selectionRange.isEmpty() || selectionRange.start.row !== 0) { - return this.editor.selection.moveCursorUp(); - } - - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.props.onArrowUpOnFirstLine(); - } - }, - { - name: 'arrowDownOnLastLine', - bindKey: { win: 'Down', mac: 'Down' }, - exec: (): void => { - const selectionRange = this.editor.getSelectionRange(); - const lastRowIndex = this.editor.session.getLength() - 1; - - if (!selectionRange.isEmpty() || selectionRange.start.row !== lastRowIndex) { - return this.editor.selection.moveCursorDown(); - } + () => null + ); + }; + this.commands = createCommands(this.props); + } - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.props.onArrowDownOnLastLine(); - } - }, - { - name: 'clearShell', - bindKey: { win: 'Ctrl-L', mac: 'Command-L' }, - exec: this.props.onClearCommand - }, - { - name: 'SIGINT', - bindKey: { win: 'Ctrl-C', mac: 'Ctrl-C' }, - exec: this.props.onSigInt, - // Types don't have it but it exists - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - readOnly: true, - } - ]} - maxLines={Infinity} - editorProps={{ - $blockScrolling: Infinity - }} - />); + render(): JSX.Element { + return ( + + ); } } diff --git a/packages/browser-repl/src/components/shell-input.tsx b/packages/browser-repl/src/components/shell-input.tsx index 6d28e4e46..3e980fb94 100644 --- a/packages/browser-repl/src/components/shell-input.tsx +++ b/packages/browser-repl/src/components/shell-input.tsx @@ -1,10 +1,10 @@ import React, { Component } from 'react'; import { Icon, css } from '@mongodb-js/compass-components'; import { Autocompleter } from '@mongosh/browser-runtime-core'; +import type { EditorRef } from '@mongodb-js/compass-editor'; import { Editor } from './editor'; import ShellLoader from './shell-loader'; import { LineWithIcon } from './utils/line-with-icon'; -import type { AceEditor as IAceEditor } from '@mongodb-js/compass-editor'; const shellInput = css({ @@ -19,8 +19,8 @@ interface ShellInputProps { onInput?(code: string): void | Promise; operationInProgress?: boolean; prompt?: string; - onEditorLoad?: (editor: IAceEditor) => void; onSigInt?(): Promise; + editorRef?: (editor: EditorRef | null) => void; } interface ShellInputState { @@ -34,7 +34,6 @@ export class ShellInput extends Component { readOnly: false }; - private editor: IAceEditor | null = null; private historyNavigationEntries: string[] = []; private historyNavigationIndex = 0; @@ -66,37 +65,43 @@ export class ShellInput extends Component { this.setState({ currentValue: value }); }; - private syncCurrentValueWithHistoryNavigation(): void { + private syncCurrentValueWithHistoryNavigation(cb: (updated: boolean) => void): void { const value = this.historyNavigationEntries[this.historyNavigationIndex]; if (value === undefined) { - return; + return cb(false); } this.setState({ currentValue: value }, () => { - // eslint-disable-next-line chai-friendly/no-unused-expressions - this.editor?.navigateFileEnd(); + cb(true); }); } - private historyBack = (): void => { - if (this.historyNavigationIndex >= this.historyNavigationEntries.length - 1) { - return; - } + private historyBack = (): Promise => { + return new Promise(resolve => { + if ( + this.historyNavigationIndex >= + this.historyNavigationEntries.length - 1 + ) { + return resolve(false); + } - this.historyNavigationIndex++; + this.historyNavigationIndex++; - this.syncCurrentValueWithHistoryNavigation(); + this.syncCurrentValueWithHistoryNavigation(resolve); + }); }; - private historyNext = (): void => { - if (this.historyNavigationIndex <= 0) { - return; - } + private historyNext = (): Promise => { + return new Promise(resolve => { + if (this.historyNavigationIndex <= 0) { + return resolve(false); + } - this.historyNavigationIndex--; + this.historyNavigationIndex--; - this.syncCurrentValueWithHistoryNavigation(); + this.syncCurrentValueWithHistoryNavigation(resolve); + }); }; private onEnter = async(): Promise => { @@ -139,11 +144,7 @@ export class ShellInput extends Component { onChange={this.onChange} onEnter={this.onEnter} onClearCommand={this.props.onClearCommand} - onEditorLoad={(editor) => { - this.editor = editor; - // eslint-disable-next-line chai-friendly/no-unused-expressions - this.props.onEditorLoad?.(editor); - }} + editorRef={this.props.editorRef} value={this.state.currentValue} operationInProgress={this.props.operationInProgress} onSigInt={this.props.onSigInt} diff --git a/packages/browser-repl/src/components/shell.tsx b/packages/browser-repl/src/components/shell.tsx index 44110888d..321a11a98 100644 --- a/packages/browser-repl/src/components/shell.tsx +++ b/packages/browser-repl/src/components/shell.tsx @@ -1,5 +1,5 @@ import React, { Component } from 'react'; -import { AceEditor as IAceEditor } from '@mongodb-js/compass-editor'; +import type { EditorRef } from '@mongodb-js/compass-editor'; import { css, ThemeProvider, Theme, palette, fontFamilies } from '@mongodb-js/compass-components'; import type { Runtime } from '@mongosh/browser-runtime-core'; import { changeHistory } from '@mongosh/history'; @@ -33,7 +33,8 @@ const shellContainer = css({ margin: 0, fontSize: 'inherit', borderRadius: 0, - color: 'inherit' + color: 'inherit', + tabSize: 2, } }); @@ -122,7 +123,7 @@ export class Shell extends Component { }; private shellInputElement: HTMLElement | null = null; - private editor?: IAceEditor; + private editor?: EditorRef | null = null; private onFinishPasswordPrompt: ((input: string) => void) = noop; private onCancelPasswordPrompt: (() => void) = noop; @@ -333,10 +334,13 @@ export class Shell extends Component { } }; + private setEditor = (editor: any | null) => { + this.editor = editor; + }; + private focusEditor = (): void => { - if (this.editor) { - this.editor.focus(); - } + // eslint-disable-next-line chai-friendly/no-unused-expressions + this.editor?.focus(); }; private onSigInt = (): Promise => { @@ -369,9 +373,7 @@ export class Shell extends Component { onClearCommand={this.onClearCommand} onInput={this.onInput} operationInProgress={this.state.operationInProgress} - onEditorLoad={(editor) => { - this.editor = editor; - }} + editorRef={this.setEditor} onSigInt={this.onSigInt} /> ); diff --git a/packages/browser-repl/src/components/utils/line-with-icon.tsx b/packages/browser-repl/src/components/utils/line-with-icon.tsx index bc93245ff..d87e2f781 100644 --- a/packages/browser-repl/src/components/utils/line-with-icon.tsx +++ b/packages/browser-repl/src/components/utils/line-with-icon.tsx @@ -16,7 +16,7 @@ const lineWithIconIcon = css({ const lineWithIconContent = css({ flex: 1, - overflowX: 'auto' + overflowX: 'auto', }); interface LineWithIconProps { diff --git a/packages/browser-repl/src/components/utils/syntax-highlight.spec.tsx b/packages/browser-repl/src/components/utils/syntax-highlight.spec.tsx index 2544b782a..a5a401dc0 100644 --- a/packages/browser-repl/src/components/utils/syntax-highlight.spec.tsx +++ b/packages/browser-repl/src/components/utils/syntax-highlight.spec.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { SyntaxHighlight as CompassSyntaxHighlight } from '@mongodb-js/compass-editor'; +import { CodemirrorInlineEditor } from '@mongodb-js/compass-editor'; import { expect } from '../../../testing/chai'; import { mount } from '../../../testing/enzyme'; import { SyntaxHighlight } from './syntax-highlight'; @@ -14,16 +14,13 @@ describe('', () => { it('renders Code', () => { wrapper = mount(); - expect(wrapper.find(CompassSyntaxHighlight)).to.have.lengthOf(1); + expect(wrapper.find(CodemirrorInlineEditor)).to.have.lengthOf(1); }); it('passes code to Code', () => { wrapper = mount(); - expect(wrapper.find(CompassSyntaxHighlight).prop('text')).to.equal('some code'); - }); - - it('uses javascript as language', () => { - wrapper = mount(); - expect(wrapper.find(CompassSyntaxHighlight).prop('language')).to.equal('javascript'); + expect(wrapper.find(CodemirrorInlineEditor).prop('initialText')).to.equal( + 'some code' + ); }); }); diff --git a/packages/browser-repl/src/components/utils/syntax-highlight.tsx b/packages/browser-repl/src/components/utils/syntax-highlight.tsx index 518dc7831..6732a9c54 100644 --- a/packages/browser-repl/src/components/utils/syntax-highlight.tsx +++ b/packages/browser-repl/src/components/utils/syntax-highlight.tsx @@ -1,14 +1,7 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; -import { css } from '@mongodb-js/compass-components'; -import { SyntaxHighlight as CompassSyntaxHighlight } from '@mongodb-js/compass-editor'; - -const syntaxHighlightStyles = css({ - lineHeight: '24px', - '& .cm-scroller': { - lineHeight: '24px' - } -}); +import { CodemirrorInlineEditor } from '@mongodb-js/compass-editor'; +import { editorStyles } from '../editor'; interface SyntaxHighlightProps { code: string; @@ -21,12 +14,14 @@ export class SyntaxHighlight extends Component { render(): JSX.Element { return ( - ); } diff --git a/packages/browser-repl/src/iframe-runtime/iframe-runtime.ts b/packages/browser-repl/src/iframe-runtime/iframe-runtime.ts index 0b0879a31..b7b57b92f 100644 --- a/packages/browser-repl/src/iframe-runtime/iframe-runtime.ts +++ b/packages/browser-repl/src/iframe-runtime/iframe-runtime.ts @@ -65,7 +65,7 @@ export class IframeRuntime implements Runtime { const iframe = this.container.firstElementChild as HTMLIFrameElement; this.iframe = iframe; this.readyPromise = new Promise((resolve) => { - iframe.onload = (): void => resolve(); + iframe.onload = () => void resolve(); }); document.body.appendChild(this.container);