diff --git a/apm/package.json b/apm/package.json index ba8d06f1d..7117147f1 100644 --- a/apm/package.json +++ b/apm/package.json @@ -6,6 +6,6 @@ "url": "https://github.com/atom/atom.git" }, "dependencies": { - "atom-package-manager": "1.18.3" + "atom-package-manager": "1.18.4" } } diff --git a/appveyor.yml b/appveyor.yml index 6a8d3ac91..5195d81cc 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -19,9 +19,11 @@ environment: global: ATOM_DEV_RESOURCE_PATH: c:\projects\atom TEST_JUNIT_XML_ROOT: c:\projects\junit-test-results + NODE_VERSION: 6.9.4 matrix: - - NODE_VERSION: 6.9.4 + - TASK: test + - TASK: installer matrix: fast_finish: true @@ -34,14 +36,26 @@ install: build_script: - CD %APPVEYOR_BUILD_FOLDER% - - script\build.cmd --code-sign --compress-artifacts + - IF NOT EXIST C:\tmp MKDIR C:\tmp + - SET SQUIRREL_TEMP=C:\tmp + - IF [%TASK%]==[installer] ( + IF [%APPVEYOR_REPO_BRANCH:~-9%]==[-releases] ( + script\build.cmd --code-sign --compress-artifacts --create-windows-installer + ) ELSE ( + ECHO Skipping installer and Atom build on non-release branch + ) + ) ELSE ( + ECHO Skipping installer build on non-installer build matrix row && + script\build.cmd --code-sign --compress-artifacts + ) test_script: - - script\lint.cmd - - script\test.cmd - -after_test: - - IF [%APPVEYOR_REPO_BRANCH:~-9%]==[-releases] ( script\create-installer.cmd ) + - IF [%TASK%]==[test] ( + script\lint.cmd && + script\test.cmd + ) ELSE ( + ECHO Skipping tests on installer build matrix row + ) deploy: off artifacts: diff --git a/docs/native-profiling.md b/docs/native-profiling.md index 58a164982..afac6b4ab 100644 --- a/docs/native-profiling.md +++ b/docs/native-profiling.md @@ -6,7 +6,7 @@ * Open the dev tools with `alt-cmd-i` * Evaluate `process.versions.electron` in the console. * Based on this version, download the appropriate Electron symbols from the [releases](https://github.com/atom/electron/releases) page. - * The file name should look like `electron-v0.X.Y-darwin-x64-dsym.zip`. + * The file name should look like `electron-v1.X.Y-darwin-x64-dsym.zip`. * Decompress these symbols in your `~/Downloads` directory. * Now create a time profile in Instruments. * Open `Instruments.app`. diff --git a/package.json b/package.json index 25da3b9e4..6949a69d1 100644 --- a/package.json +++ b/package.json @@ -99,7 +99,7 @@ "autosave": "0.24.3", "background-tips": "0.27.1", "bookmarks": "0.44.4", - "bracket-matcher": "0.87.0", + "bracket-matcher": "0.87.3", "command-palette": "0.40.4", "dalek": "0.2.1", "deprecation-cop": "0.56.7", @@ -108,7 +108,7 @@ "exception-reporting": "0.41.4", "find-and-replace": "0.209.5", "fuzzy-finder": "1.5.8", - "github": "0.4.0", + "github": "0.4.2", "git-diff": "1.3.6", "go-to-line": "0.32.1", "grammar-selector": "0.49.5", @@ -139,14 +139,14 @@ "language-clojure": "0.22.4", "language-coffee-script": "0.48.9", "language-csharp": "0.14.2", - "language-css": "0.42.3", + "language-css": "0.42.4", "language-gfm": "0.90.0", "language-git": "0.19.1", "language-go": "0.44.2", "language-html": "0.47.3", "language-hyperlink": "0.16.2", - "language-java": "0.27.2", - "language-javascript": "0.127.1", + "language-java": "0.27.3", + "language-javascript": "0.127.2", "language-json": "0.19.1", "language-less": "0.33.0", "language-make": "0.22.3", diff --git a/script/deprecated-packages.json b/script/deprecated-packages.json index 12638967e..dc97e3734 100644 --- a/script/deprecated-packages.json +++ b/script/deprecated-packages.json @@ -875,10 +875,6 @@ "hasDeprecations": true, "latestHasDeprecations": false }, - "language-typescript": { - "hasAlternative": true, - "alternative": "atom-typescript" - }, "laravel-facades": { "version": "<=1.0.0", "hasDeprecations": true, diff --git a/script/package.json b/script/package.json index 52f0b6d55..454d561aa 100644 --- a/script/package.json +++ b/script/package.json @@ -9,7 +9,7 @@ "csslint": "1.0.2", "donna": "1.0.16", "electron-chromedriver": "~1.6", - "electron-link": "0.1.0", + "electron-link": "0.1.1", "electron-mksnapshot": "~1.6", "electron-packager": "7.3.0", "electron-winstaller": "2.6.2", diff --git a/spec/main-process/atom-application.test.js b/spec/main-process/atom-application.test.js index 1e5ae0a86..62fae82b3 100644 --- a/spec/main-process/atom-application.test.js +++ b/spec/main-process/atom-application.test.js @@ -14,10 +14,11 @@ const ATOM_RESOURCE_PATH = path.resolve(__dirname, '..', '..') describe('AtomApplication', function () { this.timeout(60 * 1000) - let originalAppQuit, originalAtomHome, atomApplicationsToDestroy + let originalAppQuit, originalShowMessageBox, originalAtomHome, atomApplicationsToDestroy beforeEach(function () { originalAppQuit = electron.app.quit + originalShowMessageBox = electron.dialog.showMessageBox mockElectronAppQuit() originalAtomHome = process.env.ATOM_HOME process.env.ATOM_HOME = makeTempDir('atom-home') @@ -39,6 +40,7 @@ describe('AtomApplication', function () { } await clearElectronSession() electron.app.quit = originalAppQuit + electron.dialog.showMessageBox = originalShowMessageBox }) describe('launch', function () { @@ -462,20 +464,42 @@ describe('AtomApplication', function () { }) }) - describe('before quitting', function () { - it('waits until all the windows have saved their state and then quits', async function () { - const dirAPath = makeTempDir("a") - const dirBPath = makeTempDir("b") - const atomApplication = buildAtomApplication() - const window1 = atomApplication.launch(parseCommandLine([path.join(dirAPath, 'file-a')])) - await focusWindow(window1) - const window2 = atomApplication.launch(parseCommandLine([path.join(dirBPath, 'file-b')])) - await focusWindow(window2) - electron.app.quit() - assert(!electron.app.hasQuitted()) - await Promise.all([window1.lastPrepareToUnloadPromise, window2.lastPrepareToUnloadPromise]) - assert(electron.app.hasQuitted()) + it('waits until all the windows have saved their state before quitting', async function () { + const dirAPath = makeTempDir("a") + const dirBPath = makeTempDir("b") + const atomApplication = buildAtomApplication() + const window1 = atomApplication.launch(parseCommandLine([path.join(dirAPath, 'file-a')])) + await focusWindow(window1) + const window2 = atomApplication.launch(parseCommandLine([path.join(dirBPath, 'file-b')])) + await focusWindow(window2) + electron.app.quit() + assert(!electron.app.hasQuitted()) + await Promise.all([window1.lastPrepareToUnloadPromise, window2.lastPrepareToUnloadPromise]) + assert(electron.app.hasQuitted()) + }) + + it('prevents quitting if user cancels when prompted to save an item', async () => { + const atomApplication = buildAtomApplication() + const window1 = atomApplication.launch(parseCommandLine([])) + const window2 = atomApplication.launch(parseCommandLine([])) + await Promise.all([window1.loadedPromise, window2.loadedPromise]) + await evalInWebContents(window1.browserWindow.webContents, function (sendBackToMainProcess) { + atom.workspace.getActiveTextEditor().insertText('unsaved text') + sendBackToMainProcess() }) + + // Choosing "Cancel" + mockElectronShowMessageBox({choice: 1}) + electron.app.quit() + await atomApplication.lastBeforeQuitPromise + assert(!electron.app.hasQuitted()) + assert.equal(electron.app.quit.callCount, 1) // Ensure choosing "Cancel" doesn't try to quit the electron app more than once (regression) + + // Choosing "Don't save" + mockElectronShowMessageBox({choice: 2}) + electron.app.quit() + await atomApplication.lastBeforeQuitPromise + assert(electron.app.hasQuitted()) }) function buildAtomApplication () { @@ -496,6 +520,12 @@ describe('AtomApplication', function () { function mockElectronAppQuit () { let quitted = false electron.app.quit = function () { + if (electron.app.quit.callCount) { + electron.app.quit.callCount++ + } else { + electron.app.quit.callCount = 1 + } + let shouldQuit = true electron.app.emit('before-quit', {preventDefault: () => { shouldQuit = false }}) if (shouldQuit) { @@ -507,6 +537,12 @@ describe('AtomApplication', function () { } } + function mockElectronShowMessageBox ({choice}) { + electron.dialog.showMessageBox = function () { + return choice + } + } + function makeTempDir (name) { const temp = require('temp').track() return fs.realpathSync(temp.mkdirSync(name)) diff --git a/spec/pane-spec.js b/spec/pane-spec.js index c36abbf6a..68e93c38f 100644 --- a/spec/pane-spec.js +++ b/spec/pane-spec.js @@ -551,10 +551,11 @@ describe('Pane', () => { itemURI = 'test' confirm.andReturn(0) - await pane.destroyItem(item1) + const success = await pane.destroyItem(item1) expect(item1.save).toHaveBeenCalled() expect(pane.getItems().includes(item1)).toBe(false) expect(item1.isDestroyed()).toBe(true) + expect(success).toBe(true) }) }) @@ -565,11 +566,12 @@ describe('Pane', () => { showSaveDialog.andReturn('/selected/path') confirm.andReturn(0) - await pane.destroyItem(item1) + const success = await pane.destroyItem(item1) expect(showSaveDialog).toHaveBeenCalled() expect(item1.saveAs).toHaveBeenCalledWith('/selected/path') expect(pane.getItems().includes(item1)).toBe(false) expect(item1.isDestroyed()).toBe(true) + expect(success).toBe(true) }) }) }) @@ -578,10 +580,11 @@ describe('Pane', () => { it('removes and destroys the item without saving it', async () => { confirm.andReturn(2) - await pane.destroyItem(item1) + const success = await pane.destroyItem(item1) expect(item1.save).not.toHaveBeenCalled() expect(pane.getItems().includes(item1)).toBe(false) expect(item1.isDestroyed()).toBe(true) + expect(success).toBe(true); }) }) @@ -589,19 +592,21 @@ describe('Pane', () => { it('does not save, remove, or destroy the item', async () => { confirm.andReturn(1) - await pane.destroyItem(item1) + const success = await pane.destroyItem(item1) expect(item1.save).not.toHaveBeenCalled() expect(pane.getItems().includes(item1)).toBe(true) expect(item1.isDestroyed()).toBe(false) + expect(success).toBe(false) }) }) describe('when force=true', () => { it('destroys the item immediately', async () => { - await pane.destroyItem(item1, true) + const success = await pane.destroyItem(item1, true) expect(item1.save).not.toHaveBeenCalled() expect(pane.getItems().includes(item1)).toBe(false) expect(item1.isDestroyed()).toBe(true) + expect(success).toBe(true) }) }) }) @@ -630,18 +635,20 @@ describe('Pane', () => { }) describe('when passed a permanent dock item', () => { - it("doesn't destroy the item", () => { + it("doesn't destroy the item", async () => { spyOn(item1, 'isPermanentDockItem').andReturn(true) - pane.destroyItem(item1) + const success = await pane.destroyItem(item1) expect(pane.getItems().includes(item1)).toBe(true) expect(item1.isDestroyed()).toBe(false) + expect(success).toBe(false); }) - it('destroy the item if force=true', () => { + it('destroy the item if force=true', async () => { spyOn(item1, 'isPermanentDockItem').andReturn(true) - pane.destroyItem(item1, true) + const success = await pane.destroyItem(item1, true) expect(pane.getItems().includes(item1)).toBe(false) expect(item1.isDestroyed()).toBe(true) + expect(success).toBe(true) }) }) }) diff --git a/spec/text-editor-component-spec.js b/spec/text-editor-component-spec.js index 3b2f2072a..4c0108b33 100644 --- a/spec/text-editor-component-spec.js +++ b/spec/text-editor-component-spec.js @@ -119,6 +119,44 @@ describe('TextEditorComponent', () => { } }) + it('re-renders lines when their height changes', async () => { + const {component, element, editor} = buildComponent({rowsPerTile: 3, autoHeight: false}) + element.style.height = 4 * component.measurements.lineHeight + 'px' + await component.getNextUpdatePromise() + expect(element.querySelectorAll('.line-number:not(.dummy)').length).toBe(9) + expect(element.querySelectorAll('.line:not(.dummy)').length).toBe(9) + + element.style.lineHeight = '2.0' + TextEditor.didUpdateStyles() + await component.getNextUpdatePromise() + expect(element.querySelectorAll('.line-number:not(.dummy)').length).toBe(6) + expect(element.querySelectorAll('.line:not(.dummy)').length).toBe(6) + + element.style.lineHeight = '0.7' + TextEditor.didUpdateStyles() + await component.getNextUpdatePromise() + expect(element.querySelectorAll('.line-number:not(.dummy)').length).toBe(12) + expect(element.querySelectorAll('.line:not(.dummy)').length).toBe(12) + + element.style.lineHeight = '0.05' + TextEditor.didUpdateStyles() + await component.getNextUpdatePromise() + expect(element.querySelectorAll('.line-number:not(.dummy)').length).toBe(13) + expect(element.querySelectorAll('.line:not(.dummy)').length).toBe(13) + + element.style.lineHeight = '0' + TextEditor.didUpdateStyles() + await component.getNextUpdatePromise() + expect(element.querySelectorAll('.line-number:not(.dummy)').length).toBe(13) + expect(element.querySelectorAll('.line:not(.dummy)').length).toBe(13) + + element.style.lineHeight = '1' + TextEditor.didUpdateStyles() + await component.getNextUpdatePromise() + expect(element.querySelectorAll('.line-number:not(.dummy)').length).toBe(9) + expect(element.querySelectorAll('.line:not(.dummy)').length).toBe(9) + }) + it('makes the content at least as tall as the scroll container client height', async () => { const {component, element, editor} = buildComponent({text: 'a', height: 100}) expect(component.refs.content.offsetHeight).toBe(100) @@ -1524,6 +1562,27 @@ describe('TextEditorComponent', () => { await setScrollTop(component, component.getLineHeight() * 3) expect(element.querySelectorAll('.highlight.a').length).toBe(0) }) + + it('does not move existing highlights when adding or removing other highlight decorations (regression)', async () => { + const {component, element, editor} = buildComponent() + + const marker1 = editor.markScreenRange([[1, 6], [1, 10]]) + editor.decorateMarker(marker1, {type: 'highlight', class: 'a'}) + await component.getNextUpdatePromise() + const marker1Region = element.querySelector('.highlight.a') + expect(Array.from(marker1Region.parentElement.children).indexOf(marker1Region)).toBe(0) + + const marker2 = editor.markScreenRange([[1, 2], [1, 4]]) + editor.decorateMarker(marker2, {type: 'highlight', class: 'b'}) + await component.getNextUpdatePromise() + const marker2Region = element.querySelector('.highlight.b') + expect(Array.from(marker1Region.parentElement.children).indexOf(marker1Region)).toBe(0) + expect(Array.from(marker2Region.parentElement.children).indexOf(marker2Region)).toBe(1) + + marker2.destroy() + await component.getNextUpdatePromise() + expect(Array.from(marker1Region.parentElement.children).indexOf(marker1Region)).toBe(0) + }) }) describe('overlay decorations', () => { @@ -2094,6 +2153,39 @@ describe('TextEditorComponent', () => { expect(component.refs.blockDecorationMeasurementArea.offsetWidth).toBe(component.getScrollWidth()) }) + it('does not change the cursor position when clicking on a block decoration', async () => { + const {editor, component} = buildComponent() + + const decorationElement = document.createElement('div') + decorationElement.textContent = 'Parent' + const childElement = document.createElement('div') + childElement.textContent = 'Child' + decorationElement.appendChild(childElement) + const marker = editor.markScreenPosition([4, 0]) + editor.decorateMarker(marker, {type: 'block', item: decorationElement}) + await component.getNextUpdatePromise() + + const decorationElementClientRect = decorationElement.getBoundingClientRect() + component.didMouseDownOnContent({ + target: decorationElement, + detail: 1, + button: 0, + clientX: decorationElementClientRect.left, + clientY: decorationElementClientRect.top + }) + expect(editor.getCursorScreenPosition()).toEqual([0, 0]) + + const childElementClientRect = childElement.getBoundingClientRect() + component.didMouseDownOnContent({ + target: childElement, + detail: 1, + button: 0, + clientX: childElementClientRect.left, + clientY: childElementClientRect.top + }) + expect(editor.getCursorScreenPosition()).toEqual([0, 0]) + }) + function createBlockDecorationAtScreenRow(editor, screenRow, {height, margin, position}) { const marker = editor.markScreenPosition([screenRow, 0], {invalidate: 'never'}) const item = document.createElement('div') @@ -3028,198 +3120,421 @@ describe('TextEditorComponent', () => { }) describe('keyboard input', () => { - it('handles inserted accented characters via the press-and-hold menu on macOS correctly', () => { - const {editor, component, element} = buildComponent({text: ''}) - editor.insertText('x') - editor.setCursorBufferPosition([0, 1]) + describe('on Chrome 56', () => { + it('handles inserted accented characters via the press-and-hold menu on macOS correctly', async () => { + const {editor, component, element} = buildComponent({text: '', chromeVersion: 56}) + editor.insertText('x') + editor.setCursorBufferPosition([0, 1]) - // Simulate holding the A key to open the press-and-hold menu, - // then closing it via ESC. - component.didKeydown({code: 'KeyA'}) - component.didKeypress({code: 'KeyA'}) - component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}}) - component.didKeydown({code: 'KeyA'}) - component.didKeydown({code: 'KeyA'}) - component.didKeyup({code: 'KeyA'}) - component.didKeydown({code: 'Escape'}) - component.didKeyup({code: 'Escape'}) - expect(editor.getText()).toBe('xa') - // Ensure another "a" can be typed correctly. - component.didKeydown({code: 'KeyA'}) - component.didKeypress({code: 'KeyA'}) - component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}}) - component.didKeyup({code: 'KeyA'}) - expect(editor.getText()).toBe('xaa') - editor.undo() - expect(editor.getText()).toBe('x') + // Simulate holding the A key to open the press-and-hold menu, + // then closing it via ESC. + component.didKeydown({code: 'KeyA'}) + component.didKeypress({code: 'KeyA'}) + component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}}) + component.didKeydown({code: 'KeyA'}) + component.didKeydown({code: 'KeyA'}) + component.didKeyup({code: 'KeyA'}) + component.didKeydown({code: 'Escape'}) + component.didKeyup({code: 'Escape'}) + expect(editor.getText()).toBe('xa') + // Ensure another "a" can be typed correctly. + component.didKeydown({code: 'KeyA'}) + component.didKeypress({code: 'KeyA'}) + component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}}) + component.didKeyup({code: 'KeyA'}) + expect(editor.getText()).toBe('xaa') + editor.undo() + expect(editor.getText()).toBe('x') - // Simulate holding the A key to open the press-and-hold menu, - // then selecting an alternative by typing a number. - component.didKeydown({code: 'KeyA'}) - component.didKeypress({code: 'KeyA'}) - component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}}) - component.didKeydown({code: 'KeyA'}) - component.didKeydown({code: 'KeyA'}) - component.didKeyup({code: 'KeyA'}) - component.didKeydown({code: 'Digit2'}) - component.didKeyup({code: 'Digit2'}) - component.didTextInput({data: 'á', stopPropagation: () => {}, preventDefault: () => {}}) - expect(editor.getText()).toBe('xá') - // Ensure another "a" can be typed correctly. - component.didKeydown({code: 'KeyA'}) - component.didKeypress({code: 'KeyA'}) - component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}}) - component.didKeyup({code: 'KeyA'}) - expect(editor.getText()).toBe('xáa') - editor.undo() - expect(editor.getText()).toBe('x') + // Simulate holding the A key to open the press-and-hold menu, + // then selecting an alternative by typing a number. + component.didKeydown({code: 'KeyA'}) + component.didKeypress({code: 'KeyA'}) + component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}}) + component.didKeydown({code: 'KeyA'}) + component.didKeydown({code: 'KeyA'}) + component.didKeyup({code: 'KeyA'}) + component.didKeydown({code: 'Digit2'}) + component.didKeyup({code: 'Digit2'}) + component.didTextInput({data: 'á', stopPropagation: () => {}, preventDefault: () => {}}) + expect(editor.getText()).toBe('xá') + // Ensure another "a" can be typed correctly. + component.didKeydown({code: 'KeyA'}) + component.didKeypress({code: 'KeyA'}) + component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}}) + component.didKeyup({code: 'KeyA'}) + expect(editor.getText()).toBe('xáa') + editor.undo() + expect(editor.getText()).toBe('x') - // Simulate holding the A key to open the press-and-hold menu, - // then selecting an alternative by clicking on it. - component.didKeydown({code: 'KeyA'}) - component.didKeypress({code: 'KeyA'}) - component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}}) - component.didKeydown({code: 'KeyA'}) - component.didKeydown({code: 'KeyA'}) - component.didKeyup({code: 'KeyA'}) - component.didTextInput({data: 'á', stopPropagation: () => {}, preventDefault: () => {}}) - expect(editor.getText()).toBe('xá') - // Ensure another "a" can be typed correctly. - component.didKeydown({code: 'KeyA'}) - component.didKeypress({code: 'KeyA'}) - component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}}) - component.didKeyup({code: 'KeyA'}) - expect(editor.getText()).toBe('xáa') - editor.undo() - expect(editor.getText()).toBe('x') + // Simulate holding the A key to open the press-and-hold menu, + // then selecting an alternative by clicking on it. + component.didKeydown({code: 'KeyA'}) + component.didKeypress({code: 'KeyA'}) + component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}}) + component.didKeydown({code: 'KeyA'}) + component.didKeydown({code: 'KeyA'}) + component.didKeyup({code: 'KeyA'}) + component.didTextInput({data: 'á', stopPropagation: () => {}, preventDefault: () => {}}) + expect(editor.getText()).toBe('xá') + // Ensure another "a" can be typed correctly. + component.didKeydown({code: 'KeyA'}) + component.didKeypress({code: 'KeyA'}) + component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}}) + component.didKeyup({code: 'KeyA'}) + expect(editor.getText()).toBe('xáa') + editor.undo() + expect(editor.getText()).toBe('x') - // Simulate holding the A key to open the press-and-hold menu, - // cycling through the alternatives with the arrows, then selecting one of them with Enter. - component.didKeydown({code: 'KeyA'}) - component.didKeypress({code: 'KeyA'}) - component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}}) - component.didKeydown({code: 'KeyA'}) - component.didKeydown({code: 'KeyA'}) - component.didKeyup({code: 'KeyA'}) - component.didKeydown({code: 'ArrowRight'}) - component.didCompositionStart({data: ''}) - component.didCompositionUpdate({data: 'à'}) - component.didKeyup({code: 'ArrowRight'}) - expect(editor.getText()).toBe('xà') - component.didKeydown({code: 'ArrowRight'}) - component.didCompositionUpdate({data: 'á'}) - component.didKeyup({code: 'ArrowRight'}) - expect(editor.getText()).toBe('xá') - component.didKeydown({code: 'Enter'}) - component.didCompositionUpdate({data: 'á'}) - component.didTextInput({data: 'á', stopPropagation: () => {}, preventDefault: () => {}}) - component.didCompositionEnd({data: 'á', target: component.refs.cursorsAndInput.refs.hiddenInput}) - component.didKeyup({code: 'Enter'}) - expect(editor.getText()).toBe('xá') - // Ensure another "a" can be typed correctly. - component.didKeydown({code: 'KeyA'}) - component.didKeypress({code: 'KeyA'}) - component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}}) - component.didKeyup({code: 'KeyA'}) - expect(editor.getText()).toBe('xáa') - editor.undo() - expect(editor.getText()).toBe('x') + // Simulate holding the A key to open the press-and-hold menu, + // cycling through the alternatives with the arrows, then selecting one of them with Enter. + component.didKeydown({code: 'KeyA'}) + component.didKeypress({code: 'KeyA'}) + component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}}) + component.didKeydown({code: 'KeyA'}) + component.didKeydown({code: 'KeyA'}) + component.didKeyup({code: 'KeyA'}) + component.didKeydown({code: 'ArrowRight'}) + component.didCompositionStart({data: ''}) + component.didCompositionUpdate({data: 'à'}) + component.getHiddenInput().value = 'à' + component.didKeyup({code: 'ArrowRight'}) + await getNextTickPromise() + expect(editor.getText()).toBe('xà') + component.didKeydown({code: 'ArrowRight'}) + component.didCompositionUpdate({data: 'á'}) + component.getHiddenInput().value = 'á' + component.didKeyup({code: 'ArrowRight'}) + await getNextTickPromise() + expect(editor.getText()).toBe('xá') + component.didKeydown({code: 'Enter'}) + component.didCompositionUpdate({data: 'á'}) + component.getHiddenInput().value = 'á' + component.didTextInput({data: 'á', stopPropagation: () => {}, preventDefault: () => {}}) + component.didCompositionEnd({data: 'á', target: component.getHiddenInput()}) + component.didKeyup({code: 'Enter'}) + await getNextTickPromise() + expect(editor.getText()).toBe('xá') - // Simulate holding the A key to open the press-and-hold menu, - // cycling through the alternatives with the arrows, then closing it via ESC. - component.didKeydown({code: 'KeyA'}) - component.didKeypress({code: 'KeyA'}) - component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}}) - component.didKeydown({code: 'KeyA'}) - component.didKeydown({code: 'KeyA'}) - component.didKeyup({code: 'KeyA'}) - component.didKeydown({code: 'ArrowRight'}) - component.didCompositionStart({data: ''}) - component.didCompositionUpdate({data: 'à'}) - component.didKeyup({code: 'ArrowRight'}) - expect(editor.getText()).toBe('xà') - component.didKeydown({code: 'ArrowRight'}) - component.didCompositionUpdate({data: 'á'}) - component.didKeyup({code: 'ArrowRight'}) - expect(editor.getText()).toBe('xá') - component.didKeydown({code: 'Escape'}) - component.didCompositionUpdate({data: 'a'}) - component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}}) - component.didCompositionEnd({data: 'a', target: component.refs.cursorsAndInput.refs.hiddenInput}) - component.didKeyup({code: 'Escape'}) - expect(editor.getText()).toBe('xa') - // Ensure another "a" can be typed correctly. - component.didKeydown({code: 'KeyA'}) - component.didKeypress({code: 'KeyA'}) - component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}}) - component.didKeyup({code: 'KeyA'}) - expect(editor.getText()).toBe('xaa') - editor.undo() - expect(editor.getText()).toBe('x') + // Ensure another "a" can be typed correctly. + component.didKeydown({code: 'KeyA'}) + component.didKeypress({code: 'KeyA'}) + component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}}) + component.didKeyup({code: 'KeyA'}) + expect(editor.getText()).toBe('xáa') + editor.undo() + expect(editor.getText()).toBe('x') - // Simulate pressing the O key and holding the A key to open the press-and-hold menu right before releasing the O key, - // cycling through the alternatives with the arrows, then closing it via ESC. - component.didKeydown({code: 'KeyO'}) - component.didKeypress({code: 'KeyO'}) - component.didTextInput({data: 'o', stopPropagation: () => {}, preventDefault: () => {}}) - component.didKeydown({code: 'KeyA'}) - component.didKeypress({code: 'KeyA'}) - component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}}) - component.didKeyup({code: 'KeyO'}) - component.didKeydown({code: 'KeyA'}) - component.didKeydown({code: 'KeyA'}) - component.didKeydown({code: 'ArrowRight'}) - component.didCompositionStart({data: ''}) - component.didCompositionUpdate({data: 'à'}) - component.didKeyup({code: 'ArrowRight'}) - expect(editor.getText()).toBe('xoà') - component.didKeydown({code: 'ArrowRight'}) - component.didCompositionUpdate({data: 'á'}) - component.didKeyup({code: 'ArrowRight'}) - expect(editor.getText()).toBe('xoá') - component.didKeydown({code: 'Escape'}) - component.didCompositionUpdate({data: 'a'}) - component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}}) - component.didCompositionEnd({data: 'a', target: component.refs.cursorsAndInput.refs.hiddenInput}) - component.didKeyup({code: 'Escape'}) - expect(editor.getText()).toBe('xoa') - // Ensure another "a" can be typed correctly. - component.didKeydown({code: 'KeyA'}) - component.didKeypress({code: 'KeyA'}) - component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}}) - component.didKeyup({code: 'KeyA'}) - editor.undo() - expect(editor.getText()).toBe('x') + // Simulate holding the A key to open the press-and-hold menu, + // cycling through the alternatives with the arrows, then closing it via ESC. + component.didKeydown({code: 'KeyA'}) + component.didKeypress({code: 'KeyA'}) + component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}}) + component.didKeydown({code: 'KeyA'}) + component.didKeydown({code: 'KeyA'}) + component.didKeyup({code: 'KeyA'}) + component.didKeydown({code: 'ArrowRight'}) + component.didCompositionStart({data: ''}) + component.didCompositionUpdate({data: 'à'}) + component.getHiddenInput().value = 'à' + component.didKeyup({code: 'ArrowRight'}) + await getNextTickPromise() + expect(editor.getText()).toBe('xà') + component.didKeydown({code: 'ArrowRight'}) + component.didCompositionUpdate({data: 'á'}) + component.getHiddenInput().value = 'á' + component.didKeyup({code: 'ArrowRight'}) + await getNextTickPromise() + expect(editor.getText()).toBe('xá') + component.didKeydown({code: 'Escape'}) + component.didCompositionUpdate({data: 'a'}) + component.getHiddenInput().value = 'a' + component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}}) + component.didCompositionEnd({data: 'a', target: component.refs.cursorsAndInput.refs.hiddenInput}) + component.didKeyup({code: 'Escape'}) + await getNextTickPromise() + expect(editor.getText()).toBe('xa') + // Ensure another "a" can be typed correctly. + component.didKeydown({code: 'KeyA'}) + component.didKeypress({code: 'KeyA'}) + component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}}) + component.didKeyup({code: 'KeyA'}) + expect(editor.getText()).toBe('xaa') + editor.undo() + expect(editor.getText()).toBe('x') - // Simulate holding the A key to open the press-and-hold menu, - // cycling through the alternatives with the arrows, then closing it by changing focus. - component.didKeydown({code: 'KeyA'}) - component.didKeypress({code: 'KeyA'}) - component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}}) - component.didKeydown({code: 'KeyA'}) - component.didKeydown({code: 'KeyA'}) - component.didKeyup({code: 'KeyA'}) - component.didKeydown({code: 'ArrowRight'}) - component.didCompositionStart({data: ''}) - component.didCompositionUpdate({data: 'à'}) - component.didKeyup({code: 'ArrowRight'}) - expect(editor.getText()).toBe('xà') - component.didKeydown({code: 'ArrowRight'}) - component.didCompositionUpdate({data: 'á'}) - component.didKeyup({code: 'ArrowRight'}) - expect(editor.getText()).toBe('xá') - component.didCompositionUpdate({data: 'á'}) - component.didTextInput({data: 'á', stopPropagation: () => {}, preventDefault: () => {}}) - component.didCompositionEnd({data: 'á', target: component.refs.cursorsAndInput.refs.hiddenInput}) - expect(editor.getText()).toBe('xá') - // Ensure another "a" can be typed correctly. - component.didKeydown({code: 'KeyA'}) - component.didKeypress({code: 'KeyA'}) - component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}}) - component.didKeyup({code: 'KeyA'}) - expect(editor.getText()).toBe('xáa') - editor.undo() - expect(editor.getText()).toBe('x') + // Simulate pressing the O key and holding the A key to open the press-and-hold menu right before releasing the O key, + // cycling through the alternatives with the arrows, then closing it via ESC. + component.didKeydown({code: 'KeyO'}) + component.didKeypress({code: 'KeyO'}) + component.didTextInput({data: 'o', stopPropagation: () => {}, preventDefault: () => {}}) + component.didKeydown({code: 'KeyA'}) + component.didKeypress({code: 'KeyA'}) + component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}}) + component.didKeyup({code: 'KeyO'}) + component.didKeydown({code: 'KeyA'}) + component.didKeydown({code: 'KeyA'}) + component.didKeydown({code: 'ArrowRight'}) + component.didCompositionStart({data: ''}) + component.didCompositionUpdate({data: 'à'}) + component.getHiddenInput().value = 'à' + component.didKeyup({code: 'ArrowRight'}) + await getNextTickPromise() + expect(editor.getText()).toBe('xoà') + component.didKeydown({code: 'ArrowRight'}) + component.didCompositionUpdate({data: 'á'}) + component.getHiddenInput().value = 'á' + component.didKeyup({code: 'ArrowRight'}) + await getNextTickPromise() + expect(editor.getText()).toBe('xoá') + component.didKeydown({code: 'Escape'}) + component.didCompositionUpdate({data: 'a'}) + component.getHiddenInput().value = 'a' + component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}}) + component.didCompositionEnd({data: 'a', target: component.refs.cursorsAndInput.refs.hiddenInput}) + component.didKeyup({code: 'Escape'}) + await getNextTickPromise() + expect(editor.getText()).toBe('xoa') + // Ensure another "a" can be typed correctly. + component.didKeydown({code: 'KeyA'}) + component.didKeypress({code: 'KeyA'}) + component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}}) + component.didKeyup({code: 'KeyA'}) + editor.undo() + expect(editor.getText()).toBe('x') + + // Simulate holding the A key to open the press-and-hold menu, + // cycling through the alternatives with the arrows, then closing it by changing focus. + component.didKeydown({code: 'KeyA'}) + component.didKeypress({code: 'KeyA'}) + component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}}) + component.didKeydown({code: 'KeyA'}) + component.didKeydown({code: 'KeyA'}) + component.didKeyup({code: 'KeyA'}) + component.didKeydown({code: 'ArrowRight'}) + component.didCompositionStart({data: ''}) + component.didCompositionUpdate({data: 'à'}) + component.getHiddenInput().value = 'à' + component.didKeyup({code: 'ArrowRight'}) + await getNextTickPromise() + expect(editor.getText()).toBe('xà') + component.didKeydown({code: 'ArrowRight'}) + component.didCompositionUpdate({data: 'á'}) + component.getHiddenInput().value = 'á' + component.didKeyup({code: 'ArrowRight'}) + await getNextTickPromise() + expect(editor.getText()).toBe('xá') + component.didCompositionUpdate({data: 'á'}) + component.getHiddenInput().value = 'á' + component.didTextInput({data: 'á', stopPropagation: () => {}, preventDefault: () => {}}) + component.didCompositionEnd({data: 'á', target: component.refs.cursorsAndInput.refs.hiddenInput}) + await getNextTickPromise() + expect(editor.getText()).toBe('xá') + // Ensure another "a" can be typed correctly. + component.didKeydown({code: 'KeyA'}) + component.didKeypress({code: 'KeyA'}) + component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}}) + component.didKeyup({code: 'KeyA'}) + expect(editor.getText()).toBe('xáa') + editor.undo() + expect(editor.getText()).toBe('x') + }) + }) + + describe('on other versions of Chrome', () => { + it('handles inserted accented characters via the press-and-hold menu on macOS correctly', () => { + const {editor, component, element} = buildComponent({text: '', chromeVersion: 57}) + editor.insertText('x') + editor.setCursorBufferPosition([0, 1]) + + // Simulate holding the A key to open the press-and-hold menu, + // then closing it via ESC. + component.didKeydown({code: 'KeyA'}) + component.didKeypress({code: 'KeyA'}) + component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}}) + component.didKeydown({code: 'KeyA'}) + component.didKeydown({code: 'KeyA'}) + component.didKeyup({code: 'KeyA'}) + component.didKeydown({code: 'Escape'}) + component.didKeyup({code: 'Escape'}) + expect(editor.getText()).toBe('xa') + // Ensure another "a" can be typed correctly. + component.didKeydown({code: 'KeyA'}) + component.didKeypress({code: 'KeyA'}) + component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}}) + component.didKeyup({code: 'KeyA'}) + expect(editor.getText()).toBe('xaa') + editor.undo() + expect(editor.getText()).toBe('x') + + // Simulate holding the A key to open the press-and-hold menu, + // then selecting an alternative by typing a number. + component.didKeydown({code: 'KeyA'}) + component.didKeypress({code: 'KeyA'}) + component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}}) + component.didKeydown({code: 'KeyA'}) + component.didKeydown({code: 'KeyA'}) + component.didKeyup({code: 'KeyA'}) + component.didKeydown({code: 'Digit2'}) + component.didKeyup({code: 'Digit2'}) + component.didTextInput({data: 'á', stopPropagation: () => {}, preventDefault: () => {}}) + expect(editor.getText()).toBe('xá') + // Ensure another "a" can be typed correctly. + component.didKeydown({code: 'KeyA'}) + component.didKeypress({code: 'KeyA'}) + component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}}) + component.didKeyup({code: 'KeyA'}) + expect(editor.getText()).toBe('xáa') + editor.undo() + expect(editor.getText()).toBe('x') + + // Simulate holding the A key to open the press-and-hold menu, + // then selecting an alternative by clicking on it. + component.didKeydown({code: 'KeyA'}) + component.didKeypress({code: 'KeyA'}) + component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}}) + component.didKeydown({code: 'KeyA'}) + component.didKeydown({code: 'KeyA'}) + component.didKeyup({code: 'KeyA'}) + component.didTextInput({data: 'á', stopPropagation: () => {}, preventDefault: () => {}}) + expect(editor.getText()).toBe('xá') + // Ensure another "a" can be typed correctly. + component.didKeydown({code: 'KeyA'}) + component.didKeypress({code: 'KeyA'}) + component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}}) + component.didKeyup({code: 'KeyA'}) + expect(editor.getText()).toBe('xáa') + editor.undo() + expect(editor.getText()).toBe('x') + + // Simulate holding the A key to open the press-and-hold menu, + // cycling through the alternatives with the arrows, then selecting one of them with Enter. + component.didKeydown({code: 'KeyA'}) + component.didKeypress({code: 'KeyA'}) + component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}}) + component.didKeydown({code: 'KeyA'}) + component.didKeydown({code: 'KeyA'}) + component.didKeyup({code: 'KeyA'}) + component.didKeydown({code: 'ArrowRight'}) + component.didCompositionStart({data: ''}) + component.didCompositionUpdate({data: 'à'}) + component.didKeyup({code: 'ArrowRight'}) + expect(editor.getText()).toBe('xà') + component.didKeydown({code: 'ArrowRight'}) + component.didCompositionUpdate({data: 'á'}) + component.didKeyup({code: 'ArrowRight'}) + expect(editor.getText()).toBe('xá') + component.didKeydown({code: 'Enter'}) + component.didCompositionUpdate({data: 'á'}) + component.didTextInput({data: 'á', stopPropagation: () => {}, preventDefault: () => {}}) + component.didCompositionEnd({data: 'á', target: component.refs.cursorsAndInput.refs.hiddenInput}) + component.didKeyup({code: 'Enter'}) + expect(editor.getText()).toBe('xá') + // Ensure another "a" can be typed correctly. + component.didKeydown({code: 'KeyA'}) + component.didKeypress({code: 'KeyA'}) + component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}}) + component.didKeyup({code: 'KeyA'}) + expect(editor.getText()).toBe('xáa') + editor.undo() + expect(editor.getText()).toBe('x') + + // Simulate holding the A key to open the press-and-hold menu, + // cycling through the alternatives with the arrows, then closing it via ESC. + component.didKeydown({code: 'KeyA'}) + component.didKeypress({code: 'KeyA'}) + component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}}) + component.didKeydown({code: 'KeyA'}) + component.didKeydown({code: 'KeyA'}) + component.didKeyup({code: 'KeyA'}) + component.didKeydown({code: 'ArrowRight'}) + component.didCompositionStart({data: ''}) + component.didCompositionUpdate({data: 'à'}) + component.didKeyup({code: 'ArrowRight'}) + expect(editor.getText()).toBe('xà') + component.didKeydown({code: 'ArrowRight'}) + component.didCompositionUpdate({data: 'á'}) + component.didKeyup({code: 'ArrowRight'}) + expect(editor.getText()).toBe('xá') + component.didKeydown({code: 'Escape'}) + component.didCompositionUpdate({data: 'a'}) + component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}}) + component.didCompositionEnd({data: 'a', target: component.refs.cursorsAndInput.refs.hiddenInput}) + component.didKeyup({code: 'Escape'}) + expect(editor.getText()).toBe('xa') + // Ensure another "a" can be typed correctly. + component.didKeydown({code: 'KeyA'}) + component.didKeypress({code: 'KeyA'}) + component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}}) + component.didKeyup({code: 'KeyA'}) + expect(editor.getText()).toBe('xaa') + editor.undo() + expect(editor.getText()).toBe('x') + + // Simulate pressing the O key and holding the A key to open the press-and-hold menu right before releasing the O key, + // cycling through the alternatives with the arrows, then closing it via ESC. + component.didKeydown({code: 'KeyO'}) + component.didKeypress({code: 'KeyO'}) + component.didTextInput({data: 'o', stopPropagation: () => {}, preventDefault: () => {}}) + component.didKeydown({code: 'KeyA'}) + component.didKeypress({code: 'KeyA'}) + component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}}) + component.didKeyup({code: 'KeyO'}) + component.didKeydown({code: 'KeyA'}) + component.didKeydown({code: 'KeyA'}) + component.didKeydown({code: 'ArrowRight'}) + component.didCompositionStart({data: ''}) + component.didCompositionUpdate({data: 'à'}) + component.didKeyup({code: 'ArrowRight'}) + expect(editor.getText()).toBe('xoà') + component.didKeydown({code: 'ArrowRight'}) + component.didCompositionUpdate({data: 'á'}) + component.didKeyup({code: 'ArrowRight'}) + expect(editor.getText()).toBe('xoá') + component.didKeydown({code: 'Escape'}) + component.didCompositionUpdate({data: 'a'}) + component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}}) + component.didCompositionEnd({data: 'a', target: component.refs.cursorsAndInput.refs.hiddenInput}) + component.didKeyup({code: 'Escape'}) + expect(editor.getText()).toBe('xoa') + // Ensure another "a" can be typed correctly. + component.didKeydown({code: 'KeyA'}) + component.didKeypress({code: 'KeyA'}) + component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}}) + component.didKeyup({code: 'KeyA'}) + editor.undo() + expect(editor.getText()).toBe('x') + + // Simulate holding the A key to open the press-and-hold menu, + // cycling through the alternatives with the arrows, then closing it by changing focus. + component.didKeydown({code: 'KeyA'}) + component.didKeypress({code: 'KeyA'}) + component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}}) + component.didKeydown({code: 'KeyA'}) + component.didKeydown({code: 'KeyA'}) + component.didKeyup({code: 'KeyA'}) + component.didKeydown({code: 'ArrowRight'}) + component.didCompositionStart({data: ''}) + component.didCompositionUpdate({data: 'à'}) + component.didKeyup({code: 'ArrowRight'}) + expect(editor.getText()).toBe('xà') + component.didKeydown({code: 'ArrowRight'}) + component.didCompositionUpdate({data: 'á'}) + component.didKeyup({code: 'ArrowRight'}) + expect(editor.getText()).toBe('xá') + component.didCompositionUpdate({data: 'á'}) + component.didTextInput({data: 'á', stopPropagation: () => {}, preventDefault: () => {}}) + component.didCompositionEnd({data: 'á', target: component.refs.cursorsAndInput.refs.hiddenInput}) + expect(editor.getText()).toBe('xá') + // Ensure another "a" can be typed correctly. + component.didKeydown({code: 'KeyA'}) + component.didKeypress({code: 'KeyA'}) + component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}}) + component.didKeyup({code: 'KeyA'}) + expect(editor.getText()).toBe('xáa') + editor.undo() + expect(editor.getText()).toBe('x') + }) }) }) @@ -3378,6 +3693,15 @@ describe('TextEditorComponent', () => { expect(top).toBe(clientTopForLine(referenceComponent, 12) - referenceContentRect.top) expect(left).toBe(clientLeftForCharacter(referenceComponent, 12, 1) - referenceContentRect.left) } + + // Measuring a currently rendered line while an autoscroll that causes + // that line to go off-screen is in progress. + { + editor.setCursorScreenPosition([10, 0]) + const {top, left} = component.pixelPositionForScreenPosition({row: 3, column: 5}) + expect(top).toBe(clientTopForLine(referenceComponent, 3) - referenceContentRect.top) + expect(left).toBe(clientLeftForCharacter(referenceComponent, 3, 5) - referenceContentRect.left) + } }) it('does not get the component into an inconsistent state when the model has unflushed changes (regression)', async () => { @@ -3424,6 +3748,16 @@ describe('TextEditorComponent', () => { pixelPosition.left += component.getBaseCharacterWidth() / 3 expect(component.screenPositionForPixelPosition(pixelPosition)).toEqual([12, 1]) } + + // Measuring a currently rendered line while an autoscroll that causes + // that line to go off-screen is in progress. + { + const pixelPosition = referenceComponent.pixelPositionForScreenPosition({row: 3, column: 4}) + pixelPosition.top += component.getLineHeight() / 3 + pixelPosition.left += component.getBaseCharacterWidth() / 3 + editor.setCursorBufferPosition([10, 0]) + expect(component.screenPositionForPixelPosition(pixelPosition)).toEqual([3, 4]) + } }) }) @@ -3525,6 +3859,7 @@ function buildComponent (params = {}) { rowsPerTile: params.rowsPerTile, updatedSynchronously: params.updatedSynchronously || false, platform: params.platform, + chromeVersion: params.chromeVersion, mouseWheelScrollSensitivity: params.mouseWheelScrollSensitivity }) const {element} = component @@ -3658,3 +3993,7 @@ function getElementHeight (element) { bottomRuler.remove() return height } + +function getNextTickPromise () { + return new Promise((resolve) => process.nextTick(resolve)) +} diff --git a/spec/tokenized-buffer-spec.coffee b/spec/tokenized-buffer-spec.coffee index 5b1863b35..07e7e80e6 100644 --- a/spec/tokenized-buffer-spec.coffee +++ b/spec/tokenized-buffer-spec.coffee @@ -184,7 +184,7 @@ describe "TokenizedBuffer", -> it "schedules the invalidated lines to be tokenized in the background", -> buffer.insert([5, 30], '/* */') buffer.setTextInRange([[2, 0], [3, 0]], '/*') - expect(tokenizedBuffer.tokenizedLines[2].tokens[0].scopes).toEqual ['source.js', 'comment.block.js', 'punctuation.definition.comment.js'] + expect(tokenizedBuffer.tokenizedLines[2].tokens[0].scopes).toEqual ['source.js', 'comment.block.js', 'punctuation.definition.comment.begin.js'] expect(tokenizedBuffer.tokenizedLines[3].tokens[0].scopes).toEqual ['source.js'] advanceClock() @@ -214,7 +214,7 @@ describe "TokenizedBuffer", -> it "schedules the invalidated lines to be tokenized in the background", -> buffer.insert([5, 30], '/* */') buffer.insert([2, 0], '/*\nabcde\nabcder') - expect(tokenizedBuffer.tokenizedLines[2].tokens[0].scopes).toEqual ['source.js', 'comment.block.js', 'punctuation.definition.comment.js'] + expect(tokenizedBuffer.tokenizedLines[2].tokens[0].scopes).toEqual ['source.js', 'comment.block.js', 'punctuation.definition.comment.begin.js'] expect(tokenizedBuffer.tokenizedLines[3].tokens[0].scopes).toEqual ['source.js', 'comment.block.js'] expect(tokenizedBuffer.tokenizedLines[4].tokens[0].scopes).toEqual ['source.js', 'comment.block.js'] expect(tokenizedBuffer.tokenizedLines[5].tokens[0].scopes).toEqual ['source.js'] @@ -596,10 +596,10 @@ describe "TokenizedBuffer", -> {position: Point(0, 9), closeTags: ["syntax--keyword syntax--operator syntax--assignment syntax--js"], openTags: []} {position: Point(0, 10), closeTags: [], openTags: ["syntax--constant syntax--numeric syntax--decimal syntax--js"]} {position: Point(0, 11), closeTags: ["syntax--constant syntax--numeric syntax--decimal syntax--js"], openTags: []} - {position: Point(0, 12), closeTags: [], openTags: ["syntax--comment syntax--block syntax--js", "syntax--punctuation syntax--definition syntax--comment syntax--js"]} - {position: Point(0, 14), closeTags: ["syntax--punctuation syntax--definition syntax--comment syntax--js"], openTags: []} - {position: Point(1, 5), closeTags: [], openTags: ["syntax--punctuation syntax--definition syntax--comment syntax--js"]} - {position: Point(1, 7), closeTags: ["syntax--punctuation syntax--definition syntax--comment syntax--js", "syntax--comment syntax--block syntax--js"], openTags: ["syntax--storage syntax--type syntax--var syntax--js"]} + {position: Point(0, 12), closeTags: [], openTags: ["syntax--comment syntax--block syntax--js", "syntax--punctuation syntax--definition syntax--comment syntax--begin syntax--js"]} + {position: Point(0, 14), closeTags: ["syntax--punctuation syntax--definition syntax--comment syntax--begin syntax--js"], openTags: []} + {position: Point(1, 5), closeTags: [], openTags: ["syntax--punctuation syntax--definition syntax--comment syntax--end syntax--js"]} + {position: Point(1, 7), closeTags: ["syntax--punctuation syntax--definition syntax--comment syntax--end syntax--js", "syntax--comment syntax--block syntax--js"], openTags: ["syntax--storage syntax--type syntax--var syntax--js"]} {position: Point(1, 10), closeTags: ["syntax--storage syntax--type syntax--var syntax--js"], openTags: []} {position: Point(1, 15), closeTags: [], openTags: ["syntax--keyword syntax--operator syntax--assignment syntax--js"]} {position: Point(1, 16), closeTags: ["syntax--keyword syntax--operator syntax--assignment syntax--js"], openTags: []} diff --git a/spec/workspace-spec.js b/spec/workspace-spec.js index bdd5677c8..476a4ba5b 100644 --- a/spec/workspace-spec.js +++ b/spec/workspace-spec.js @@ -2394,6 +2394,22 @@ i = /test/; #FIXME\ expect(results[0].replacements).toBe(6) }) }) + + it('does not discard the multiline flag', () => { + const filePath = path.join(projectDir, 'sample.js') + fs.copyFileSync(path.join(fixturesDir, 'sample.js'), filePath) + + const results = [] + waitsForPromise(() => + atom.workspace.replace(/;$/gmi, 'items', [filePath], result => results.push(result)) + ) + + runs(() => { + expect(results).toHaveLength(1) + expect(results[0].filePath).toBe(filePath) + expect(results[0].replacements).toBe(8) + }) + }) }) describe('when a buffer is already open', () => { diff --git a/src/main-process/atom-application.coffee b/src/main-process/atom-application.coffee index 8e8fb8b55..dcc7c6513 100644 --- a/src/main-process/atom-application.coffee +++ b/src/main-process/atom-application.coffee @@ -269,10 +269,19 @@ class AtomApplication @openPathOnEvent('application:open-license', path.join(process.resourcesPath, 'LICENSE.md')) @disposable.add ipcHelpers.on app, 'before-quit', (event) => - unless @quitting + resolveBeforeQuitPromise = null + @lastBeforeQuitPromise = new Promise((resolve) -> resolveBeforeQuitPromise = resolve) + if @quitting + resolveBeforeQuitPromise() + else event.preventDefault() @quitting = true - Promise.all(@windows.map((window) -> window.prepareToUnload())).then(-> app.quit()) + windowUnloadPromises = @windows.map((window) -> window.prepareToUnload()) + Promise.all(windowUnloadPromises).then((windowUnloadedResults) -> + didUnloadAllWindows = windowUnloadedResults.every((didUnloadWindow) -> didUnloadWindow) + app.quit() if didUnloadAllWindows + resolveBeforeQuitPromise() + ) @disposable.add ipcHelpers.on app, 'will-quit', => @killAllProcesses() diff --git a/src/pane.coffee b/src/pane.coffee index 64f215bd7..dc9173992 100644 --- a/src/pane.coffee +++ b/src/pane.coffee @@ -621,12 +621,15 @@ class Pane destroyItem: (item, force) -> index = @items.indexOf(item) if index isnt -1 - return false if not force and @getContainer()?.getLocation() isnt 'center' and item.isPermanentDockItem?() + if not force and @getContainer()?.getLocation() isnt 'center' and item.isPermanentDockItem?() + return Promise.resolve(false) + @emitter.emit 'will-destroy-item', {item, index} @container?.willDestroyPaneItem({item, index, pane: this}) if force or not item?.shouldPromptToSave?() @removeItem(item, false) item.destroy?() + Promise.resolve(true) else @promptToSaveItem(item).then (result) => if result diff --git a/src/text-editor-component.js b/src/text-editor-component.js index 881aaad81..e409001a8 100644 --- a/src/text-editor-component.js +++ b/src/text-editor-component.js @@ -110,8 +110,8 @@ class TextEditorComponent { this.cursorsBlinking = false this.cursorsBlinkedOff = false this.nextUpdateOnlyBlinksCursors = null - this.extraLinesToMeasure = null - this.extraRenderedScreenLines = null + this.linesToMeasure = new Map() + this.extraRenderedScreenLines = new Map() this.horizontalPositionsToMeasure = new Map() // Keys are rows with positions we want to measure, values are arrays of columns to measure this.horizontalPixelPositionsByScreenLineId = new Map() // Values are maps from column to horiontal pixel positions this.blockDecorationsToMeasure = new Set() @@ -355,6 +355,7 @@ class TextEditorComponent { this.queryLineNumbersToRender() this.queryGuttersToRender() this.queryDecorationsToRender() + this.queryExtraScreenLinesToRender() this.shouldRenderDummyScrollbars = !this.remeasureScrollbars etch.updateSync(this) this.updateClassList() @@ -369,8 +370,6 @@ class TextEditorComponent { } const wasHorizontalScrollbarVisible = this.isHorizontalScrollbarVisible() - this.extraRenderedScreenLines = this.extraLinesToMeasure - this.extraLinesToMeasure = null this.measureLongestLineWidth() this.measureHorizontalPositions() this.updateAbsolutePositionedDecorations() @@ -606,21 +605,19 @@ class TextEditorComponent { }) } - if (this.extraLinesToMeasure) { - this.extraLinesToMeasure.forEach((screenLine, screenRow) => { - if (screenRow < startRow || screenRow >= endRow) { - tileNodes.push($(LineComponent, { - key: 'extra-' + screenLine.id, - screenLine, - screenRow, - displayLayer, - nodePool: this.lineNodesPool, - lineNodesByScreenLineId, - textNodesByScreenLineId - })) - } - }) - } + this.extraRenderedScreenLines.forEach((screenLine, screenRow) => { + if (screenRow < startRow || screenRow >= endRow) { + tileNodes.push($(LineComponent, { + key: 'extra-' + screenLine.id, + screenLine, + screenRow, + displayLayer, + nodePool: this.lineNodesPool, + lineNodesByScreenLineId, + textNodesByScreenLineId + })) + } + }) return $.div({ key: 'lineTiles', @@ -830,12 +827,22 @@ class TextEditorComponent { const longestLineRow = model.getApproximateLongestScreenRow() const longestLine = model.screenLineForScreenRow(longestLineRow) if (longestLine !== this.previousLongestLine) { - this.requestExtraLineToMeasure(longestLineRow, longestLine) + this.requestLineToMeasure(longestLineRow, longestLine) this.longestLineToMeasure = longestLine this.previousLongestLine = longestLine } } + queryExtraScreenLinesToRender () { + this.extraRenderedScreenLines.clear() + this.linesToMeasure.forEach((screenLine, row) => { + if (row < this.getRenderedStartRow() || row >= this.getRenderedEndRow()) { + this.extraRenderedScreenLines.set(row, screenLine) + } + }) + this.linesToMeasure.clear() + } + queryLineNumbersToRender () { const {model} = this.props if (!model.isLineNumberGutterVisible()) return @@ -906,7 +913,7 @@ class TextEditorComponent { renderedScreenLineForRow (row) { return ( this.renderedScreenLines[row - this.getRenderedStartRow()] || - (this.extraRenderedScreenLines ? this.extraRenderedScreenLines.get(row) : null) + this.extraRenderedScreenLines.get(row) ) } @@ -1563,6 +1570,10 @@ class TextEditorComponent { } didTextInput (event) { + // Workaround for Chromium not preventing composition events when + // preventDefault is called on the keydown event that precipitated them. + if (this.lastKeydown && this.lastKeydown.defaultPrevented) return + if (!this.isInputEnabled()) return event.stopPropagation() @@ -1619,7 +1630,6 @@ class TextEditorComponent { didKeypress (event) { this.lastKeydownBeforeKeypress = this.lastKeydown - this.lastKeydown = null // This cancels the accented character behavior if we type a key normally // with the menu open. @@ -1629,7 +1639,6 @@ class TextEditorComponent { didKeyup (event) { if (this.lastKeydownBeforeKeypress && this.lastKeydownBeforeKeypress.code === event.code) { this.lastKeydownBeforeKeypress = null - this.lastKeydown = null } } @@ -1644,6 +1653,10 @@ class TextEditorComponent { // 4. compositionend fired // 5. textInput fired; event.data == the completion string didCompositionStart () { + if (this.getChromeVersion() === 56) { + this.getHiddenInput().value = '' + } + this.compositionCheckpoint = this.props.model.createCheckpoint() if (this.accentedCharacterMenuIsOpen) { this.props.model.selectLeft() @@ -1651,7 +1664,20 @@ class TextEditorComponent { } didCompositionUpdate (event) { - this.props.model.insertText(event.data, {select: true}) + // Workaround for Chromium not preventing composition events when + // preventDefault is called on the keydown event that precipitated them. + if (this.lastKeydown && this.lastKeydown.defaultPrevented) return + + if (this.getChromeVersion() === 56) { + process.nextTick(() => { + if (this.compositionCheckpoint != null) { + const previewText = this.getHiddenInput().value + this.props.model.insertText(previewText, {select: true}) + } + }) + } else { + this.props.model.insertText(event.data, {select: true}) + } } didCompositionEnd (event) { @@ -1663,6 +1689,18 @@ class TextEditorComponent { const {target, button, detail, ctrlKey, shiftKey, metaKey} = event const platform = this.getPlatform() + // Ignore clicks on block decorations. + if (target) { + let element = target + while (element && element !== this.element) { + if (this.blockDecorationsByElement.has(element)) { + return + } + + element = element.parentElement + } + } + // On Linux, position the cursor on middle mouse button click. A // textInput event with the contents of the selection clipboard will be // dispatched by the browser automatically on mouseup. @@ -2046,7 +2084,7 @@ class TextEditorComponent { } measureCharacterDimensions () { - this.measurements.lineHeight = this.refs.characterMeasurementLine.getBoundingClientRect().height + this.measurements.lineHeight = Math.max(1, this.refs.characterMeasurementLine.getBoundingClientRect().height) this.measurements.baseCharacterWidth = this.refs.normalWidthCharacterSpan.getBoundingClientRect().width this.measurements.doubleWidthCharacterWidth = this.refs.doubleWidthCharacterSpan.getBoundingClientRect().width this.measurements.halfWidthCharacterWidth = this.refs.halfWidthCharacterSpan.getBoundingClientRect().width @@ -2125,29 +2163,24 @@ class TextEditorComponent { } } - requestExtraLineToMeasure (row, screenLine) { - if (!this.extraLinesToMeasure) this.extraLinesToMeasure = new Map() - this.extraLinesToMeasure.set(row, screenLine) + requestLineToMeasure (row, screenLine) { + this.linesToMeasure.set(row, screenLine) } requestHorizontalMeasurement (row, column) { if (column === 0) return - if (row < this.getRenderedStartRow() || row >= this.getRenderedEndRow()) { - const screenLine = this.props.model.screenLineForScreenRow(row) - if (screenLine) { - this.requestExtraLineToMeasure(row, screenLine) - } else { - return - } - } + const screenLine = this.props.model.screenLineForScreenRow(row) + if (screenLine) { + this.requestLineToMeasure(row, screenLine) - let columns = this.horizontalPositionsToMeasure.get(row) - if (columns == null) { - columns = [] - this.horizontalPositionsToMeasure.set(row, columns) + let columns = this.horizontalPositionsToMeasure.get(row) + if (columns == null) { + columns = [] + this.horizontalPositionsToMeasure.set(row, columns) + } + columns.push(column) } - columns.push(column) } measureHorizontalPositions () { @@ -2260,7 +2293,7 @@ class TextEditorComponent { let screenLine = this.renderedScreenLineForRow(row) if (!screenLine) { - this.requestExtraLineToMeasure(row, model.screenLineForScreenRow(row)) + this.requestLineToMeasure(row, model.screenLineForScreenRow(row)) this.updateSyncBeforeMeasuringContent() this.measureContentDuringUpdateSync() screenLine = this.renderedScreenLineForRow(row) @@ -2808,9 +2841,17 @@ class TextEditorComponent { return this.props.inputEnabled != null ? this.props.inputEnabled : true } + getHiddenInput () { + return this.refs.cursorsAndInput.refs.hiddenInput + } + getPlatform () { return this.props.platform || process.platform } + + getChromeVersion () { + return this.props.chromeVersion || parseInt(process.versions.chrome) + } } class DummyScrollbarComponent { @@ -2851,20 +2892,22 @@ class DummyScrollbarComponent { outerStyle.bottom = 0 outerStyle.left = 0 outerStyle.right = right + 'px' - outerStyle.height = '20px' + outerStyle.height = '15px' outerStyle.overflowY = 'hidden' outerStyle.overflowX = this.props.forceScrollbarVisible ? 'scroll' : 'auto' - innerStyle.height = '20px' + outerStyle.cursor = 'default' + innerStyle.height = '15px' innerStyle.width = (this.props.scrollWidth || 0) + 'px' } else { let bottom = (this.props.horizontalScrollbarHeight || 0) outerStyle.right = 0 outerStyle.top = 0 outerStyle.bottom = bottom + 'px' - outerStyle.width = '20px' + outerStyle.width = '15px' outerStyle.overflowX = 'hidden' outerStyle.overflowY = this.props.forceScrollbarVisible ? 'scroll' : 'auto' - innerStyle.width = '20px' + outerStyle.cursor = 'default' + innerStyle.width = '15px' innerStyle.height = (this.props.scrollHeight || 0) + 'px' } @@ -3417,8 +3460,10 @@ class CursorsAndInputComponent { class LinesTileComponent { constructor (props) { + this.highlightComponentsByKey = new Map() this.props = props etch.initialize(this) + this.updateHighlights() this.createLines() this.updateBlockDecorations({}, props) } @@ -3432,13 +3477,22 @@ class LinesTileComponent { this.updateLines(oldProps, newProps) this.updateBlockDecorations(oldProps, newProps) } + this.updateHighlights() } } destroy () { + this.highlightComponentsByKey.forEach((highlightComponent) => { + highlightComponent.destroy() + }) + this.highlightComponentsByKey.clear() + for (let i = 0; i < this.lineComponents.length; i++) { this.lineComponents[i].destroy() } + this.lineComponents.length = 0 + + return etch.destroy(this) } render () { @@ -3456,34 +3510,12 @@ class LinesTileComponent { backgroundColor: 'inherit' } }, - this.renderHighlights() - // Lines and block decorations will be manually inserted here for efficiency - ) - } - - renderHighlights () { - const {top, lineHeight, highlightDecorations} = this.props - - let children = null - if (highlightDecorations) { - const decorationCount = highlightDecorations.length - children = new Array(decorationCount) - for (let i = 0; i < decorationCount; i++) { - const highlightProps = Object.assign( - {parentTileTop: top, lineHeight}, - highlightDecorations[i] - ) - children[i] = $(HighlightComponent, highlightProps) - highlightDecorations[i].flashRequested = false - } - } - - return $.div( - { + $.div({ + ref: 'highlights', className: 'highlights', - style: {contain: 'layout'} - }, - children + style: {layout: 'contain'} + }) + // Lines and block decorations will be manually inserted here for efficiency ) } @@ -3676,6 +3708,40 @@ class LinesTileComponent { } } + updateHighlights () { + const {top, lineHeight, highlightDecorations} = this.props + + const visibleHighlightDecorations = new Set() + if (highlightDecorations) { + for (let i = 0; i < highlightDecorations.length; i++) { + const highlightDecoration = highlightDecorations[i] + + const highlightProps = Object.assign( + {parentTileTop: top, lineHeight}, + highlightDecorations[i] + ) + let highlightComponent = this.highlightComponentsByKey.get(highlightDecoration.key) + if (highlightComponent) { + highlightComponent.update(highlightProps) + } else { + highlightComponent = new HighlightComponent(highlightProps) + this.refs.highlights.appendChild(highlightComponent.element) + this.highlightComponentsByKey.set(highlightDecoration.key, highlightComponent) + } + + highlightDecorations[i].flashRequested = false + visibleHighlightDecorations.add(highlightDecoration.key) + } + } + + this.highlightComponentsByKey.forEach((highlightComponent, key) => { + if (!visibleHighlightDecorations.has(key)) { + highlightComponent.destroy() + this.highlightComponentsByKey.delete(key) + } + }) + } + shouldUpdate (newProps) { const oldProps = this.props if (oldProps.top !== newProps.top) return true @@ -3876,6 +3942,17 @@ class HighlightComponent { if (this.props.flashRequested) this.performFlash() } + destroy () { + if (this.timeoutsByClassName) { + this.timeoutsByClassName.forEach((timeout) => { + window.clearTimeout(timeout) + }) + this.timeoutsByClassName.clear() + } + + return etch.destroy(this) + } + update (newProps) { this.props = newProps etch.updateSync(this) diff --git a/src/text-editor.coffee b/src/text-editor.coffee index 6962bf10a..39abd05a0 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -3527,6 +3527,10 @@ class TextEditor extends Model else 1 + Object.defineProperty(@prototype, 'rowsPerPage', { + get: -> @getRowsPerPage() + }) + ### Section: Config ### diff --git a/src/workspace.js b/src/workspace.js index 3bf112461..17c6b2a8b 100644 --- a/src/workspace.js +++ b/src/workspace.js @@ -1950,6 +1950,7 @@ module.exports = class Workspace extends Model { if (!outOfProcessFinished.length) { let flags = 'g' + if (regex.multiline) { flags += 'm' } if (regex.ignoreCase) { flags += 'i' } const task = Task.once(