diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index b9ff69a5fd..78eb6f5b72 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -1,2 +1,14 @@ + +**Oni Version:** +**Neovim Version (Linux only):** +**Operating System:** + +**Issue:** + +**Expected behavior:** + +**Actual behavior:** + +**Steps to reproduce:** diff --git a/.github/config.yml b/.github/config.yml index 289126c333..ac06da58b8 100644 --- a/.github/config.yml +++ b/.github/config.yml @@ -30,3 +30,6 @@ backers: - 1718128 - 2042893 - 14060883 +- 244396 +- 8832878 +- 5127194 diff --git a/@types/classnames/index.d.ts b/@types/classnames/index.d.ts new file mode 100644 index 0000000000..61e25b6a66 --- /dev/null +++ b/@types/classnames/index.d.ts @@ -0,0 +1,20 @@ +// Custom type definitions for classnames master branch +// Project: https://github.com/JedWatson/classnames + +declare module "classnames" { + type ClassValue = string | number | ClassDictionary | ClassArray | undefined | null | false + + interface ClassDictionary { + [id: string]: boolean | undefined | null + } + + // This is the only way I found to break circular references between ClassArray and ClassValue + // https://github.com/Microsoft/TypeScript/issues/3496#issuecomment-128553540 + interface ClassArray extends Array {} // tslint:disable-line no-empty-interface + + type ClassNamesFn = (...classes: ClassValue[]) => string + + const classNames: ClassNamesFn + + export default classNames +} diff --git a/BACKERS.md b/BACKERS.md index 0e56c65d35..846712cef8 100644 --- a/BACKERS.md +++ b/BACKERS.md @@ -4,11 +4,11 @@ Oni is an MIT-licensed open-source project. It's an independent project without If you use Oni, please consider joining them via the following options: -* Become a backer on [Patreon](https://patreon.com/onivim) -* Become a backer on [OpenCollective](https://opencollective.com/oni#backer) -* Become a backer on [Bountysource](https://salt.bountysource.com/teams/oni) -* Make a donation via [PayPal](https://www.paypal.me/bryphe/25) -* Make a donation via Bitcoin / Ethereum (coming soon) +* Become a backer on [Patreon](https://patreon.com/onivim) +* Become a backer on [OpenCollective](https://opencollective.com/oni#backer) +* Become a backer on [Bountysource](https://salt.bountysource.com/teams/oni) +* Make a donation via [PayPal](https://www.paypal.me/bryphe/25) +* Make a donation via Bitcoin / Ethereum (coming soon) Thanks you to all our backers for making Oni possible! @@ -55,67 +55,81 @@ Thanks you to all our backers for making Oni possible! ## VIP Backers via BountySource -* @jordwalke -* @mhartington -* @MikaAK -* @emolitor +* @jordwalke +* @mhartington +* @MikaAK +* @emolitor ## VIP Backers via Patreon -* @mikl -* Tom Boland +* @mikl +* Tom Boland ## Backers via BountySource -* @adambard -* @akin_so -* @ayohan -* @badosu -* @josemarluedke -* @napcode -* @robtrac -* @rrichardson -* @sbuljac -* @parkerault -* @city41 -* @nithesh -* @erandac -* @appelgriebsch +* @adambard +* @akin_so +* @ayohan +* @badosu +* @josemarluedke +* @napcode +* @robtrac +* @rrichardson +* @sbuljac +* @parkerault +* @city41 +* @nithesh +* @erandac +* @appelgriebsch ## Backers via PayPal -* @mchalkley -* @am2605 -* Nathan Ensmenger +* @mchalkley +* @am2605 +* Nathan Ensmenger ## Backers via OpenCollective -* Tal Amuyal -* Akinola Sowemimo -* Martijn Arts -* Amadeus Folego -* Kiyoshi Murata -* @Himura2la +* Tal Amuyal +* Akinola Sowemimo +* Martijn Arts +* Amadeus Folego +* Kiyoshi Murata +* @Himura2la ## Backers via Patreon -* @bennettrogers -* @muream -* Johnnie Hård -* @robin-pham -* Ryan Campbell -* Balint Fulop -* Quasar Jarosz -* Channing Conger -* Clinton Bloodworth -* Lex Song -* Paul Baumgart -* Kaiden Sin -* Troy Vitullo -* Leo Critchley -* Patrick Massot -* Jerome Pellois -* Wesley Moore +* @bennettrogers +* @muream +* Johnnie Hård +* @robin-pham +* Ryan Campbell +* Balint Fulop +* Quasar Jarosz +* Channing Conger +* Clinton Bloodworth +* Lex Song +* Paul Baumgart +* Kaiden Sin +* Troy Vitullo +* Leo Critchley +* Patrick Massot +* Jerome Pellois +* Wesley Moore +* Kim Fiedler +* Nicolaus Hepler +* Nick Price +* Domenico Maisano +* Daniel Polanco +* Eric Hall +* Dimas Cyriaco +* Carlos Coves Prieto +* Bryan Germann +* James Herdman +* Wayan Jimmy +* Alex +* Phil Plückthun +* Norikazu Hayashi diff --git a/README.md b/README.md index 9f0079d64b..1f61912bc9 100644 --- a/README.md +++ b/README.md @@ -11,9 +11,9 @@ Oni is an independent, MIT-licensed open source project. Please consider supporting Oni by: -* [Become a backer or sponsor on Patreon](https://www.patreon.com/onivim) -* [Become a backer or sponsor on Open Collective](https://opencollective.com/oni) -* [Become a backer on BountySource](https://www.bountysource.com/teams/oni) +* [Become a backer or sponsor on Patreon](https://www.patreon.com/onivim) +* [Become a backer or sponsor on Open Collective](https://opencollective.com/oni) +* [Become a backer on BountySource](https://www.bountysource.com/teams/oni)

Sponsors via OpenCollective

@@ -65,13 +65,13 @@ Check out [Releases](https://github.com/onivim/oni/releases) for the latest bina Oni brings several IDE-like integrations to neovim: -* [Embedded Browser](https://github.com/onivim/oni/wiki/Features#embedded-browser) -* [Quick Info](https://github.com/onivim/oni/wiki/Features#quick-info) -* [Code Completion](https://github.com/onivim/oni/wiki/Features#code-completion) -* [Syntax / Compilation Errors](https://github.com/onivim/oni/wiki/Features#syntax--compilation-errors) -* [Fuzzy Finding](https://github.com/onivim/oni/wiki/Features#fuzzy-finder) -* [Status Bar](https://github.com/onivim/oni/wiki/Features#status-bar) -* [Interactive Tutorial](https://github.com/onivim/oni/wiki/Features#interactive-tutorial) +* [Embedded Browser](https://github.com/onivim/oni/wiki/Features#embedded-browser) +* [Quick Info](https://github.com/onivim/oni/wiki/Features#quick-info) +* [Code Completion](https://github.com/onivim/oni/wiki/Features#code-completion) +* [Syntax / Compilation Errors](https://github.com/onivim/oni/wiki/Features#syntax--compilation-errors) +* [Fuzzy Finding](https://github.com/onivim/oni/wiki/Features#fuzzy-finder) +* [Status Bar](https://github.com/onivim/oni/wiki/Features#status-bar) +* [Interactive Tutorial](https://github.com/onivim/oni/wiki/Features#interactive-tutorial) And more coming - check out our [Roadmap](https://github.com/onivim/oni/wiki/Roadmap) @@ -83,9 +83,9 @@ Oni is cross-platform and supports Windows, Mac, and Linux. We have installation guides for each platform: -* [Windows](https://github.com/onivim/oni/wiki/Installation-Guide#windows) -* [Mac](https://github.com/onivim/oni/wiki/Installation-Guide#mac) -* [Linux](https://github.com/onivim/oni/wiki/Installation-Guide#linux) +* [Windows](https://github.com/onivim/oni/wiki/Installation-Guide#windows) +* [Mac](https://github.com/onivim/oni/wiki/Installation-Guide#mac) +* [Linux](https://github.com/onivim/oni/wiki/Installation-Guide#linux) The latest binaries are available on our [Releases](https://github.com/onivim/oni/releases) page, and if you'd prefer to build from source, check out our [Development](https://github.com/onivim/oni/wiki/Development) guide. @@ -93,12 +93,12 @@ The latest binaries are available on our [Releases](https://github.com/onivim/on The goal of this project is to provide both the full-fledged Vim experience, with no compromises, while pushing forward to enable new productivity scenarios. -* **Modern UX** - The Vim experience should not be compromised by terminal limitations. -* **Rich plugin development** - using JavaScript, instead of VimL. -* **Cross-platform support** - across Windows, OS X, and Linux. -* **Batteries included** - rich features are available out of the box - minimal setup needed to be productive. -* **Performance** - no compromises, Vim is fast, and Oni should be fast too. -* **Ease Learning Curve** - without sacrificing the Vim experience. +* **Modern UX** - The Vim experience should not be compromised by terminal limitations. +* **Rich plugin development** - using JavaScript, instead of VimL. +* **Cross-platform support** - across Windows, OS X, and Linux. +* **Batteries included** - rich features are available out of the box - minimal setup needed to be productive. +* **Performance** - no compromises, Vim is fast, and Oni should be fast too. +* **Ease Learning Curve** - without sacrificing the Vim experience. Vim is an incredible tool for manipulating _text_ at the speed of thought. With a composable, modal command language, it is no wonder that Vim usage is still prevalent today. @@ -108,41 +108,44 @@ The goal of this project is to give an editor that gives the best of both worlds ## Documentation -* Check out the [Wiki](https://github.com/onivim/oni/wiki) for documentation on how to use and modify Oni. -* [FAQ](https://github.com/onivim/oni/wiki/FAQ) -* [Roadmap](https://github.com/onivim/oni/wiki/Roadmap) +* Check out the [Wiki](https://github.com/onivim/oni/wiki) for documentation on how to use and modify Oni. +* [FAQ](https://github.com/onivim/oni/wiki/FAQ) +* [Roadmap](https://github.com/onivim/oni/wiki/Roadmap) ## Contributing There many ways to get involved & contribute to Oni: -* Support Oni financially by making a donation via: - * [Patreon](https://patreon.com/onivim) - * [OpenCollective](https://opencollective.com/oni) - * [Bountysource](https://salt.bountysource.com/teams/oni) -* Thumbs up existing [issues](https://github.com/onivim/oni/issues) if they impact you. -* [Create an issue](https://github.com/onivim/oni/issues) for bugs or new features. -* Review and update our [documentation](https://github.com/onivim/oni/wiki). -* Try out the latest [released build](https://github.com/onivim/oni/releases). -* Help us [develop](https://github.com/onivim/oni/wiki/Development): - * Review [PRs](https://github.com/onivim/oni/pulls) - * Submit a bug fix or feature - * Add test cases -* Create a blog post or YouTube video -* Follow us on [Twitter](https://twitter.com/oni_vim) +* Support Oni financially by making a donation via: + * [Patreon](https://patreon.com/onivim) + * [OpenCollective](https://opencollective.com/oni) + * [Bountysource](https://salt.bountysource.com/teams/oni) +* Thumbs up existing [issues](https://github.com/onivim/oni/issues) if they impact you. +* [Create an issue](https://github.com/onivim/oni/issues) for bugs or new features. +* Review and update our [documentation](https://github.com/onivim/oni/wiki). +* Try out the latest [released build](https://github.com/onivim/oni/releases). +* Help us [develop](https://github.com/onivim/oni/wiki/Development): + * Review [PRs](https://github.com/onivim/oni/pulls) + * Submit a bug fix or feature + * Add test cases +* Create a blog post or YouTube video +* Follow us on [Twitter](https://twitter.com/oni_vim) ## Acknowledgements Oni is an independent project and is made possible by the support of some exceptional people. Big thanks to the following people for helping to realize this project: -* the [neovim team](https://neovim.io/), especially [justinmk](https://github.com/justinmk) and [tarruda](https://github.com/tarruda) - Oni would not be possible without their vision -* [jordwalke](https://github.com/jordwalke) for his generous support, inspiration, and ideas. And React ;) -* [keforbes](https://github.com/keforbes) for helping to get this project off the ground -* [tillarnold](https://github.com/tillarnold) for giving us the `oni` npm package name -* [mhartington](https://github.com/mhartington) for his generous support -* [badosu](https://github.com/badosu) for his support, contributions, and managing the AUR releases -* All our current monthly [sponsors](https://salt.bountysource.com/teams/oni/supporters) and [backers](BACKERS.md) -* All of our [contributors](https://github.com/onivim/oni/graphs/contributors) - thanks for helping to improve this project! +* the [neovim team](https://neovim.io/), especially [justinmk](https://github.com/justinmk) and [tarruda](https://github.com/tarruda) - Oni would not be possible without their vision +* [jordwalke](https://github.com/jordwalke) for his generous support, inspiration, and ideas. And React ;) +* [keforbes](https://github.com/keforbes) for helping to get this project off the ground +* [Akin909](https://github.com/Akin909) for his extensive contributions +* [CrossR](https://github.com/CrossR) for polishing features and configurations +* [Cryza](https://github.com/Cryza) for the webgl renderer +* [tillarnold](https://github.com/tillarnold) for giving us the `oni` npm package name +* [mhartington](https://github.com/mhartington) for his generous support +* [badosu](https://github.com/badosu) for his support, contributions, and managing the AUR releases +* All our current monthly [sponsors](https://salt.bountysource.com/teams/oni/supporters) and [backers](BACKERS.md) +* All of our [contributors](https://github.com/onivim/oni/graphs/contributors) - thanks for helping to improve this project! Several other great neovim front-end UIs [here](https://github.com/neovim/neovim/wiki/Related-projects) served as a reference, especially [NyaoVim](https://github.com/rhysd/NyaoVim) and [VimR](https://github.com/qvacua/vimr). I encourage you to check those out! @@ -163,9 +166,9 @@ Windows and OSX have a bundled version of Neovim, which is covered under [Neovim Bundled plugins have their own license terms. These include: -* [typescript-vim](https://github.com/leafgarland/typescript-vim) (`oni/vim/core/typescript.vim`) -* [targets.vim](https://github.com/wellle/targets.vim) (`oni/vim/default/bundle/targets.vim`) -* [vim-commentary](https://github.com/tpope/vim-commentary) (`oni/vim/default/bundle/vim-commentary`) -* [vim-unimpaired](https://github.com/tpope/vim-unimpaired) (`oni/vim/default/bundle/vim-unimpaired`) -* [vim-surround](https://github.com/tpope/vim-surround) (`oni/vim/default/bundle/vim-surround`) -* [vim-reasonml](https://github.com/reasonml-editor/vim-reason) (`.vim` files in `oni/vim/core/oni-plugin-reasonml`) +* [typescript-vim](https://github.com/leafgarland/typescript-vim) (`oni/vim/core/typescript.vim`) +* [targets.vim](https://github.com/wellle/targets.vim) (`oni/vim/default/bundle/targets.vim`) +* [vim-commentary](https://github.com/tpope/vim-commentary) (`oni/vim/default/bundle/vim-commentary`) +* [vim-unimpaired](https://github.com/tpope/vim-unimpaired) (`oni/vim/default/bundle/vim-unimpaired`) +* [vim-surround](https://github.com/tpope/vim-surround) (`oni/vim/default/bundle/vim-surround`) +* [vim-reasonml](https://github.com/reasonml-editor/vim-reason) (`.vim` files in `oni/vim/core/oni-plugin-reasonml`) diff --git a/appveyor.yml b/appveyor.yml index 7e21b50f47..2480e4974a 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -9,6 +9,12 @@ branches: - master - /^release.*/ +# Skip CI build if the changes match these rules exactly. +# Ie, if the BACKERS.md file is changed, we don't need to build. +skip_commits: + files: + - '**/*.md' + cache: - .oni_build_cache -> package.json diff --git a/browser/src/App.ts b/browser/src/App.ts index 336e137b8b..70ac8807fd 100644 --- a/browser/src/App.ts +++ b/browser/src/App.ts @@ -5,12 +5,13 @@ */ import { ipcRenderer } from "electron" +import * as fs from "fs" import * as minimist from "minimist" import * as path from "path" import { IDisposable } from "oni-types" -import * as Log from "./Log" +import * as Log from "oni-core-logging" import * as Performance from "./Performance" import * as Utility from "./Utility" @@ -100,10 +101,27 @@ export const start = async (args: string[]): Promise => { const parsedArgs = minimist(args) const currentWorkingDirectory = process.cwd() - const filesToOpen = parsedArgs._.map( + const normalizedFiles = parsedArgs._.map( arg => (path.isAbsolute(arg) ? arg : path.join(currentWorkingDirectory, arg)), ) + const filesToOpen = normalizedFiles.filter(f => fs.existsSync(f) && fs.statSync(f).isFile()) + const foldersToOpen = normalizedFiles.filter( + f => fs.existsSync(f) && fs.statSync(f).isDirectory(), + ) + + Log.info("Files to open: " + JSON.stringify(filesToOpen)) + Log.info("Folders to open: " + JSON.stringify(foldersToOpen)) + + let workspaceToLoad = null + + // If a folder has been specified, we'll change directory to it + if (foldersToOpen.length > 0) { + workspaceToLoad = foldersToOpen[0] + } else if (filesToOpen.length > 0) { + workspaceToLoad = path.dirname(filesToOpen[0]) + } + // Helper for debugging: Performance.startMeasure("Oni.Start.Config") @@ -133,6 +151,28 @@ export const start = async (args: string[]): Promise => { PluginManager.activate(configuration) const pluginManager = PluginManager.getInstance() + const developmentPlugin = parsedArgs["plugin-develop"] + let developmentPluginError: { title: string; errorText: string } + + if (typeof developmentPlugin === "string") { + Log.info("Registering development plugin: " + developmentPlugin) + if (fs.existsSync(developmentPlugin)) { + pluginManager.addDevelopmentPlugin(developmentPlugin) + } else { + developmentPluginError = { + title: "Error parsing arguments", + errorText: "Could not find plugin: " + developmentPlugin, + } + Log.warn(developmentPluginError.errorText) + } + } else if (typeof developmentPlugin === "boolean") { + developmentPluginError = { + title: "Error parsing arguments", + errorText: "--plugin-develop must be followed by a plugin path", + } + Log.warn(developmentPluginError.errorText) + } + Performance.startMeasure("Oni.Start.Plugins.Discover") pluginManager.discoverPlugins() Performance.endMeasure("Oni.Start.Plugins.Discover") @@ -160,7 +200,7 @@ export const start = async (args: string[]): Promise => { const { editorManager } = await editorManagerPromise const Workspace = await workspacePromise - Workspace.activate(configuration, editorManager) + Workspace.activate(configuration, editorManager, workspaceToLoad) const workspace = Workspace.getInstance() const WindowManager = await windowManagerPromise @@ -191,6 +231,17 @@ export const start = async (args: string[]): Promise => { const Notifications = await notificationsPromise Notifications.activate(configuration, overlayManager) + if (typeof developmentPluginError !== "undefined") { + const notifications = Notifications.getInstance() + const notification = notifications.createItem() + notification.setContents(developmentPluginError.title, developmentPluginError.errorText) + notification.setLevel("error") + notification.onClick.subscribe(() => + commandManager.executeCommand("oni.config.openConfigJs"), + ) + notification.show() + } + configuration.onConfigurationError.subscribe(err => { const notifications = Notifications.getInstance() const notification = notifications.createItem() diff --git a/browser/src/CSS.ts b/browser/src/CSS.ts index 3c486fe9c8..99458547d2 100644 --- a/browser/src/CSS.ts +++ b/browser/src/CSS.ts @@ -13,6 +13,5 @@ export const activate = () => { require("./Services/Menu/Menu.less") require("./UI/components/InstallHelp.less") - require("./UI/components/QuickInfo.less") require("./UI/components/Tabs.less") } diff --git a/browser/src/Editor/BufferManager.ts b/browser/src/Editor/BufferManager.ts index e12492dfb2..6ae98f1c47 100644 --- a/browser/src/Editor/BufferManager.ts +++ b/browser/src/Editor/BufferManager.ts @@ -18,6 +18,7 @@ import { Store } from "redux" import * as detectIndent from "detect-indent" import * as Oni from "oni-api" +import * as Log from "oni-core-logging" import { BufferEventContext, @@ -38,13 +39,18 @@ import * as Actions from "./NeovimEditor/NeovimEditorActions" import * as State from "./NeovimEditor/NeovimEditorStore" import * as Constants from "./../Constants" -import * as Log from "./../Log" import { TokenColor } from "./../Services/TokenColors" import { IBufferLayer } from "./NeovimEditor/BufferLayerManager" +/** + * Candidate API methods + */ export interface IBuffer extends Oni.Buffer { setLanguage(lang: string): Promise + + getLayerById(id: string): T + getCursorPosition(): Promise handleInput(key: string): boolean detectIndentation(): Promise @@ -143,10 +149,12 @@ export class Buffer implements IBuffer { this._actions.addBufferLayer(parseInt(this._id, 10), layer) } - public getLayerById(id: string): T { - return (this._store - .getState() - .layers[parseInt(this._id, 10)].find(layer => layer.id === id) as any) as T + public getLayerById(id: string): T | null { + return ( + ((this._store + .getState() + .layers[parseInt(this._id, 10)].find(layer => layer.id === id) as any) as T) || null + ) } public removeLayer(layer: IBufferLayer): void { @@ -214,7 +222,6 @@ export class Buffer implements IBuffer { ["nvim_command", ["setlocal noswapfile"]], ["nvim_command", ["setlocal nobuflisted"]], ["nvim_command", ["setlocal nomodifiable"]], - ["nvim_command", ["windo set scrollbind!"]], ] const [result, error] = await this._neovimInstance.request( diff --git a/browser/src/Editor/Editor.ts b/browser/src/Editor/Editor.ts index ec63c108ba..5f03741a08 100644 --- a/browser/src/Editor/Editor.ts +++ b/browser/src/Editor/Editor.ts @@ -86,6 +86,10 @@ export class Editor extends Disposable implements Oni.Editor { return Promise.reject("Not implemented") } + public setTextOptions(options: Oni.EditorTextOptions): Promise { + return Promise.reject("Not implemented") + } + protected setMode(mode: Oni.Vim.Mode): void { if (mode !== this._currentMode) { this._currentMode = mode diff --git a/browser/src/Editor/NeovimEditor/FileDropHandler.tsx b/browser/src/Editor/NeovimEditor/FileDropHandler.tsx new file mode 100644 index 0000000000..7309f24a6b --- /dev/null +++ b/browser/src/Editor/NeovimEditor/FileDropHandler.tsx @@ -0,0 +1,60 @@ +import * as React from "react" + +type SetRef = (elem: HTMLElement) => void + +interface IFileDropHandler { + handleFiles: (files: FileList) => void + children: (args: { setRef: SetRef }) => React.ReactElement<{ setRef: SetRef }> +} + +type DragTypeName = "ondragover" | "ondragleave" | "ondragenter" + +/** + * Gets a target element via a callback ref and attaches a file drop event listener callback + * N.B. the element cannot be obscured as this will prevent event transmission + * @name FileDropHandler + * @function + * + * @extends {React} + */ +export default class FileDropHandler extends React.Component { + private _target: HTMLElement + + public componentDidMount() { + this.addDropHandler() + } + + public setRef = (element: HTMLElement) => { + this._target = element + } + + public addDropHandler() { + if (!this._target) { + return + } + + const dragTypes = ["ondragenter", "ondragover", "ondragleave"] + + dragTypes.map((event: DragTypeName) => { + if (this._target[event]) { + this._target[event] = ev => { + ev.preventDefault() + ev.stopPropagation() + } + } + }) + + this._target.ondrop = async ev => { + const { files } = ev.dataTransfer + + if (files.length) { + await this.props.handleFiles(files) + } + ev.preventDefault() + } + } + + public render() { + return this.props.children({ setRef: this.setRef }) + } +} diff --git a/browser/src/Editor/NeovimEditor/HoverRenderer.tsx b/browser/src/Editor/NeovimEditor/HoverRenderer.tsx index 9194533646..893acd13b9 100644 --- a/browser/src/Editor/NeovimEditor/HoverRenderer.tsx +++ b/browser/src/Editor/NeovimEditor/HoverRenderer.tsx @@ -8,23 +8,26 @@ import * as React from "react" import * as types from "vscode-languageserver-types" import getTokens from "./../../Services/SyntaxHighlighting/TokenGenerator" +import { enableMouse } from "./../../UI/components/common" import { ErrorInfo } from "./../../UI/components/ErrorInfo" +import { QuickInfoElement, QuickInfoWrapper } from "./../../UI/components/QuickInfo" import QuickInfoWithTheme from "./../../UI/components/QuickInfoContainer" import * as Helpers from "./../../Plugins/Api/LanguageClient/LanguageClientHelpers" -import { IColors } from "./../../Services/Colors" import { Configuration } from "./../../Services/Configuration" import { convertMarkdown } from "./markdown" -import * as Selectors from "./NeovimEditorSelectors" import { IToolTipsProvider } from "./ToolTipsProvider" const HoverToolTipId = "hover-tool-tip" +const HoverRendererContainer = QuickInfoWrapper.extend` + ${enableMouse}; +` + export class HoverRenderer { constructor( - private _colors: IColors, private _editor: Oni.Editor, private _configuration: Configuration, private _toolTipsProvider: IToolTipsProvider, @@ -58,42 +61,34 @@ export class HoverRenderer { errors: types.Diagnostic[], ): Promise { const titleAndContents = await getTitleAndContents(hover) - const quickInfoElement = !!titleAndContents ? ( - - ) : null - - const borderColor = this._colors.getColor("toolTip.border") - - let customErrorStyle = {} - if (quickInfoElement) { - // TODO: - customErrorStyle = { - "border-bottom": "1px solid " + borderColor, - } - } - - const errorElements = getErrorElements(errors, customErrorStyle) - let debugScopeElement: JSX.Element = null - - if (this._configuration.getValue("editor.textMateHighlighting.debugScopes")) { - debugScopeElement = this._getDebugScopesElement() - } - - // Remove falsy values as check below [null] is truthy - const elements = [...errorElements, quickInfoElement, debugScopeElement].filter(Boolean) + const showDebugScope = this._configuration.getValue( + "editor.textMateHighlighting.debugScopes", + ) - if (!elements.length) { - return null - } + const errorsExist = Boolean(errors && errors.length) + const contentExists = Boolean(errorsExist || titleAndContents || showDebugScope) return ( -
-
-
-
{elements}
-
-
-
+ contentExists && ( + + +
+
+ + + {showDebugScope && this._getDebugScopesElement()} +
+
+
+
+ ) ) } @@ -113,7 +108,8 @@ export class HoverRenderer { if (!scopeInfo || !scopeInfo.scopes) { return null } - const items = scopeInfo.scopes.map((si: string) =>
  • {si}
  • ) + + const items = scopeInfo.scopes.map((si: string) =>
  • {si}
  • ) return (
    DEBUG: TextMate Scopes:
    -
      {items}
    +
      {items}
    ) } @@ -129,12 +125,18 @@ export class HoverRenderer { const html = (str: string) => ({ __html: str }) -const getErrorElements = (errors: types.Diagnostic[], style: any): JSX.Element[] => { - if (!errors || !errors.length) { - return Selectors.EmptyArray - } else { - return [] - } +interface ErrorElementProps { + errors: types.Diagnostic[] + hasQuickInfo: boolean + isVisible: boolean +} + +const ErrorElement = ({ isVisible, errors, hasQuickInfo }: ErrorElementProps) => { + return ( + isVisible && ( + + ) + ) } const getTitleAndContents = async (result: types.Hover) => { diff --git a/browser/src/Editor/NeovimEditor/NeovimBufferLayersView.tsx b/browser/src/Editor/NeovimEditor/NeovimBufferLayersView.tsx index 5bdd719038..e4013ef4b8 100644 --- a/browser/src/Editor/NeovimEditor/NeovimBufferLayersView.tsx +++ b/browser/src/Editor/NeovimEditor/NeovimBufferLayersView.tsx @@ -6,6 +6,7 @@ import * as React from "react" import { connect } from "react-redux" +import { createSelector } from "reselect" import * as Oni from "oni-api" @@ -39,15 +40,18 @@ export class NeovimBufferLayersView extends React.PureComponent { @@ -118,20 +122,30 @@ const EmptyState: NeovimBufferLayersViewProps = { fontPixelWidth: -1, } +const getActiveVimTabPage = (state: State.IState) => state.activeVimTabPage +const getWindowState = (state: State.IState) => state.windowState + +const windowSelector = createSelector( + [getActiveVimTabPage, getWindowState], + (tabPage: State.IVimTabPage, windowState: State.IWindowState) => { + const windows = tabPage.windowIds.map(windowId => { + return windowState.windows[windowId] + }) + + return windows.sort((a, b) => a.windowId - b.windowId) + }, +) + const mapStateToProps = (state: State.IState): NeovimBufferLayersViewProps => { if (!state.activeVimTabPage) { return EmptyState } - const windows = state.activeVimTabPage.windowIds.map(windowId => { - return state.windowState.windows[windowId] - }) - - const wins = windows.sort((a, b) => a.windowId - b.windowId) + const windows = windowSelector(state) return { activeWindowId: state.windowState.activeWindow, - windows: wins, + windows, layers: state.layers, fontPixelWidth: state.fontPixelWidth, fontPixelHeight: state.fontPixelHeight, diff --git a/browser/src/Editor/NeovimEditor/NeovimEditor.tsx b/browser/src/Editor/NeovimEditor/NeovimEditor.tsx index 617a9eca76..77a31a8053 100644 --- a/browser/src/Editor/NeovimEditor/NeovimEditor.tsx +++ b/browser/src/Editor/NeovimEditor/NeovimEditor.tsx @@ -4,6 +4,7 @@ * IEditor implementation for Neovim */ +import * as os from "os" import * as React from "react" import "rxjs/add/observable/defer" @@ -20,10 +21,9 @@ import { bindActionCreators, Store } from "redux" import { clipboard, ipcRenderer } from "electron" import * as Oni from "oni-api" +import * as Log from "oni-core-logging" import { Event, IEvent } from "oni-types" -import * as Log from "./../../Log" - import { addDefaultUnitIfNeeded } from "./../../Font" import { BufferEventContext, @@ -75,7 +75,7 @@ import NeovimSurface from "./NeovimSurface" import { ContextMenuManager } from "./../../Services/ContextMenu" -import { normalizePath, sleep } from "./../../Utility" +import { asObservable, normalizePath, sleep } from "./../../Utility" import * as VimConfigurationSynchronizer from "./../../Services/VimConfigurationSynchronizer" @@ -206,12 +206,7 @@ export class NeovimEditor extends Editor implements IEditor { this._bufferManager = new BufferManager(this._neovimInstance, this._actions, this._store) this._screen = new NeovimScreen() - this._hoverRenderer = new HoverRenderer( - this._colors, - this, - this._configuration, - this._toolTipsProvider, - ) + this._hoverRenderer = new HoverRenderer(this, this._configuration, this._toolTipsProvider) this._definition = new Definition(this, this._store) this._symbols = new Symbols( @@ -307,16 +302,6 @@ export class NeovimEditor extends Editor implements IEditor { ) // Services - this._commands = new NeovimEditorCommands( - commandManager, - this._contextMenuManager, - this._definition, - this._languageIntegration, - this._neovimInstance, - this._rename, - this._symbols, - ) - const onColorsChanged = () => { const updatedColors: any = this._colors.getColors() this._actions.setColors(updatedColors) @@ -388,6 +373,9 @@ export class NeovimEditor extends Editor implements IEditor { this.trackDisposable( this._windowManager.onWindowStateChanged.subscribe(tabPageState => { + if (!tabPageState) { + return + } const filteredTabState = tabPageState.inactiveWindows.filter(w => !!w) const inactiveIds = filteredTabState.map(w => w.windowNumber) @@ -408,6 +396,7 @@ export class NeovimEditor extends Editor implements IEditor { activeWindow.topBufferLine, activeWindow.dimensions, activeWindow.bufferToScreen, + activeWindow.visibleLines, ) } @@ -429,7 +418,9 @@ export class NeovimEditor extends Editor implements IEditor { const isAllowed = isYankAndAllowed || isDeleteAndAllowed if (isAllowed) { - clipboard.writeText(yankInfo.regcontents.join(require("os").EOL)) + const content = yankInfo.regcontents.join(os.EOL) + const postfix = yankInfo.regtype === "V" ? os.EOL : "" + clipboard.writeText(content + postfix) } } }), @@ -584,25 +575,25 @@ export class NeovimEditor extends Editor implements IEditor { }) // TODO: Does any disposal need to happen for the observables? - this._cursorMoved$ = this._neovimInstance.autoCommands.onCursorMoved - .asObservable() - .map((evt): Oni.Cursor => ({ + this._cursorMoved$ = asObservable(this._neovimInstance.autoCommands.onCursorMoved).map( + (evt): Oni.Cursor => ({ line: evt.line - 1, column: evt.column - 1, - })) + }), + ) - this._cursorMovedI$ = this._neovimInstance.autoCommands.onCursorMovedI - .asObservable() - .map((evt): Oni.Cursor => ({ + this._cursorMovedI$ = asObservable(this._neovimInstance.autoCommands.onCursorMovedI).map( + (evt): Oni.Cursor => ({ line: evt.line - 1, column: evt.column - 1, - })) + }), + ) Observable.merge(this._cursorMoved$, this._cursorMovedI$).subscribe(cursorMoved => { this.notifyCursorMoved(cursorMoved) }) - this._modeChanged$ = this._neovimInstance.onModeChanged.asObservable() + this._modeChanged$ = asObservable(this._neovimInstance.onModeChanged) this.trackDisposable( this._neovimInstance.onModeChanged.subscribe(newMode => this._onModeChanged(newMode)), @@ -723,6 +714,16 @@ export class NeovimEditor extends Editor implements IEditor { }), ) + this._commands = new NeovimEditorCommands( + commandManager, + this._contextMenuManager, + this._definition, + this._languageIntegration, + this._neovimInstance, + this._rename, + this._symbols, + ) + this._render() this._onConfigChanged(this._configuration.getValues()) @@ -740,26 +741,6 @@ export class NeovimEditor extends Editor implements IEditor { ipcRenderer.on("open-file", (_evt: any, path: string) => { this._neovimInstance.command(`:e! ${path}`) }) - - // TODO: Factor this out to a react component - // enable opening a file via drag-drop - document.body.ondragover = ev => { - ev.preventDefault() - ev.stopPropagation() - } - - document.body.ondrop = ev => { - ev.preventDefault() - // TODO: the following line currently breaks explorer drag and drop functionality - ev.stopPropagation() - - const { files } = ev.dataTransfer - - if (files.length) { - const normalisedPaths = Array.from(files).map(f => normalizePath(f.path)) - this.openFiles(normalisedPaths, { openMode: Oni.FileOpenMode.Edit }) - } - } } public async blockInput( @@ -885,6 +866,19 @@ export class NeovimEditor extends Editor implements IEditor { await this._neovimInstance.request("nvim_call_atomic", [atomicCalls]) } + public async setTextOptions(textOptions: Oni.EditorTextOptions): Promise { + const { insertSpacesForTab, tabSize } = textOptions + if (insertSpacesForTab) { + await this._neovimInstance.command("set expandtab") + } else { + await this._neovimInstance.command("set noexpandtab") + } + + await this._neovimInstance.command( + `set tabstop=${tabSize} shiftwidth=${tabSize} softtabstop=${tabSize}`, + ) + } + public async openFile( file: string, openOptions: Oni.FileOpenOptions = Oni.DefaultFileOpenOptions, @@ -910,10 +904,10 @@ export class NeovimEditor extends Editor implements IEditor { return this.activeBuffer } - public async openFiles( + public openFiles = async ( files: string[], openOptions: Oni.FileOpenOptions = Oni.DefaultFileOpenOptions, - ): Promise { + ): Promise => { if (!files) { return this.activeBuffer } @@ -946,6 +940,13 @@ export class NeovimEditor extends Editor implements IEditor { commandManager.executeCommand(command, null) } + public _onFilesDropped = async (files: FileList) => { + if (files.length) { + const normalisedPaths = Array.from(files).map(f => normalizePath(f.path)) + await this.openFiles(normalisedPaths, { openMode: Oni.FileOpenMode.Edit }) + } + } + public async init( filesToOpen: string[], startOptions?: Partial, @@ -1060,6 +1061,7 @@ export class NeovimEditor extends Editor implements IEditor { return ( ): void { const fontFamily = this._configuration.getValue("editor.fontFamily") const fontSize = addDefaultUnitIfNeeded(this._configuration.getValue("editor.fontSize")) + const fontWeight = this._configuration.getValue("editor.fontWeight") const linePadding = this._configuration.getValue("editor.linePadding") - this._actions.setFont(fontFamily, fontSize) - this._neovimInstance.setFont(fontFamily, fontSize, linePadding) + this._actions.setFont(fontFamily, fontSize, fontWeight) + this._neovimInstance.setFont(fontFamily, fontSize, fontWeight, linePadding) Object.keys(newValues).forEach(key => { const value = newValues[key] diff --git a/browser/src/Editor/NeovimEditor/NeovimEditorActions.ts b/browser/src/Editor/NeovimEditor/NeovimEditorActions.ts index 87c010f742..df68087414 100644 --- a/browser/src/Editor/NeovimEditor/NeovimEditorActions.ts +++ b/browser/src/Editor/NeovimEditor/NeovimEditorActions.ts @@ -142,6 +142,7 @@ export interface ISetFont { payload: { fontFamily: string fontSize: string + fontWeight: string } } @@ -225,9 +226,11 @@ export interface ISetWindowState { bufferToScreen: Oni.Coordinates.BufferToScreen screenToPixel: Oni.Coordinates.ScreenToPixel + bufferToPixel: Oni.Coordinates.BufferToPixel topBufferLine: number bottomBufferLine: number + visibleLines: string[] } } @@ -491,11 +494,12 @@ export const setImeActive = (imeActive: boolean) => ({ }, }) -export const setFont = (fontFamily: string, fontSize: string) => ({ +export const setFont = (fontFamily: string, fontSize: string, fontWeight: string) => ({ type: "SET_FONT", payload: { fontFamily, fontSize, + fontWeight, }, }) @@ -526,6 +530,7 @@ export const setWindowState = ( topBufferLine: number, dimensions: Oni.Shapes.Rectangle, bufferToScreen: Oni.Coordinates.BufferToScreen, + visibleLines: string[], ) => (dispatch: DispatchFunction, getState: GetStateFunction) => { const { fontPixelWidth, fontPixelHeight } = getState() @@ -547,6 +552,16 @@ export const setWindowState = ( } } + const bufferToPixel = (position: types.Position): Oni.Coordinates.PixelSpacePoint => { + const screenPosition = bufferToScreen(position) + + if (!screenPosition) { + return null + } + + return screenToPixel(screenPosition) + } + dispatch({ type: "SET_WINDOW_STATE", payload: { @@ -558,8 +573,10 @@ export const setWindowState = ( line, bufferToScreen, screenToPixel, + bufferToPixel, bottomBufferLine, topBufferLine, + visibleLines, }, }) } diff --git a/browser/src/Editor/NeovimEditor/NeovimEditorReducer.ts b/browser/src/Editor/NeovimEditor/NeovimEditorReducer.ts index a66a24721d..3b91a43f21 100644 --- a/browser/src/Editor/NeovimEditor/NeovimEditorReducer.ts +++ b/browser/src/Editor/NeovimEditor/NeovimEditorReducer.ts @@ -77,6 +77,7 @@ export function reducer( ...s, fontFamily: a.payload.fontFamily, fontSize: a.payload.fontSize, + fontWeight: a.payload.fontWeight, } case "SET_MODE": return { ...s, ...{ mode: a.payload.mode } } @@ -413,10 +414,12 @@ export const windowStateReducer = ( line: a.payload.line, bufferToScreen: a.payload.bufferToScreen, screenToPixel: a.payload.screenToPixel, + bufferToPixel: a.payload.bufferToPixel, dimensions: a.payload.dimensions, topBufferLine: a.payload.topBufferLine, bottomBufferLine: a.payload.bottomBufferLine, + visibleLines: a.payload.visibleLines, }, }, } diff --git a/browser/src/Editor/NeovimEditor/NeovimEditorStore.ts b/browser/src/Editor/NeovimEditor/NeovimEditorStore.ts index db9800ca92..6ec4827068 100644 --- a/browser/src/Editor/NeovimEditor/NeovimEditorStore.ts +++ b/browser/src/Editor/NeovimEditor/NeovimEditorStore.ts @@ -58,6 +58,7 @@ export interface IState { fontPixelHeight: number fontFamily: string fontSize: string + fontWeight: string hasFocus: boolean mode: string definition: null | IDefinition @@ -159,10 +160,13 @@ export interface IWindow { bufferToScreen: Oni.Coordinates.BufferToScreen screenToPixel: Oni.Coordinates.ScreenToPixel + bufferToPixel: Oni.Coordinates.BufferToPixel dimensions: Oni.Shapes.Rectangle topBufferLine: number bottomBufferLine: number + + visibleLines: string[] } export function readConf( @@ -186,6 +190,7 @@ export const createDefaultState = (): IState => ({ fontPixelHeight: 10, fontFamily: "", fontSize: "", + fontWeight: "", hasFocus: false, imeActive: false, mode: "normal", diff --git a/browser/src/Editor/NeovimEditor/NeovimSurface.tsx b/browser/src/Editor/NeovimEditor/NeovimSurface.tsx index 9a8683306c..cabd7597f2 100644 --- a/browser/src/Editor/NeovimEditor/NeovimSurface.tsx +++ b/browser/src/Editor/NeovimEditor/NeovimSurface.tsx @@ -11,6 +11,7 @@ import { IEvent } from "oni-types" import { NeovimInstance, NeovimScreen } from "./../../neovim" import { INeovimRenderer } from "./../../Renderer" +import FileDropHandler from "./FileDropHandler" import { Cursor } from "./../../UI/components/Cursor" import { CursorLine } from "./../../UI/components/CursorLine" @@ -39,6 +40,7 @@ export interface INeovimSurfaceProps { onKeyDown?: (key: string) => void onBufferClose?: (bufferId: number) => void onBufferSelect?: (bufferId: number) => void + onFileDrop?: (files: FileList) => void onImeStart: () => void onImeEnd: () => void onBounceStart: () => void @@ -67,49 +69,53 @@ class NeovimSurface extends React.Component { public render(): JSX.Element { return ( -
    -
    - -
    -
    -
    (this._editor = e)}> - + + {({ setRef }) => ( +
    +
    + +
    +
    +
    (this._editor = e)}> + +
    +
    + + + + +
    + + +
    + +
    + + +
    -
    - - - - -
    - - -
    - -
    - - -
    -
    + )} + ) } } diff --git a/browser/src/Editor/NeovimEditor/Rename.tsx b/browser/src/Editor/NeovimEditor/Rename.tsx index c54350b2b3..c77effc322 100644 --- a/browser/src/Editor/NeovimEditor/Rename.tsx +++ b/browser/src/Editor/NeovimEditor/Rename.tsx @@ -5,8 +5,8 @@ import * as React from "react" import * as Oni from "oni-api" +import * as Log from "oni-core-logging" -import * as Log from "./../../Log" import * as Helpers from "./../../Plugins/Api/LanguageClient/LanguageClientHelpers" import { LanguageManager } from "./../../Services/Language" diff --git a/browser/src/Editor/NeovimEditor/Symbols.ts b/browser/src/Editor/NeovimEditor/Symbols.ts index 921c7f0740..598a26c384 100644 --- a/browser/src/Editor/NeovimEditor/Symbols.ts +++ b/browser/src/Editor/NeovimEditor/Symbols.ts @@ -13,6 +13,7 @@ import { LanguageManager } from "./../../Services/Language" import * as Helpers from "./../../Plugins/Api/LanguageClient/LanguageClientHelpers" +import { asObservable } from "./../../Utility" import { Definition } from "./Definition" export class Symbols { @@ -34,7 +35,7 @@ export class Symbols { ]) menu.setLoading(true) - const filterTextChanged$ = menu.onFilterTextChanged.asObservable() + const filterTextChanged$ = asObservable(menu.onFilterTextChanged) menu.onItemSelected.subscribe((selectedItem: Oni.Menu.MenuOption) => { const key = selectedItem.label + selectedItem.detail diff --git a/browser/src/Editor/NeovimEditor/markdown.ts b/browser/src/Editor/NeovimEditor/markdown.ts index eb41be0de4..a85c6e1384 100644 --- a/browser/src/Editor/NeovimEditor/markdown.ts +++ b/browser/src/Editor/NeovimEditor/markdown.ts @@ -1,7 +1,7 @@ import { unescape } from "lodash" import * as marked from "marked" -import * as Log from "./../../Log" +import * as Log from "oni-core-logging" import { IGrammarPerLine, IGrammarToken } from "./../../Services/SyntaxHighlighting/TokenGenerator" import * as DOMPurify from "dompurify" diff --git a/browser/src/Editor/OniEditor/OniEditor.tsx b/browser/src/Editor/OniEditor/OniEditor.tsx index 029190db14..a3e1e76666 100644 --- a/browser/src/Editor/OniEditor/OniEditor.tsx +++ b/browser/src/Editor/OniEditor/OniEditor.tsx @@ -12,12 +12,12 @@ import * as React from "react" import * as types from "vscode-languageserver-types" import * as Oni from "oni-api" +import * as Log from "oni-core-logging" import { IEvent } from "oni-types" // import { remote } from "electron" import * as App from "./../../App" -import * as Log from "./../../Log" import * as Utility from "./../../Utility" import { PluginManager } from "./../../Plugins/PluginManager" @@ -246,6 +246,10 @@ export class OniEditor extends Utility.Disposable implements IEditor { return this._neovimEditor.setSelection(range) } + public async setTextOptions(textOptions: Oni.EditorTextOptions): Promise { + return this._neovimEditor.setTextOptions(textOptions) + } + public async blockInput( inputFunction: (input: Oni.InputCallbackFunction) => Promise, ): Promise { diff --git a/browser/src/Font.ts b/browser/src/Font.ts index 4336a87433..331745d293 100644 --- a/browser/src/Font.ts +++ b/browser/src/Font.ts @@ -6,7 +6,12 @@ export interface IFontMeasurement { height: number } -export function measureFont(fontFamily: string, fontSize: string, characterToTest = "H") { +export function measureFont( + fontFamily: string, + fontSize: string, + fontWeight: string, + characterToTest = "H", +) { const div = document.createElement("div") div.style.position = "absolute" @@ -18,6 +23,7 @@ export function measureFont(fontFamily: string, fontSize: string, characterToTes div.textContent = characterToTest div.style.fontFamily = `${fontFamily},${FallbackFonts}` div.style.fontSize = fontSize + div.style.fontWeight = fontWeight const isItalicAvailable = isStyleAvailable(fontFamily, "italic", fontSize) const isBoldAvailable = isStyleAvailable(fontFamily, "bold", fontSize) diff --git a/browser/src/Input/KeyBindings.ts b/browser/src/Input/KeyBindings.ts index a41a38451e..f0ed4d9636 100644 --- a/browser/src/Input/KeyBindings.ts +++ b/browser/src/Input/KeyBindings.ts @@ -47,6 +47,9 @@ export const applyDefaultKeyBindings = (oni: Oni.Plugin.Api, config: Configurati input.bind("", "oni.editor.hide") input.bind("", "buffer.toggle") input.bind("", "search.searchAllFiles") + input.bind("", "sidebar.decreaseWidth") + input.bind("", "sidebar.increaseWidth") + input.bind("", "oni.config.openConfigJs") if (config.getValue("editor.clipboard.enabled")) { input.bind("", "editor.clipboard.yank", isVisualMode) @@ -59,6 +62,8 @@ export const applyDefaultKeyBindings = (oni: Oni.Plugin.Api, config: Configurati input.bind("", "browser.reload") } else { input.bind("", "oni.quit") + input.bind("", "sidebar.decreaseWidth") + input.bind("", "sidebar.increaseWidth") input.bind("", "quickOpen.show", () => isNormalMode() && !isMenuOpen()) input.bind("", "commands.show", isNormalMode) input.bind("", "language.codeAction.expand") @@ -66,6 +71,7 @@ export const applyDefaultKeyBindings = (oni: Oni.Plugin.Api, config: Configurati input.bind("", "language.symbols.document") input.bind("", "buffer.toggle") input.bind("", "search.searchAllFiles") + input.bind("", "oni.config.openConfigJs") if (config.getValue("editor.clipboard.enabled")) { input.bind("", "editor.clipboard.yank", isVisualMode) @@ -101,7 +107,7 @@ export const applyDefaultKeyBindings = (oni: Oni.Plugin.Api, config: Configurati input.bind([""], "quickOpen.openFileVertical") input.bind([""], "quickOpen.openFileHorizontal") input.bind("", "quickOpen.openFileNewTab") - input.bind([""], "quickOpen.openFileExistingTab") + input.bind([""], "quickOpen.openFileAlternative") // Snippets input.bind("", "snippet.nextPlaceholder") @@ -134,7 +140,7 @@ export const applyDefaultKeyBindings = (oni: Oni.Plugin.Api, config: Configurati // Explorer input.bind("d", "explorer.delete.persist", isExplorerActive) input.bind("", "explorer.delete.persist", isExplorerActive) - input.bind("", "explorer.delete", isExplorerActive) + input.bind("", "explorer.delete", isExplorerActive) input.bind("", "explorer.delete", isExplorerActive) input.bind("y", "explorer.yank", isExplorerActive) input.bind("p", "explorer.paste", isExplorerActive) diff --git a/browser/src/Input/KeyParser.ts b/browser/src/Input/KeyParser.ts index ba8c882200..b6daed71bf 100644 --- a/browser/src/Input/KeyParser.ts +++ b/browser/src/Input/KeyParser.ts @@ -84,3 +84,37 @@ export const parseKey = (key: string): IKey => { meta: hasMeta, } } + +// Parse a chord string (e.g. ) into textual descriptions of the relevant keys +// -> ["control", "shift", "p"] +export const parseChordParts = (keys: string): string[] => { + const parsedKeys = parseKeysFromVimString(keys) + + if (!parsedKeys || !parsedKeys.chord || parsedKeys.chord.length === 0) { + return null + } + + const firstChord = parsedKeys.chord[0] + + const chordParts: string[] = [] + + if (firstChord.meta) { + chordParts.push("meta") + } + + if (firstChord.control) { + chordParts.push("control") + } + + if (firstChord.alt) { + chordParts.push("alt") + } + + if (firstChord.shift) { + chordParts.push("shift") + } + + chordParts.push(firstChord.character) + + return chordParts +} diff --git a/browser/src/Input/Keyboard/KeyboardLayout.ts b/browser/src/Input/Keyboard/KeyboardLayout.ts index 29bbb1f102..2001b5353d 100644 --- a/browser/src/Input/Keyboard/KeyboardLayout.ts +++ b/browser/src/Input/Keyboard/KeyboardLayout.ts @@ -1,6 +1,6 @@ +import * as Log from "oni-core-logging" import { Event, IEvent } from "oni-types" -import * as Log from "./../../Log" import * as Platform from "./../../Platform" export interface IKeyMap { diff --git a/browser/src/Input/Keyboard/KeyboardResolver.ts b/browser/src/Input/Keyboard/KeyboardResolver.ts index 03853af3b6..c79213d4d8 100644 --- a/browser/src/Input/Keyboard/KeyboardResolver.ts +++ b/browser/src/Input/Keyboard/KeyboardResolver.ts @@ -3,10 +3,9 @@ * * Manages set of resolvers, and adding/removing resolvers. */ +import * as Log from "oni-core-logging" import { IDisposable } from "oni-types" -import * as Log from "./../../Log" - import { KeyResolver } from "./Resolvers" export class KeyboardResolver { diff --git a/browser/src/Input/KeyboardInput.tsx b/browser/src/Input/KeyboardInput.tsx index 08a9e4288d..343df35ad8 100644 --- a/browser/src/Input/KeyboardInput.tsx +++ b/browser/src/Input/KeyboardInput.tsx @@ -185,7 +185,7 @@ export class KeyboardInputView extends React.PureComponent< return } - const isMetaCommand = key.length > 1 + const isMetaCommand = key.length > 1 && key !== "" // We'll let the `input` handler take care of it, // unless it is a keystroke containing meta characters @@ -242,7 +242,7 @@ export class KeyboardInputView extends React.PureComponent< const valueLength = this._keyboardElement.value.length if (!this.state.isComposing && valueLength > 0) { - this._commit(this._keyboardElement.value) + this._commit(this._keyboardElement.value.replace("<", "")) } } diff --git a/browser/src/Log.ts b/browser/src/Log.ts deleted file mode 100644 index e8a34847ca..0000000000 --- a/browser/src/Log.ts +++ /dev/null @@ -1,61 +0,0 @@ -/** - * Log.ts - * - * Utilities for logging in Oni - */ - -// Log levels are the same as `npm`: -// - error -// - warn -// - info -// - verbose -// - debug - -// Debug is not enabled unless explicitly opted in via `enableDebugLogging` (can be executed in the console via `Oni.log.enableDebugLogging()`) -// Verbose is enabled for debug builds, and off for production builds - -let verboseLoggingEnabled = process.env["NODE_ENV"] === "development" // tslint:disable-line no-string-literal -let debugLoggingEnabled = false - -export const debug = (message: string): void => { - if (debugLoggingEnabled) { - console.log(message) // tslint:disable-line no-console - } -} - -export const verbose = (message: string): void => { - if (verboseLoggingEnabled || debugLoggingEnabled) { - console.log(message) // tslint:disable-line no-console - } -} - -export const info = (message: string): void => { - console.log(message) // tslint:disable-line no-console -} - -export const warn = (message: string): void => { - console.warn(message) // tslint:disable-line no-console -} - -export const error = (messageOrError: string | Error, errorDetails?: any): void => { - console.error(messageOrError) // tslint:disable-line no-console -} - -export const isDebugLoggingEnabled = () => debugLoggingEnabled -export const isVerboseLoggingEnabled = () => verboseLoggingEnabled - -export const enableDebugLogging = () => { - debugLoggingEnabled = true -} - -export const disableDebugLogging = () => { - debugLoggingEnabled = false -} - -export const enableVerboseLogging = () => { - verboseLoggingEnabled = true -} - -export const disableVerboseLogging = () => { - verboseLoggingEnabled = false -} diff --git a/browser/src/Parser.ts b/browser/src/Parser.ts deleted file mode 100644 index 4af2a8aa19..0000000000 --- a/browser/src/Parser.ts +++ /dev/null @@ -1,5 +0,0 @@ -import * as fs from "fs" - -export function parseJsonFromFile(file: string): T { - return JSON.parse(fs.readFileSync(file, "utf8")) -} diff --git a/browser/src/PeriodicJobs.ts b/browser/src/PeriodicJobs.ts index 76aed5a7a0..04be593554 100644 --- a/browser/src/PeriodicJobs.ts +++ b/browser/src/PeriodicJobs.ts @@ -1,5 +1,5 @@ +import * as Log from "oni-core-logging" import * as Constants from "./Constants" -import * as Log from "./Log" // IPeriodicJob implements the interface for a long-running job // that would be expensive to run synchronously, so it is diff --git a/browser/src/PersistentStore.ts b/browser/src/PersistentStore.ts index 240816478d..22840230e4 100644 --- a/browser/src/PersistentStore.ts +++ b/browser/src/PersistentStore.ts @@ -6,7 +6,7 @@ import { remote } from "electron" -import * as Log from "./Log" +import * as Log from "oni-core-logging" // We need to use the 'main process' version of electron-settings. // See: https://github.com/nathanbuchar/electron-settings/wiki/FAQs diff --git a/browser/src/Plugins/Api/LanguageClient/LanguageClientLogger.ts b/browser/src/Plugins/Api/LanguageClient/LanguageClientLogger.ts index 4546855852..b694270f1f 100644 --- a/browser/src/Plugins/Api/LanguageClient/LanguageClientLogger.ts +++ b/browser/src/Plugins/Api/LanguageClient/LanguageClientLogger.ts @@ -4,7 +4,7 @@ * Helper utility for handling logging from language service clients */ -import * as Log from "./../../../Log" +import * as Log from "oni-core-logging" export class LanguageClientLogger { public error(message: string): void { diff --git a/browser/src/Plugins/Api/Oni.ts b/browser/src/Plugins/Api/Oni.ts index 989dee8a16..db0086c604 100644 --- a/browser/src/Plugins/Api/Oni.ts +++ b/browser/src/Plugins/Api/Oni.ts @@ -8,6 +8,7 @@ import * as ChildProcess from "child_process" import * as OniApi from "oni-api" +import * as Log from "oni-core-logging" import Process from "./Process" import { Services } from "./Services" @@ -31,14 +32,13 @@ import { getInstance as getNotificationsInstance } from "./../../Services/Notifi import { getInstance as getOverlayInstance } from "./../../Services/Overlay" import { recorder } from "./../../Services/Recorder" import { getInstance as getSidebarInstance } from "./../../Services/Sidebar" +import { getInstance as getSneakInstance } from "./../../Services/Sneak" import { getInstance as getSnippetsInstance } from "./../../Services/Snippets" import { getInstance as getStatusBarInstance } from "./../../Services/StatusBar" import { getInstance as getTokenColorsInstance } from "./../../Services/TokenColors" import { windowManager } from "./../../Services/WindowManager" import { getInstance as getWorkspaceInstance } from "./../../Services/Workspace" -import * as Log from "./../../Log" - import * as throttle from "lodash/throttle" const react = require("react") // tslint:disable-line no-var-requires @@ -141,6 +141,10 @@ export class Oni implements OniApi.Plugin.Api { return getSidebarInstance() } + public get sneak(): any { + return getSneakInstance() + } + public get snippets(): OniApi.Snippets.SnippetManager { return getSnippetsInstance() } diff --git a/browser/src/Plugins/Api/Process.ts b/browser/src/Plugins/Api/Process.ts index 13ce7a886b..0a099fbdfd 100644 --- a/browser/src/Plugins/Api/Process.ts +++ b/browser/src/Plugins/Api/Process.ts @@ -1,7 +1,8 @@ import * as ChildProcess from "child_process" import * as Oni from "oni-api" -import * as Log from "./../../Log" +import * as Log from "oni-core-logging" + import * as Platform from "./../../Platform" import { configuration } from "./../../Services/Configuration" diff --git a/browser/src/Plugins/PackageMetadataParser.ts b/browser/src/Plugins/PackageMetadataParser.ts index af913d8445..ba8153b0d9 100644 --- a/browser/src/Plugins/PackageMetadataParser.ts +++ b/browser/src/Plugins/PackageMetadataParser.ts @@ -7,9 +7,9 @@ import * as fs from "fs" import * as path from "path" -import * as Capabilities from "./Api/Capabilities" +import * as Log from "oni-core-logging" -import * as Log from "./../Log" +import * as Capabilities from "./Api/Capabilities" const remapToAbsolutePaths = ( packageRoot: string, diff --git a/browser/src/Plugins/Plugin.ts b/browser/src/Plugins/Plugin.ts index e1ee4da78c..e56d65f2ff 100644 --- a/browser/src/Plugins/Plugin.ts +++ b/browser/src/Plugins/Plugin.ts @@ -1,7 +1,7 @@ import * as fs from "fs" import * as path from "path" -import * as Log from "./../Log" +import * as Log from "oni-core-logging" import * as Capabilities from "./Api/Capabilities" import { Oni } from "./Api/Oni" diff --git a/browser/src/Plugins/PluginConfigurationSynchronizer.ts b/browser/src/Plugins/PluginConfigurationSynchronizer.ts index 40d4af0e08..6b39ed06f0 100644 --- a/browser/src/Plugins/PluginConfigurationSynchronizer.ts +++ b/browser/src/Plugins/PluginConfigurationSynchronizer.ts @@ -4,7 +4,7 @@ * Responsible for synchronizing user's `plugin` configuration settings. */ -import * as Log from "./../Log" +import * as Log from "oni-core-logging" import { Configuration } from "./../Services/Configuration" import { PluginManager } from "./PluginManager" diff --git a/browser/src/Plugins/PluginInstaller.ts b/browser/src/Plugins/PluginInstaller.ts index d88aa1d7eb..4aa7c7e730 100644 --- a/browser/src/Plugins/PluginInstaller.ts +++ b/browser/src/Plugins/PluginInstaller.ts @@ -6,6 +6,7 @@ import * as path from "path" +import * as Log from "oni-core-logging" import { Event, IEvent } from "oni-types" // import * as Oni from "oni-api" @@ -20,8 +21,6 @@ import { IFileSystem, OniFileSystem } from "./../Services/Explorer/ExplorerFileS import Process from "./Api/Process" -import * as Log from "./../Log" - /** * Plugin identifier: * - For _git_, this should be of the form `welle/targets.vim` diff --git a/browser/src/Plugins/PluginManager.ts b/browser/src/Plugins/PluginManager.ts index 3c151f7123..e751cde4a4 100644 --- a/browser/src/Plugins/PluginManager.ts +++ b/browser/src/Plugins/PluginManager.ts @@ -24,6 +24,8 @@ export class PluginManager implements Oni.IPluginManager { private _pluginsActivated: boolean = false private _installer: IPluginInstaller = new YarnPluginInstaller() + private _developmentPluginsPath: string[] = [] + public get plugins(): Plugin[] { return this._plugins } @@ -34,6 +36,10 @@ export class PluginManager implements Oni.IPluginManager { constructor(private _config: Configuration) {} + public addDevelopmentPlugin(pluginPath: string): void { + this._developmentPluginsPath.push(pluginPath) + } + public discoverPlugins(): void { const corePluginRootPaths: string[] = [corePluginsRoot, extensionsRoot] const corePlugins: Plugin[] = this._getAllPluginPaths(corePluginRootPaths).map(p => @@ -55,12 +61,16 @@ export class PluginManager implements Oni.IPluginManager { this._createPlugin(p, "user"), ) + const developmentPlugins = this._developmentPluginsPath.map(dev => + this._createPlugin(dev, "development"), + ) + this._rootPluginPaths = [ ...corePluginRootPaths, ...defaultPluginRootPaths, ...userPluginsRootPath, ] - this._plugins = [...corePlugins, ...defaultPlugins, ...userPlugins] + this._plugins = [...corePlugins, ...defaultPlugins, ...userPlugins, ...developmentPlugins] this._anonymousPlugin = new AnonymousPlugin() } @@ -76,7 +86,10 @@ export class PluginManager implements Oni.IPluginManager { } public getAllRuntimePaths(): string[] { - const pluginPaths = this._getAllPluginPaths(this._rootPluginPaths) + const pluginPaths = [ + ...this._getAllPluginPaths(this._rootPluginPaths), + ...this._developmentPluginsPath, + ] return pluginPaths.concat(this._rootPluginPaths) } diff --git a/browser/src/Plugins/PluginSidebarPane.tsx b/browser/src/Plugins/PluginSidebarPane.tsx index fb828d68b9..34f27adac7 100644 --- a/browser/src/Plugins/PluginSidebarPane.tsx +++ b/browser/src/Plugins/PluginSidebarPane.tsx @@ -18,6 +18,56 @@ import { PluginManager } from "./../Plugins/PluginManager" import { noop } from "./../Utility" +import * as Common from "./../UI/components/common" + +import styled from "styled-components" + +const PluginIconWrapper = styled.div` + background-color: rgba(0, 0, 0, 0.1); + width: 36px; + height: 36px; +` + +const PluginCommandsWrapper = styled.div` + flex: 0 0 auto; +` + +const PluginInfoWrapper = styled.div` + flex: 1 1 auto; + width: 100%; + justify-content: center; + display: flex; + flex-direction: column; + margin-left: 8px; + margin-right: 8px; +` + +const PluginTitleWrapper = styled.div` + font-size: 1.1em; +` + +export interface PluginSidebarItemViewProps { + name: string +} + +export class PluginSidebarItemView extends React.PureComponent { + public render(): JSX.Element { + return ( + + + + + + + + {this.props.name} + + + + ) + } +} + export class PluginsSidebarPane implements SidebarPane { private _onEnter = new Event() private _onLeave = new Event() @@ -62,6 +112,7 @@ export interface IPluginsSidebarPaneViewState { isActive: boolean defaultPluginsExpanded: boolean userPluginsExpanded: boolean + workspacePluginsExpanded: boolean } export class PluginsSidebarPaneView extends React.PureComponent< @@ -77,6 +128,7 @@ export class PluginsSidebarPaneView extends React.PureComponent< isActive: false, defaultPluginsExpanded: false, userPluginsExpanded: true, + workspacePluginsExpanded: false, } } @@ -107,6 +159,7 @@ export class PluginsSidebarPaneView extends React.PureComponent< const allIds = [ "container.default", ...defaultPluginIds, + "container.workspace", "container.user", ...userPluginIds, ] @@ -115,13 +168,14 @@ export class PluginsSidebarPaneView extends React.PureComponent< this._onSelect(id)} render={(selectedId: string) => { const defaultPluginItems = defaultPlugins.map(p => ( } onClick={noop} /> )) @@ -131,7 +185,7 @@ export class PluginsSidebarPaneView extends React.PureComponent< indentationLevel={0} isFocused={p.id === selectedId} isContainer={false} - text={p.id} + text={} onClick={noop} /> )) @@ -139,18 +193,29 @@ export class PluginsSidebarPaneView extends React.PureComponent< return (
    this._onSelect("container.default")} > {defaultPluginItems} + this._onSelect("container.workspace")} + > + {[]} + this._onSelect("container.user")} > {userPluginItems} @@ -161,6 +226,29 @@ export class PluginsSidebarPaneView extends React.PureComponent< ) } + private _onSelect(id: string): void { + switch (id) { + case "container.default": + this._toggleDefaultPluginsExpanded() + return + case "container.user": + this._toggleUserPluginsExpanded() + return + } + } + + private _toggleDefaultPluginsExpanded(): void { + this.setState({ + defaultPluginsExpanded: !this.state.defaultPluginsExpanded, + }) + } + + private _toggleUserPluginsExpanded(): void { + this.setState({ + userPluginsExpanded: !this.state.userPluginsExpanded, + }) + } + private _clearExistingSubscriptions(): void { this._subscriptions.forEach(sub => sub.dispose()) this._subscriptions = [] diff --git a/browser/src/Redux/LoggingMiddleware.ts b/browser/src/Redux/LoggingMiddleware.ts index 60ccd0d974..9e0fae0196 100644 --- a/browser/src/Redux/LoggingMiddleware.ts +++ b/browser/src/Redux/LoggingMiddleware.ts @@ -6,7 +6,7 @@ import { Store } from "redux" -import * as Log from "./../Log" +import * as Log from "oni-core-logging" export const createLoggingMiddleware = (storeName: string) => (store: Store) => ( next: any, diff --git a/browser/src/Renderer/CanvasRenderer.ts b/browser/src/Renderer/CanvasRenderer.ts index b6a8687959..4504b8672c 100644 --- a/browser/src/Renderer/CanvasRenderer.ts +++ b/browser/src/Renderer/CanvasRenderer.ts @@ -123,13 +123,16 @@ export class CanvasRenderer implements INeovimRenderer { public _draw(screenInfo: IScreen, modifiedCells: IPosition[]): void { Performance.mark("CanvasRenderer.update.start") - this._canvasContext.font = screenInfo.fontSize + " " + screenInfo.fontFamily + this._canvasContext.font = `${screenInfo.fontWeight} ${screenInfo.fontSize} ${ + screenInfo.fontFamily + }` this._canvasContext.textBaseline = "top" this._canvasContext.setTransform(this._devicePixelRatio, 0, 0, this._devicePixelRatio, 0, 0) this._canvasContext.imageSmoothingEnabled = false this._editorElement.style.fontFamily = screenInfo.fontFamily this._editorElement.style.fontSize = screenInfo.fontSize + this._editorElement.style.fontWeight = screenInfo.fontWeight const rowsToEdit = getSpansToEdit(this._grid, modifiedCells) @@ -345,19 +348,20 @@ export class CanvasRenderer implements INeovimRenderer { private _setContext(): void { this._editorElement.innerHTML = "" + this._devicePixelRatio = window.devicePixelRatio - this._canvasElement = document.createElement("canvas") - this._canvasElement.style.width = "100%" - this._canvasElement.style.height = "100%" + // offsetWidth and offsetHeight always return an integer + const editorWidth = this._editorElement.offsetWidth + const editorHeight = this._editorElement.offsetHeight - this._devicePixelRatio = window.devicePixelRatio + this._canvasElement = document.createElement("canvas") + this._canvasElement.style.width = editorWidth + "px" + this._canvasElement.style.height = editorHeight + "px" this._editorElement.appendChild(this._canvasElement) - this._width = this._canvasElement.width = - this._canvasElement.offsetWidth * this._devicePixelRatio - this._height = this._canvasElement.height = - this._canvasElement.offsetHeight * this._devicePixelRatio + this._width = this._canvasElement.width = editorWidth * this._devicePixelRatio + this._height = this._canvasElement.height = editorHeight * this._devicePixelRatio if ( configuration.getValue("editor.backgroundImageUrl") && diff --git a/browser/src/Renderer/WebGL/WebGLAtlas.ts b/browser/src/Renderer/WebGL/WebGLAtlas.ts index d671917b73..229c17c961 100644 --- a/browser/src/Renderer/WebGL/WebGLAtlas.ts +++ b/browser/src/Renderer/WebGL/WebGLAtlas.ts @@ -1,131 +1,222 @@ -const defaultTextureSizeInPixels = 512 -const glyphPaddingInPixels = 0 +const backgroundColor = "black" +const foregroundColor = "white" export interface IWebGLAtlasOptions { fontFamily: string fontSize: string lineHeightInPixels: number linePaddingInPixels: number + glyphPaddingInPixels: number devicePixelRatio: number offsetGlyphVariantCount: number + textureSizeInPixels: number + textureLayerCount: number } export interface WebGLGlyph { width: number height: number + textureLayerIndex: number textureWidth: number textureHeight: number textureU: number textureV: number variantOffset: number - subpixelWidth: number + subpixelWidth: number // TODO remove this as it is unused } +export class WebGLTextureSpaceExceededError extends Error {} + export class WebGLAtlas { private _glyphContext: CanvasRenderingContext2D - private _glyphs = new Map>() + private _glyphs = new Map() + private _texture: WebGLTexture + private _currentTextureLayerIndex = 0 + private _currentTextureLayerChangedSinceLastUpload = false private _nextX = 0 private _nextY = 0 - private _textureChangedSinceLastUpload = false - private _texture: WebGLTexture - private _textureSize: number - private _uvScale: number constructor(private _gl: WebGL2RenderingContext, private _options: IWebGLAtlasOptions) { - this._textureSize = defaultTextureSizeInPixels * _options.devicePixelRatio - this._uvScale = 1 / this._textureSize - const glyphCanvas = document.createElement("canvas") - glyphCanvas.width = this._textureSize - glyphCanvas.height = this._textureSize + glyphCanvas.width = this._options.textureSizeInPixels + glyphCanvas.height = this._options.textureSizeInPixels this._glyphContext = glyphCanvas.getContext("2d", { alpha: false }) - this._glyphContext.font = `${this._options.fontSize} ${this._options.fontFamily}` - this._glyphContext.fillStyle = "white" + this._glyphContext.fillStyle = foregroundColor this._glyphContext.textBaseline = "top" this._glyphContext.scale(_options.devicePixelRatio, _options.devicePixelRatio) this._glyphContext.imageSmoothingEnabled = false - this._texture = _gl.createTexture() - _gl.bindTexture(_gl.TEXTURE_2D, this._texture) - _gl.texParameteri(_gl.TEXTURE_2D, _gl.TEXTURE_MIN_FILTER, _gl.LINEAR) - _gl.texParameteri(_gl.TEXTURE_2D, _gl.TEXTURE_WRAP_S, _gl.CLAMP_TO_EDGE) - _gl.texParameteri(_gl.TEXTURE_2D, _gl.TEXTURE_WRAP_T, _gl.CLAMP_TO_EDGE) - this._textureChangedSinceLastUpload = true - this.uploadTexture() + document.body.appendChild(glyphCanvas) + + this._texture = this._gl.createTexture() + this._gl.bindTexture(this._gl.TEXTURE_2D_ARRAY, this._texture) + this._gl.texParameteri( + this._gl.TEXTURE_2D_ARRAY, + this._gl.TEXTURE_MIN_FILTER, + this._gl.LINEAR, + ) + this._gl.texParameteri( + this._gl.TEXTURE_2D_ARRAY, + this._gl.TEXTURE_WRAP_S, + this._gl.CLAMP_TO_EDGE, + ) + this._gl.texParameteri( + this._gl.TEXTURE_2D_ARRAY, + this._gl.TEXTURE_WRAP_T, + this._gl.CLAMP_TO_EDGE, + ) + + const textureLayerCount = Math.min( + this._options.textureLayerCount, + this._gl.MAX_ARRAY_TEXTURE_LAYERS, + ) + this._gl.texImage3D( + this._gl.TEXTURE_2D_ARRAY, + 0, + this._gl.RGBA, + this._options.textureSizeInPixels, + this._options.textureSizeInPixels, + textureLayerCount, + 0, + this._gl.RGBA, + this._gl.UNSIGNED_BYTE, + null, + ) } - public getGlyph(text: string, variantIndex: number) { - let glyphVariants = this._glyphs.get(text) - if (!glyphVariants) { - glyphVariants = new Map() - this._glyphs.set(text, glyphVariants) + public getGlyph(text: string, isBold: boolean, isItalic: boolean, variantIndex: number) { + // The mapping goes from character to styles (bold etc.) to subpixel-offset variant, + // e.g. this._glyphs.get("a")[0][0] is the regular "a" with 0 offset, + // while this._glyphs.get("a")[3][1] is the bold italic "a" with 1/offsetGlyphVariantCount px offset + let glyphStyleVariants = this._glyphs.get(text) + if (!glyphStyleVariants) { + glyphStyleVariants = new Array(glyphStyles.length) + this._glyphs.set(text, glyphStyleVariants) + } + const glyphStyleIndex = getGlyphStyleIndex(isBold, isItalic) + let glyphOffsetVariants = glyphStyleVariants[glyphStyleIndex] + if (!glyphOffsetVariants) { + glyphOffsetVariants = new Array(this._options.offsetGlyphVariantCount) + glyphStyleVariants[glyphStyleIndex] = glyphOffsetVariants } - let glyph = glyphVariants.get(variantIndex) + let glyph = glyphOffsetVariants[variantIndex] if (!glyph) { - glyph = this._rasterizeGlyph(text, variantIndex) - glyphVariants.set(variantIndex, glyph) + glyph = this._rasterizeGlyph(text, isBold, isItalic, variantIndex) + glyphOffsetVariants[variantIndex] = glyph } return glyph } public uploadTexture() { - if (this._textureChangedSinceLastUpload) { - this._gl.texImage2D( - this._gl.TEXTURE_2D, + if (this._currentTextureLayerChangedSinceLastUpload) { + this._gl.bindTexture(this._gl.TEXTURE_2D_ARRAY, this._texture) + this._gl.texSubImage3D( + this._gl.TEXTURE_2D_ARRAY, + 0, 0, - this._gl.RGBA, - this._textureSize, - this._textureSize, 0, + this._currentTextureLayerIndex, + this._options.textureSizeInPixels, + this._options.textureSizeInPixels, + 1, this._gl.RGBA, this._gl.UNSIGNED_BYTE, this._glyphContext.canvas, ) - this._textureChangedSinceLastUpload = false + this._currentTextureLayerChangedSinceLastUpload = false } } - private _rasterizeGlyph(text: string, variantIndex: number) { - this._textureChangedSinceLastUpload = true + private _rasterizeGlyph( + text: string, + isBold: boolean, + isItalic: boolean, + variantIndex: number, + ) { + this._currentTextureLayerChangedSinceLastUpload = true const { devicePixelRatio, lineHeightInPixels, linePaddingInPixels, + glyphPaddingInPixels, offsetGlyphVariantCount, } = this._options + const style = getGlyphStyleString(isBold, isItalic) + this._glyphContext.font = `${style} ${this._options.fontSize} ${this._options.fontFamily}` const variantOffset = variantIndex / offsetGlyphVariantCount - const height = lineHeightInPixels - const { width: subpixelWidth } = this._glyphContext.measureText(text) - const width = Math.ceil(variantOffset) + Math.ceil(subpixelWidth) + const height = lineHeightInPixels + 2 * glyphPaddingInPixels + const { width: measuredGlyphWidth } = this._glyphContext.measureText(text) + const width = + Math.ceil(variantOffset) + Math.ceil(measuredGlyphWidth) + 2 * glyphPaddingInPixels - if ((this._nextX + width) * devicePixelRatio > this._textureSize) { + if ((this._nextX + width) * devicePixelRatio > this._options.textureSizeInPixels) { this._nextX = 0 - this._nextY = Math.ceil(this._nextY + height + glyphPaddingInPixels) + this._nextY = Math.ceil(this._nextY + height) } - if ((this._nextY + height) * devicePixelRatio > this._textureSize) { - // TODO implement a fallback instead of just throwing - throw new Error("Texture is too small") + if ((this._nextY + height) * devicePixelRatio > this._options.textureSizeInPixels) { + this._switchToNextLayer() } const x = this._nextX const y = this._nextY - this._glyphContext.fillText(text, x + variantOffset, y + linePaddingInPixels / 2) + this._glyphContext.fillText( + text, + x + glyphPaddingInPixels + variantOffset, + y + glyphPaddingInPixels + linePaddingInPixels / 2, + ) this._nextX += width return { - textureU: x * devicePixelRatio * this._uvScale, - textureV: y * devicePixelRatio * this._uvScale, - textureWidth: width * devicePixelRatio * this._uvScale, - textureHeight: height * devicePixelRatio * this._uvScale, width: width * devicePixelRatio, height: height * devicePixelRatio, - subpixelWidth: subpixelWidth * devicePixelRatio, + textureLayerIndex: this._currentTextureLayerIndex, + textureU: x * devicePixelRatio / this._options.textureSizeInPixels, + textureV: y * devicePixelRatio / this._options.textureSizeInPixels, + textureWidth: width * devicePixelRatio / this._options.textureSizeInPixels, + textureHeight: height * devicePixelRatio / this._options.textureSizeInPixels, + subpixelWidth: measuredGlyphWidth * devicePixelRatio, variantOffset, } as WebGLGlyph } + + private _switchToNextLayer() { + if (this._currentTextureLayerIndex + 1 >= this._options.textureLayerCount) { + throw new WebGLTextureSpaceExceededError( + "The WebGL renderer ran out of texture space. Please re-open the editor " + + "with more texture layers or switch to a different renderer.", + ) + } + + this.uploadTexture() + + this._glyphContext.fillStyle = backgroundColor + this._glyphContext.fillRect( + 0, + 0, + this._glyphContext.canvas.width, + this._glyphContext.canvas.width, + ) + this._glyphContext.fillStyle = foregroundColor + this._currentTextureLayerIndex++ + this._nextX = 0 + this._nextY = 0 + this._currentTextureLayerChangedSinceLastUpload = true + } } + +const getGlyphStyleIndex = (isBold: boolean, isItalic: boolean) => + isBold ? (isItalic ? 3 : 1) : isItalic ? 2 : 0 + +const glyphStyles = [ + "", // regular, 0 + "bold", // 1 + "italic", // 2 + "bold italic", // 3 +] +const getGlyphStyleString = (isBold: boolean, isItalic: boolean) => + glyphStyles[getGlyphStyleIndex(isBold, isItalic)] diff --git a/browser/src/Renderer/WebGL/WebGLRenderer.ts b/browser/src/Renderer/WebGL/WebGLRenderer.ts index b7816bb346..3f585f686c 100644 --- a/browser/src/Renderer/WebGL/WebGLRenderer.ts +++ b/browser/src/Renderer/WebGL/WebGLRenderer.ts @@ -2,7 +2,7 @@ import { INeovimRenderer } from ".." import { IScreen } from "../../neovim" import { CachedColorNormalizer } from "./CachedColorNormalizer" import { IColorNormalizer } from "./IColorNormalizer" -import { IWebGLAtlasOptions } from "./WebGLAtlas" +import { IWebGLAtlasOptions, WebGLTextureSpaceExceededError } from "./WebGLAtlas" import { WebGLSolidRenderer } from "./WebGLSolidRenderer" import { WebGlTextRenderer } from "./WebGLTextRenderer" @@ -10,6 +10,8 @@ export class WebGLRenderer implements INeovimRenderer { private _editorElement: HTMLElement private _colorNormalizer: IColorNormalizer private _previousAtlasOptions: IWebGLAtlasOptions + private _textureSizeInPixels = 1024 + private _textureLayerCount = 2 private _gl: WebGL2RenderingContext private _solidRenderer: WebGLSolidRenderer @@ -20,9 +22,6 @@ export class WebGLRenderer implements INeovimRenderer { this._colorNormalizer = new CachedColorNormalizer() const canvasElement = document.createElement("canvas") - canvasElement.style.width = `100%` - canvasElement.style.height = `100%` - this._editorElement.innerHTML = "" this._editorElement.appendChild(canvasElement) @@ -30,10 +29,24 @@ export class WebGLRenderer implements INeovimRenderer { } public redrawAll(screenInfo: IScreen): void { + if (!this._editorElement) { + return + } + this._updateCanvasDimensions() this._createNewRendererIfRequired(screenInfo) this._clear(screenInfo.backgroundColor) - this._draw(screenInfo) + + try { + this._draw(screenInfo) + } catch (error) { + if (error instanceof WebGLTextureSpaceExceededError) { + this._textureLayerCount *= 2 + this.redrawAll(screenInfo) + } else { + throw error + } + } } public draw(screenInfo: IScreen): void { @@ -46,8 +59,11 @@ export class WebGLRenderer implements INeovimRenderer { private _updateCanvasDimensions() { const devicePixelRatio = window.devicePixelRatio - this._gl.canvas.width = this._editorElement.offsetWidth * devicePixelRatio - this._gl.canvas.height = this._editorElement.offsetHeight * devicePixelRatio + const canvas = this._gl.canvas + canvas.width = this._editorElement.offsetWidth * devicePixelRatio + canvas.height = this._editorElement.offsetHeight * devicePixelRatio + canvas.style.width = `${canvas.width / devicePixelRatio}px` + canvas.style.height = `${canvas.height / devicePixelRatio}px` } private _createNewRendererIfRequired({ @@ -60,14 +76,17 @@ export class WebGLRenderer implements INeovimRenderer { fontSize, }: IScreen) { const devicePixelRatio = window.devicePixelRatio - const offsetGlyphVariantCount = Math.max(4 / devicePixelRatio, 1) + const offsetGlyphVariantCount = Math.max(Math.ceil(4 / devicePixelRatio), 1) const atlasOptions = { fontFamily, fontSize, lineHeightInPixels: fontHeightInPixels, linePaddingInPixels, + glyphPaddingInPixels: Math.ceil(fontHeightInPixels / 4), devicePixelRatio, offsetGlyphVariantCount, + textureSizeInPixels: this._textureSizeInPixels, + textureLayerCount: this._textureLayerCount, } as IWebGLAtlasOptions if ( diff --git a/browser/src/Renderer/WebGL/WebGLSolidRenderer.ts b/browser/src/Renderer/WebGL/WebGLSolidRenderer.ts index dadafec5ed..51abf7aa73 100644 --- a/browser/src/Renderer/WebGL/WebGLSolidRenderer.ts +++ b/browser/src/Renderer/WebGL/WebGLSolidRenderer.ts @@ -6,8 +6,6 @@ import { createUnitQuadVerticesBuffer, } from "./WebGLUtilities" -// tslint:disable-next-line:no-bitwise -const maxCellInstances = 1 << 14 // TODO find a reasonable way of determining this const solidInstanceFieldCount = 8 const solidInstanceSizeInBytes = solidInstanceFieldCount * Float32Array.BYTES_PER_ELEMENT @@ -81,6 +79,8 @@ export class WebGLSolidRenderer { viewportScaleX: number, viewportScaleY: number, ) { + const cellCount = columnCount * rowCount + this.recreateSolidInstancesArrayIfRequired(cellCount) const solidInstanceCount = this.populateSolidInstances( columnCount, rowCount, @@ -95,7 +95,6 @@ export class WebGLSolidRenderer { private createBuffers() { this._unitQuadVerticesBuffer = createUnitQuadVerticesBuffer(this._gl) this._unitQuadElementIndicesBuffer = createUnitQuadElementIndicesBuffer(this._gl) - this._solidInstances = new Float32Array(maxCellInstances * solidInstanceFieldCount) this._solidInstancesBuffer = this._gl.createBuffer() } @@ -152,6 +151,13 @@ export class WebGLSolidRenderer { this._gl.vertexAttribDivisor(vertexShaderAttributes.colorRGBA, 1) } + private recreateSolidInstancesArrayIfRequired(cellCount: number) { + const requiredArrayLength = cellCount * solidInstanceFieldCount + if (!this._solidInstances || this._solidInstances.length < requiredArrayLength) { + this._solidInstances = new Float32Array(requiredArrayLength) + } + } + private populateSolidInstances( columnCount: number, rowCount: number, diff --git a/browser/src/Renderer/WebGL/WebGLTextRenderer.ts b/browser/src/Renderer/WebGL/WebGLTextRenderer.ts index a3a36124ca..c8348fec4e 100644 --- a/browser/src/Renderer/WebGL/WebGLTextRenderer.ts +++ b/browser/src/Renderer/WebGL/WebGLTextRenderer.ts @@ -7,9 +7,7 @@ import { createUnitQuadVerticesBuffer, } from "./WebGLUtilities" -// tslint:disable-next-line:no-bitwise -const maxGlyphInstances = 1 << 14 // TODO find a reasonable way of determining this -const glyphInstanceFieldCount = 12 +const glyphInstanceFieldCount = 13 const glyphInstanceSizeInBytes = glyphInstanceFieldCount * Float32Array.BYTES_PER_ELEMENT const vertexShaderAttributes = { @@ -17,8 +15,9 @@ const vertexShaderAttributes = { targetOrigin: 1, targetSize: 2, textColorRGBA: 3, - atlasOrigin: 4, - atlasSize: 5, + atlasLayerIndex: 4, + atlasOrigin: 5, + atlasSize: 6, } const vertexShaderSource = ` @@ -28,12 +27,14 @@ const vertexShaderSource = ` layout (location = 1) in vec2 targetOrigin; layout (location = 2) in vec2 targetSize; layout (location = 3) in vec4 textColorRGBA; - layout (location = 4) in vec2 atlasOrigin; - layout (location = 5) in vec2 atlasSize; + layout (location = 4) in float atlasLayerIndex; + layout (location = 5) in vec2 atlasOrigin; + layout (location = 6) in vec2 atlasSize; uniform vec2 viewportScale; flat out vec4 textColor; + flat out int convertedAtlasLayerIndex; out vec2 atlasPosition; void main() { @@ -41,6 +42,7 @@ const vertexShaderSource = ` vec2 targetPosition = targetPixelPosition * viewportScale + vec2(-1.0, 1.0); gl_Position = vec4(targetPosition, 0.0, 1.0); textColor = textColorRGBA; + convertedAtlasLayerIndex = int(atlasLayerIndex); atlasPosition = atlasOrigin + unitQuadVertex * atlasSize; } `.trim() @@ -49,15 +51,17 @@ const firstPassFragmentShaderSource = ` #version 300 es precision mediump float; + precision mediump sampler2DArray; layout(location = 0) out vec4 outColor; flat in vec4 textColor; + flat in int convertedAtlasLayerIndex; in vec2 atlasPosition; - uniform sampler2D atlasTexture; + uniform sampler2DArray atlasTextures; void main() { - vec4 atlasColor = texture(atlasTexture, atlasPosition); + vec4 atlasColor = texture(atlasTextures, vec3(atlasPosition, convertedAtlasLayerIndex)); outColor = textColor.a * atlasColor; } `.trim() @@ -66,15 +70,17 @@ const secondPassFragmentShaderSource = ` #version 300 es precision mediump float; + precision mediump sampler2DArray; layout(location = 0) out vec4 outColor; flat in vec4 textColor; + flat in int convertedAtlasLayerIndex; in vec2 atlasPosition; - uniform sampler2D atlasTexture; + uniform sampler2DArray atlasTextures; void main() { - vec3 atlasColor = texture(atlasTexture, atlasPosition).rgb; + vec3 atlasColor = texture(atlasTextures, vec3(atlasPosition, convertedAtlasLayerIndex)).rgb; vec3 outColorRGB = atlasColor * textColor.rgb; float outColorA = max(outColorRGB.r, max(outColorRGB.g, outColorRGB.b)); outColor = vec4(outColorRGB, outColorA); @@ -85,13 +91,16 @@ const isWhiteSpace = (text: string) => text === null || text === "" || text === export class WebGlTextRenderer { private _atlas: WebGLAtlas + private _glyphOverlapInPixels: number private _subpixelDivisor: number private _devicePixelRatio: number private _firstPassProgram: WebGLProgram private _firstPassViewportScaleLocation: WebGLUniformLocation + private _firstPassAtlasTexturesLocation: WebGLUniformLocation private _secondPassProgram: WebGLProgram private _secondPassViewportScaleLocation: WebGLUniformLocation + private _secondPassAtlasTexturesLocation: WebGLUniformLocation private _unitQuadVerticesBuffer: WebGLBuffer private _unitQuadElementIndicesBuffer: WebGLBuffer private _glyphInstances: Float32Array @@ -103,8 +112,9 @@ export class WebGlTextRenderer { private _colorNormalizer: IColorNormalizer, atlasOptions: IWebGLAtlasOptions, ) { - this._devicePixelRatio = atlasOptions.devicePixelRatio + this._glyphOverlapInPixels = atlasOptions.glyphPaddingInPixels this._subpixelDivisor = atlasOptions.offsetGlyphVariantCount + this._devicePixelRatio = atlasOptions.devicePixelRatio this._atlas = new WebGLAtlas(this._gl, atlasOptions) this._firstPassProgram = createProgram( @@ -127,6 +137,15 @@ export class WebGlTextRenderer { "viewportScale", ) + this._firstPassAtlasTexturesLocation = this._gl.getUniformLocation( + this._firstPassProgram, + "atlasTextures", + ) + this._secondPassAtlasTexturesLocation = this._gl.getUniformLocation( + this._secondPassProgram, + "atlasTextures", + ) + this.createBuffers() this.createVertexArrayObject() } @@ -141,6 +160,8 @@ export class WebGlTextRenderer { viewportScaleX: number, viewportScaleY: number, ) { + const cellCount = columnCount * rowCount + this.recreateGlyphInstancesArrayIfRequired(cellCount) const glyphInstanceCount = this.populateGlyphInstances( columnCount, rowCount, @@ -155,7 +176,6 @@ export class WebGlTextRenderer { private createBuffers() { this._unitQuadVerticesBuffer = createUnitQuadVerticesBuffer(this._gl) this._unitQuadElementIndicesBuffer = createUnitQuadElementIndicesBuffer(this._gl) - this._glyphInstances = new Float32Array(maxGlyphInstances * glyphInstanceFieldCount) this._glyphInstancesBuffer = this._gl.createBuffer() } @@ -211,6 +231,17 @@ export class WebGlTextRenderer { ) this._gl.vertexAttribDivisor(vertexShaderAttributes.textColorRGBA, 1) + this._gl.enableVertexAttribArray(vertexShaderAttributes.atlasLayerIndex) + this._gl.vertexAttribPointer( + vertexShaderAttributes.atlasLayerIndex, + 1, + this._gl.FLOAT, + false, + glyphInstanceSizeInBytes, + 8 * Float32Array.BYTES_PER_ELEMENT, + ) + this._gl.vertexAttribDivisor(vertexShaderAttributes.atlasLayerIndex, 1) + this._gl.enableVertexAttribArray(vertexShaderAttributes.atlasOrigin) this._gl.vertexAttribPointer( vertexShaderAttributes.atlasOrigin, @@ -218,7 +249,7 @@ export class WebGlTextRenderer { this._gl.FLOAT, false, glyphInstanceSizeInBytes, - 8 * Float32Array.BYTES_PER_ELEMENT, + 9 * Float32Array.BYTES_PER_ELEMENT, ) this._gl.vertexAttribDivisor(vertexShaderAttributes.atlasOrigin, 1) @@ -229,11 +260,18 @@ export class WebGlTextRenderer { this._gl.FLOAT, false, glyphInstanceSizeInBytes, - 10 * Float32Array.BYTES_PER_ELEMENT, + 11 * Float32Array.BYTES_PER_ELEMENT, ) this._gl.vertexAttribDivisor(vertexShaderAttributes.atlasSize, 1) } + private recreateGlyphInstancesArrayIfRequired(cellCount: number) { + const requiredArrayLength = cellCount * glyphInstanceFieldCount + if (!this._glyphInstances || this._glyphInstances.length < requiredArrayLength) { + this._glyphInstances = new Float32Array(requiredArrayLength) + } + } + private populateGlyphInstances( columnCount: number, rowCount: number, @@ -244,6 +282,7 @@ export class WebGlTextRenderer { ) { const pixelRatioAdaptedFontWidth = fontWidthInPixels * this._devicePixelRatio const pixelRatioAdaptedFontHeight = fontHeightInPixels * this._devicePixelRatio + const pixelRatioAdaptedGlyphOverlap = this._glyphOverlapInPixels * this._devicePixelRatio let glyphCount = 0 let y = 0 @@ -258,14 +297,14 @@ export class WebGlTextRenderer { if (!isWhiteSpace(char)) { const variantIndex = Math.round(x * this._subpixelDivisor) % this._subpixelDivisor - const glyph = this._atlas.getGlyph(char, variantIndex) + const glyph = this._atlas.getGlyph(char, cell.bold, cell.italic, variantIndex) const colorToUse = cell.foregroundColor || defaultForegroundColor || "white" const normalizedTextColor = this._colorNormalizer.normalizeColor(colorToUse) this.updateGlyphInstance( glyphCount, - Math.round(x - glyph.variantOffset), - y, + Math.round(x - glyph.variantOffset) - pixelRatioAdaptedGlyphOverlap, + y - pixelRatioAdaptedGlyphOverlap, glyph, normalizedTextColor, ) @@ -287,9 +326,13 @@ export class WebGlTextRenderer { this._gl.enable(this._gl.BLEND) this._gl.useProgram(this._firstPassProgram) + this._gl.uniform2f(this._firstPassViewportScaleLocation, viewportScaleX, viewportScaleY) + this._gl.uniform1i(this._firstPassAtlasTexturesLocation, 0) + this._gl.bindBuffer(this._gl.ARRAY_BUFFER, this._glyphInstancesBuffer) this._gl.bufferData(this._gl.ARRAY_BUFFER, this._glyphInstances, this._gl.STREAM_DRAW) + this._gl.blendFuncSeparate( this._gl.ZERO, this._gl.ONE_MINUS_SRC_COLOR, @@ -299,13 +342,17 @@ export class WebGlTextRenderer { this._gl.drawElementsInstanced(this._gl.TRIANGLES, 6, this._gl.UNSIGNED_BYTE, 0, glyphCount) this._gl.useProgram(this._secondPassProgram) + this._gl.blendFuncSeparate( this._gl.ONE, this._gl.ONE, this._gl.ONE, this._gl.ONE_MINUS_SRC_ALPHA, ) + this._gl.uniform2f(this._secondPassViewportScaleLocation, viewportScaleX, viewportScaleY) + this._gl.uniform1i(this._secondPassAtlasTexturesLocation, 0) + this._gl.drawElementsInstanced(this._gl.TRIANGLES, 6, this._gl.UNSIGNED_BYTE, 0, glyphCount) } @@ -328,11 +375,13 @@ export class WebGlTextRenderer { this._glyphInstances[5 + startOffset] = color[1] this._glyphInstances[6 + startOffset] = color[2] this._glyphInstances[7 + startOffset] = color[3] + // atlasLayerIndex + this._glyphInstances[8 + startOffset] = glyph.textureLayerIndex // atlasOrigin - this._glyphInstances[8 + startOffset] = glyph.textureU - this._glyphInstances[9 + startOffset] = glyph.textureV + this._glyphInstances[9 + startOffset] = glyph.textureU + this._glyphInstances[10 + startOffset] = glyph.textureV // atlasSize - this._glyphInstances[10 + startOffset] = glyph.textureWidth - this._glyphInstances[11 + startOffset] = glyph.textureHeight + this._glyphInstances[11 + startOffset] = glyph.textureWidth + this._glyphInstances[12 + startOffset] = glyph.textureHeight } } diff --git a/browser/src/Services/AutoClosingPairs.ts b/browser/src/Services/AutoClosingPairs.ts index d710239512..314aa65b32 100644 --- a/browser/src/Services/AutoClosingPairs.ts +++ b/browser/src/Services/AutoClosingPairs.ts @@ -5,6 +5,7 @@ */ import * as Oni from "oni-api" +import * as Log from "oni-core-logging" import { IBuffer } from "./../Editor/BufferManager" import { Configuration } from "./Configuration" @@ -14,8 +15,6 @@ import { LanguageManager } from "./Language" import { NeovimInstance } from "./../neovim" -import * as Log from "./../Log" - export interface IAutoClosingPair { open: string close: string diff --git a/browser/src/Services/Automation.ts b/browser/src/Services/Automation.ts index 7742e5bf94..19600d0d87 100644 --- a/browser/src/Services/Automation.ts +++ b/browser/src/Services/Automation.ts @@ -7,6 +7,7 @@ import { remote } from "electron" import * as OniApi from "oni-api" +import * as Log from "oni-core-logging" import * as App from "./../App" import * as Utility from "./../Utility" @@ -15,8 +16,6 @@ import { getUserConfigFilePath } from "./Configuration" import { editorManager } from "./EditorManager" import { inputManager } from "./InputManager" -import * as Log from "./../Log" - import { IKey, parseKeysFromVimString } from "./../Input/KeyParser" export interface ITestResult { @@ -189,10 +188,21 @@ export class Automation implements OniApi.Automation.Api { this._getOrCreateTestContainer("automated-test-container"), ) - resultElement.textContent = JSON.stringify({ - passed, - exception: exception || null, - }) + if (exception && exception.code && exception.code === "ERR_ASSERTION") { + resultElement.textContent = JSON.stringify({ + passed, + exception, + expected: exception.expected, + actual: exception.actual, + message: exception.message, + operator: exception.operator, + }) + } else { + resultElement.textContent = JSON.stringify({ + passed, + exception: exception || null, + }) + } } private _createElement(className: string, parentElement: HTMLElement): HTMLDivElement { diff --git a/browser/src/Services/Browser/AddressBarView.tsx b/browser/src/Services/Browser/AddressBarView.tsx index c1b0cd3331..283be95240 100644 --- a/browser/src/Services/Browser/AddressBarView.tsx +++ b/browser/src/Services/Browser/AddressBarView.tsx @@ -80,7 +80,7 @@ export class AddressBarView extends React.PureComponent< private _renderAddressSpan(): JSX.Element { return ( - this._setActive()}> + this._setActive()} tag={"browser.address"}> this._setActive()}>{this.props.url} ) diff --git a/browser/src/Services/Browser/BrowserView.tsx b/browser/src/Services/Browser/BrowserView.tsx index f830e0239f..58dfb96ae3 100644 --- a/browser/src/Services/Browser/BrowserView.tsx +++ b/browser/src/Services/Browser/BrowserView.tsx @@ -69,6 +69,9 @@ export interface IBrowserViewProps { scrollDown: IEvent scrollLeft: IEvent scrollRight: IEvent + + webviewRef?: (webviewTag: WebviewTag) => void + onFocusTag?: (tagName: string | null) => void } export interface IBrowserViewState { @@ -324,6 +327,24 @@ export class BrowserView extends React.PureComponent { focusManager.popFocus(this._webviewElement) }) + + this._webviewElement.addEventListener("ipc-message", event => { + switch (event.channel) { + case "focusin": + if (this.props.onFocusTag) { + this.props.onFocusTag(event.args[0]) + } + return + case "focusout": + if (this.props.onFocusTag) { + this.props.onFocusTag(null) + } + } + }) + + if (this.props.webviewRef) { + this.props.webviewRef(this._webviewElement) + } } } } diff --git a/browser/src/Services/Browser/index.tsx b/browser/src/Services/Browser/index.tsx index 02d7b35856..d164714dd8 100644 --- a/browser/src/Services/Browser/index.tsx +++ b/browser/src/Services/Browser/index.tsx @@ -4,15 +4,18 @@ * Entry point for browser integration plugin */ -import { shell } from "electron" +import { ipcRenderer, shell, WebviewTag } from "electron" import * as React from "react" import * as Oni from "oni-api" import { Event } from "oni-types" +import { IBuffer } from "./../../Editor/BufferManager" + import { CommandManager } from "./../CommandManager" import { Configuration } from "./../Configuration" import { EditorManager } from "./../EditorManager" +import { focusManager } from "./../FocusManager" import { AchievementsManager, getInstance as getAchievementsInstance, @@ -30,12 +33,23 @@ export class BrowserLayer implements Oni.BufferLayer { private _scrollRightEvent = new Event() private _scrollLeftEvent = new Event() + private _webview: WebviewTag | null = null + private _activeTagName: string | null = null + constructor(private _url: string, private _configuration: Configuration) {} public get id(): string { return "oni.browser" } + public get webviewElement(): HTMLElement { + return this._webview + } + + public get activeTagName(): string { + return this._activeTagName + } + public render(): JSX.Element { return ( (this._webview = webview)} + onFocusTag={newTag => (this._activeTagName = newTag)} /> ) } @@ -92,8 +108,6 @@ export const activate = ( ) => { let count = 0 - const activeLayers: { [bufferId: string]: BrowserLayer } = {} - const browserEnabledSetting = configuration.registerSetting("browser.enabled", { requiresReload: false, description: @@ -128,7 +142,6 @@ export const activate = ( const layer = new BrowserLayer(url, configuration) buffer.addLayer(layer) - activeLayers[buffer.id] = layer const achievements = getAchievementsInstance() achievements.notifyGoal("oni.goal.openBrowser") @@ -160,18 +173,58 @@ export const activate = ( detail: null, }) + const getLayerForBuffer = (buffer: Oni.Buffer): BrowserLayer => { + return (buffer as IBuffer).getLayerById("oni.browser") + } + const executeCommandForLayer = (callback: (browserLayer: BrowserLayer) => void) => () => { const activeBuffer = editorManager.activeEditor.activeBuffer - const browserLayer = activeLayers[activeBuffer.id] + const browserLayer = getLayerForBuffer(activeBuffer) if (browserLayer) { callback(browserLayer) } } - const isBrowserLayerActive = () => - !!activeLayers[editorManager.activeEditor.activeBuffer.id] && - browserEnabledSetting.getValue() + const isBrowserCommandEnabled = (): boolean => { + if (!browserEnabledSetting.getValue()) { + return false + } + + const layer = getLayerForBuffer(editorManager.activeEditor.activeBuffer) + if (!layer) { + return false + } + + // If the layer is open, but not focused, we shouldn't execute commands. + // This could happen if there is a pop-up menu, or if we're working with some + // non-webview UI in the browser (like the address bar) + if (layer.webviewElement !== focusManager.focusedElement) { + return false + } + + return true + } + + const isInputTag = (tagName: string): boolean => { + return tagName === "INPUT" || tagName === "TEXTAREA" + } + + const isBrowserScrollCommandEnabled = (): boolean => { + if (!isBrowserCommandEnabled()) { + return false + } + + const layer = getLayerForBuffer(editorManager.activeEditor.activeBuffer) + + // Finally, if the webview _is_ focused, but something has focus, we'll + // skip our bindings and defer to the browser + if (isInputTag(layer.activeTagName)) { + return false + } + + return true + } // Per-layer commands commandManager.registerCommand({ @@ -179,7 +232,7 @@ export const activate = ( execute: executeCommandForLayer(browser => browser.openDebugger()), name: "Browser: Open DevTools", detail: "Open the devtools pane for the current browser window.", - enabled: isBrowserLayerActive, + enabled: isBrowserCommandEnabled, }) commandManager.registerCommand({ @@ -187,7 +240,7 @@ export const activate = ( execute: executeCommandForLayer(browser => browser.goBack()), name: "Browser: Go back", detail: "", - enabled: isBrowserLayerActive, + enabled: isBrowserCommandEnabled, }) commandManager.registerCommand({ @@ -195,7 +248,7 @@ export const activate = ( execute: executeCommandForLayer(browser => browser.goForward()), name: "Browser: Go forward", detail: "", - enabled: isBrowserLayerActive, + enabled: isBrowserCommandEnabled, }) commandManager.registerCommand({ @@ -203,7 +256,7 @@ export const activate = ( execute: executeCommandForLayer(browser => browser.reload()), name: "Browser: Reload", detail: "", - enabled: isBrowserLayerActive, + enabled: isBrowserCommandEnabled, }) commandManager.registerCommand({ @@ -211,7 +264,7 @@ export const activate = ( execute: executeCommandForLayer(browser => browser.scrollDown()), name: "Browser: Scroll Down", detail: "", - enabled: isBrowserLayerActive, + enabled: isBrowserScrollCommandEnabled, }) commandManager.registerCommand({ @@ -219,7 +272,7 @@ export const activate = ( execute: executeCommandForLayer(browser => browser.scrollUp()), name: "Browser: Scroll Up", detail: "", - enabled: isBrowserLayerActive, + enabled: isBrowserScrollCommandEnabled, }) commandManager.registerCommand({ @@ -227,7 +280,7 @@ export const activate = ( execute: executeCommandForLayer(browser => browser.scrollLeft()), name: "Browser: Scroll Left", detail: "", - enabled: isBrowserLayerActive, + enabled: isBrowserScrollCommandEnabled, }) commandManager.registerCommand({ @@ -235,7 +288,11 @@ export const activate = ( execute: executeCommandForLayer(browser => browser.scrollRight()), name: "Browser: Scroll Right", detail: "", - enabled: isBrowserLayerActive, + enabled: isBrowserScrollCommandEnabled, + }) + + ipcRenderer.on("open-oni-browser", (event: string, args: string) => { + openUrl(args) }) } diff --git a/browser/src/Services/BrowserWindowConfigurationSynchronizer.ts b/browser/src/Services/BrowserWindowConfigurationSynchronizer.ts index 8ba03bcb96..9d04c03104 100644 --- a/browser/src/Services/BrowserWindowConfigurationSynchronizer.ts +++ b/browser/src/Services/BrowserWindowConfigurationSynchronizer.ts @@ -45,9 +45,13 @@ export const activate = (configuration: Configuration, colors: Colors) => { document.body.style["-webkit-font-smoothing"] = fontSmoothing } - const hideMenu: boolean = configuration.getValue("oni.hideMenu") - browserWindow.setAutoHideMenuBar(hideMenu) - browserWindow.setMenuBarVisibility(!hideMenu) + const hideMenu: boolean | "hidden" = configuration.getValue("oni.hideMenu") + if (hideMenu === "hidden") { + browserWindow.setMenu(null) + } else { + browserWindow.setAutoHideMenuBar(hideMenu) + browserWindow.setMenuBarVisibility(!hideMenu) + } const loadInit: boolean = configuration.getValue("oni.loadInitVim") if (loadInit !== loadInitVim) { diff --git a/browser/src/Services/CommandManager.ts b/browser/src/Services/CommandManager.ts index ef5b91f40f..29dec21f7e 100644 --- a/browser/src/Services/CommandManager.ts +++ b/browser/src/Services/CommandManager.ts @@ -7,8 +7,8 @@ import * as values from "lodash/values" import * as Oni from "oni-api" +import * as Log from "oni-core-logging" -import * as Log from "./../Log" import { INeovimInstance } from "./../neovim" import { ITask, ITaskProvider } from "./Tasks" diff --git a/browser/src/Services/Completion/CompletionsRequestor.ts b/browser/src/Services/Completion/CompletionsRequestor.ts index d42cb47400..c546b12ab7 100644 --- a/browser/src/Services/Completion/CompletionsRequestor.ts +++ b/browser/src/Services/Completion/CompletionsRequestor.ts @@ -6,7 +6,7 @@ import * as types from "vscode-languageserver-types" -import * as Log from "./../../Log" +import * as Log from "oni-core-logging" import * as Helpers from "./../../Plugins/Api/LanguageClient/LanguageClientHelpers" import { LanguageManager } from "./../Language" diff --git a/browser/src/Services/Configuration/Configuration.ts b/browser/src/Services/Configuration/Configuration.ts index e6692b511c..a788745331 100644 --- a/browser/src/Services/Configuration/Configuration.ts +++ b/browser/src/Services/Configuration/Configuration.ts @@ -4,9 +4,9 @@ import { merge } from "lodash" import * as Oni from "oni-api" +import * as Log from "oni-core-logging" import { Event, IDisposable, IEvent } from "oni-types" import { applyDefaultKeyBindings } from "./../../Input/KeyBindings" -import * as Log from "./../../Log" import * as Performance from "./../../Performance" import { diff } from "./../../Utility" diff --git a/browser/src/Services/Configuration/ConfigurationEditor.ts b/browser/src/Services/Configuration/ConfigurationEditor.ts index 35aba1bc16..a5155e9fc4 100644 --- a/browser/src/Services/Configuration/ConfigurationEditor.ts +++ b/browser/src/Services/Configuration/ConfigurationEditor.ts @@ -9,7 +9,7 @@ import * as path from "path" import * as mkdirp from "mkdirp" -import * as Log from "./../../Log" +import * as Log from "oni-core-logging" import { EditorManager } from "./../EditorManager" diff --git a/browser/src/Services/Configuration/DefaultConfiguration.ts b/browser/src/Services/Configuration/DefaultConfiguration.ts index f2acbbf3ef..2b9dc36174 100644 --- a/browser/src/Services/Configuration/DefaultConfiguration.ts +++ b/browser/src/Services/Configuration/DefaultConfiguration.ts @@ -6,6 +6,8 @@ import * as os from "os" +import * as Oni from "oni-api" + import * as path from "path" import * as Platform from "./../../Platform" @@ -55,6 +57,9 @@ const BaseConfiguration: IConfigurationValues = { "experimental.preview.enabled": false, "experimental.welcome.enabled": false, + "experimental.markdownPreview.enabled": false, + "experimental.markdownPreview.autoScroll": true, + "experimental.neovim.transport": "stdio", // TODO: Enable pipe transport for Windows // "experimental.neovim.transport": Platform.isWindows() ? "pipe" : "stdio", @@ -102,12 +107,15 @@ const BaseConfiguration: IConfigurationValues = { "editor.fontLigatures": true, "editor.fontSize": "12px", + "editor.fontWeight": "normal", "editor.fontFamily": "", "editor.linePadding": 2, "editor.quickOpen.execCommand": null, "editor.quickOpen.filterStrategy": "vscode", + "editor.quickOpen.defaultOpenMode": Oni.FileOpenMode.Edit, + "editor.quickOpen.alternativeOpenMode": Oni.FileOpenMode.ExistingTab, "editor.split.mode": "native", @@ -372,7 +380,7 @@ const BaseConfiguration: IConfigurationValues = { "sidebar.enabled": true, "sidebar.default.open": true, - "sidebar.width": "50px", + "sidebar.width": "15em", "sidebar.marks.enabled": false, "sidebar.plugins.enabled": false, @@ -454,7 +462,9 @@ const LinuxConfigOverrides: Partial = { const PlatformConfigOverride = Platform.isWindows() ? WindowsConfigOverrides - : Platform.isLinux() ? LinuxConfigOverrides : MacConfigOverrides + : Platform.isLinux() + ? LinuxConfigOverrides + : MacConfigOverrides export const DefaultConfiguration = { ...BaseConfiguration, diff --git a/browser/src/Services/Configuration/DeprecatedConfigurationValues.ts b/browser/src/Services/Configuration/DeprecatedConfigurationValues.ts index 841feba670..d56d52f2eb 100644 --- a/browser/src/Services/Configuration/DeprecatedConfigurationValues.ts +++ b/browser/src/Services/Configuration/DeprecatedConfigurationValues.ts @@ -6,7 +6,7 @@ * deprecation policy in place - like we'll support deprecated configurations for x releases. */ -import * as Log from "./../../Log" +import * as Log from "oni-core-logging" export interface IDeprecatedConfigurationInfo { replacementConfigurationName: string diff --git a/browser/src/Services/Configuration/FileConfigurationProvider.ts b/browser/src/Services/Configuration/FileConfigurationProvider.ts index d945c175a6..091bfd8695 100644 --- a/browser/src/Services/Configuration/FileConfigurationProvider.ts +++ b/browser/src/Services/Configuration/FileConfigurationProvider.ts @@ -9,13 +9,13 @@ import * as isError from "lodash/isError" import * as mkdirp from "mkdirp" import * as path from "path" +import "rxjs/add/operator/debounceTime" import { Subject } from "rxjs/Subject" import * as Oni from "oni-api" +import * as Log from "oni-core-logging" import { Event, IEvent } from "oni-types" -import * as Log from "./../../Log" - import { IConfigurationProvider } from "./Configuration" import { IConfigurationValues } from "./IConfigurationValues" diff --git a/browser/src/Services/Configuration/IConfigurationValues.ts b/browser/src/Services/Configuration/IConfigurationValues.ts index b515d33454..b760f827c0 100644 --- a/browser/src/Services/Configuration/IConfigurationValues.ts +++ b/browser/src/Services/Configuration/IConfigurationValues.ts @@ -49,6 +49,10 @@ export interface IConfigurationValues { // Whether or not the learning pane is available "experimental.particles.enabled": boolean + // Whether the markdown preview pane should be shown + "experimental.markdownPreview.enabled": boolean + "experimental.markdownPreview.autoScroll": boolean + // The transport to use for Neovim // Valid values are "stdio" and "pipe" "experimental.neovim.transport": string @@ -89,7 +93,8 @@ export interface IConfigurationValues { // If true, hide Menu bar by default // (can still be activated by pressing 'Alt') - "oni.hideMenu": boolean + // If hidden, menu bar is hidden entirely. + "oni.hideMenu": boolean | "hidden" // glob pattern of files to exclude from fuzzy finder (Ctrl-P) "oni.exclude": string[] @@ -131,6 +136,8 @@ export interface IConfigurationValues { "editor.quickInfo.enabled": boolean // Delay (in ms) for showing QuickInfo, when the cursor is on a term "editor.quickInfo.delay": number + "editor.quickOpen.defaultOpenMode": Oni.FileOpenMode + "editor.quickOpen.alternativeOpenMode": Oni.FileOpenMode "editor.errors.slideOnFocus": boolean "editor.formatting.formatOnSwitchToNormalMode": boolean // TODO: Make this setting reliable. If formatting is slow, it will hose edits... not fun @@ -151,6 +158,7 @@ export interface IConfigurationValues { // If true (default), ligatures are enabled "editor.fontLigatures": boolean "editor.fontSize": string + "editor.fontWeight": string "editor.fontFamily": string // Platform specific // Additional padding between lines diff --git a/browser/src/Services/Configuration/UserConfiguration.ts b/browser/src/Services/Configuration/UserConfiguration.ts index ff1b0c5130..f8ad35ca26 100644 --- a/browser/src/Services/Configuration/UserConfiguration.ts +++ b/browser/src/Services/Configuration/UserConfiguration.ts @@ -6,9 +6,9 @@ import * as path from "path" -import * as Platform from "./../../Platform" +import * as Log from "oni-core-logging" -import * as Log from "./../../Log" +import * as Platform from "./../../Platform" export const getUserConfigFilePath = (): string => { const configFileFromEnv = process.env["ONI_CONFIG_FILE"] as string // tslint:disable-line diff --git a/browser/src/Services/EditorManager.ts b/browser/src/Services/EditorManager.ts index 6488bd2ac1..e2642c9ff8 100644 --- a/browser/src/Services/EditorManager.ts +++ b/browser/src/Services/EditorManager.ts @@ -177,6 +177,10 @@ class AnyEditorProxy implements Oni.Editor { return this._activeEditor.getBuffers() } + public setTextOptions(options: Oni.EditorTextOptions): Promise { + return this._activeEditor.setTextOptions(options) + } + /** * Internal methods */ diff --git a/browser/src/Services/Explorer/ExplorerFileSystem.ts b/browser/src/Services/Explorer/ExplorerFileSystem.ts index 7ad60071e7..d651e6f6a5 100644 --- a/browser/src/Services/Explorer/ExplorerFileSystem.ts +++ b/browser/src/Services/Explorer/ExplorerFileSystem.ts @@ -5,7 +5,7 @@ */ import * as fs from "fs" -import { emptyDirSync, ensureDirSync, mkdirp, move, pathExists, remove, writeFile } from "fs-extra" +import { ensureDirSync, mkdirp, move, pathExists, remove, writeFile } from "fs-extra" import * as os from "os" import * as path from "path" import { promisify } from "util" @@ -53,7 +53,6 @@ export class FileSystem implements IFileSystem { public init = () => { ensureDirSync(this._backupDirectory) - emptyDirSync(this._backupDirectory) } public async readdir(directoryPath: string): Promise { @@ -61,8 +60,11 @@ export class FileSystem implements IFileSystem { const filesAndFolders = files.map(async f => { const fullPath = path.join(directoryPath, f) - const stat = await this._fs.stat(fullPath) - if (stat.isDirectory()) { + const isDirectory = await this._fs + .stat(fullPath) + .then(stat => stat.isDirectory()) + .catch(() => false) + if (isDirectory) { return { type: "folder", fullPath, diff --git a/browser/src/Services/Explorer/ExplorerSelectors.ts b/browser/src/Services/Explorer/ExplorerSelectors.ts index 7aef5f63e8..12f549c43b 100644 --- a/browser/src/Services/Explorer/ExplorerSelectors.ts +++ b/browser/src/Services/Explorer/ExplorerSelectors.ts @@ -80,7 +80,7 @@ export const mapStateToNodeList = (state: IExplorerState): ExplorerNode[] => { ret.push({ id: "explorer", type: "container", - expanded: true, + expanded: !!state.expandedFolders[state.rootFolder.fullPath], name: state.rootFolder.fullPath, }) diff --git a/browser/src/Services/Explorer/ExplorerSplit.tsx b/browser/src/Services/Explorer/ExplorerSplit.tsx index 126e11f088..d2810cb321 100644 --- a/browser/src/Services/Explorer/ExplorerSplit.tsx +++ b/browser/src/Services/Explorer/ExplorerSplit.tsx @@ -112,7 +112,9 @@ export class ExplorerSplit { } private _inputInProgress = () => { - const { register: { rename, create } } = this._store.getState() + const { + register: { rename, create }, + } = this._store.getState() return rename.active || create.active } @@ -239,18 +241,16 @@ export class ExplorerSplit { // Should be being called with an ID not an active editor windowManager.focusSplit("oni.window.0") return + case "container": case "folder": - const isDirectoryExpanded = ExplorerSelectors.isPathExpanded( - state, - selectedItem.folderPath, - ) + const directoryPath = + selectedItem.type === "container" ? selectedItem.name : selectedItem.folderPath + const isDirectoryExpanded = ExplorerSelectors.isPathExpanded(state, directoryPath) this._store.dispatch({ type: isDirectoryExpanded ? "COLLAPSE_DIRECTORY" : "EXPAND_DIRECTORY", - directoryPath: selectedItem.folderPath, + directoryPath, }) return - default: - alert("Not implemented yet.") // tslint:disable-line } } @@ -337,7 +337,9 @@ export class ExplorerSplit { } private _onUndoItem(): void { - const { register: { undo } } = this._store.getState() + const { + register: { undo }, + } = this._store.getState() if (undo.length) { this._store.dispatch({ type: "UNDO" }) } @@ -349,7 +351,9 @@ export class ExplorerSplit { return } - const { register: { yank } } = this._store.getState() + const { + register: { yank }, + } = this._store.getState() const inYankRegister = yank.some(({ id }) => id === selectedItem.id) if (!inYankRegister) { @@ -365,7 +369,9 @@ export class ExplorerSplit { return } - const { register: { yank } } = this._store.getState() + const { + register: { yank }, + } = this._store.getState() if (yank.length && pasteTarget) { const sources = yank.map( diff --git a/browser/src/Services/Explorer/ExplorerStore.ts b/browser/src/Services/Explorer/ExplorerStore.ts index e522b86792..e235927aa7 100644 --- a/browser/src/Services/Explorer/ExplorerStore.ts +++ b/browser/src/Services/Explorer/ExplorerStore.ts @@ -16,7 +16,8 @@ import { forkJoin } from "rxjs/observable/forkJoin" import { fromPromise } from "rxjs/observable/fromPromise" import { timer } from "rxjs/observable/timer" -import * as Log from "./../../Log" +import * as Log from "oni-core-logging" + import { createStore as createReduxStore } from "./../../Redux" import { configuration } from "./../Configuration" import { EmptyNode, ExplorerNode } from "./ExplorerSelectors" @@ -801,7 +802,9 @@ const persistOrDeleteNode = async ( export const undoEpic: ExplorerEpic = (action$, store, { fileSystem }) => action$.ofType("UNDO").mergeMap(action => { - const { register: { undo } } = store.getState() + const { + register: { undo }, + } = store.getState() const lastAction = last(undo) switch (lastAction.type) { @@ -919,7 +922,11 @@ const expandDirectoryEpic: ExplorerEpic = (action$, store, { fileSystem }) => export const createNodeEpic: ExplorerEpic = (action$, store, { fileSystem }) => action$.ofType("CREATE_NODE_COMMIT").mergeMap(({ name }: ICreateNodeCommitAction) => { - const { register: { create: { nodeType } } } = store.getState() + const { + register: { + create: { nodeType }, + }, + } = store.getState() const shouldExpand = Actions.expandDirectory(path.dirname(name)) const createFileOrFolder = nodeType === "file" ? fileSystem.writeFile(name) : fileSystem.mkdir(name) diff --git a/browser/src/Services/FileSystemWatcher/index.ts b/browser/src/Services/FileSystemWatcher/index.ts index b3210aadd4..ddf967c4dd 100644 --- a/browser/src/Services/FileSystemWatcher/index.ts +++ b/browser/src/Services/FileSystemWatcher/index.ts @@ -2,7 +2,7 @@ import * as chokidar from "chokidar" import { Stats } from "fs" import { Event, IEvent } from "oni-types" -import * as Log from "./../../Log" +import * as Log from "oni-core-logging" export type Targets = string | string[] diff --git a/browser/src/Services/FocusManager.ts b/browser/src/Services/FocusManager.ts index 9671789883..6321624c37 100644 --- a/browser/src/Services/FocusManager.ts +++ b/browser/src/Services/FocusManager.ts @@ -2,11 +2,15 @@ * FocusManager.ts */ -import * as Log from "./../Log" +import * as Log from "oni-core-logging" class FocusManager { private _focusElementStack: HTMLElement[] = [] + public get focusedElement(): HTMLElement | null { + return this._focusElementStack.length > 0 ? this._focusElementStack[0] : null + } + public pushFocus(element: HTMLElement) { this._focusElementStack = [element, ...this._focusElementStack] diff --git a/browser/src/Services/IconThemes/IconThemeLoader.ts b/browser/src/Services/IconThemes/IconThemeLoader.ts index fb58b5bc22..9e675e4602 100644 --- a/browser/src/Services/IconThemes/IconThemeLoader.ts +++ b/browser/src/Services/IconThemes/IconThemeLoader.ts @@ -5,11 +5,13 @@ */ import * as fs from "fs" + +import * as Log from "oni-core-logging" + import { IIconThemeContribution } from "./../../Plugins/Api/Capabilities" import { IIconTheme } from "./Icons" -import * as Log from "./../../Log" import { PluginManager } from "./../../Plugins/PluginManager" export interface IIconThemeLoadResult { diff --git a/browser/src/Services/InputManager.ts b/browser/src/Services/InputManager.ts index 2d188b7907..b6c1b16160 100644 --- a/browser/src/Services/InputManager.ts +++ b/browser/src/Services/InputManager.ts @@ -8,8 +8,6 @@ export type ActionOrCommand = string | ActionFunction export type FilterFunction = () => boolean -import { IKeyChord, parseKeysFromVimString } from "./../Input/KeyParser" - export interface KeyBinding { action: ActionOrCommand filter?: FilterFunction @@ -20,6 +18,7 @@ export interface KeyBindingMap { } const MAX_DELAY_BETWEEN_KEY_CHORD = 250 /* milliseconds */ +const MAX_CHORD_SIZE = 4 import { KeyboardResolver } from "./../Input/Keyboard/KeyboardResolver" @@ -39,7 +38,7 @@ export const getRecentKeyPresses = ( keys: KeyPressInfo[], maxTimeBetweenKeyPresses: number, ): KeyPressInfo[] => { - return keys.reduce( + const chords = keys.reduce( (prev, curr) => { if (prev.length === 0) { return [curr] @@ -55,6 +54,8 @@ export const getRecentKeyPresses = ( }, [] as KeyPressInfo[], ) + + return chords.slice(0, MAX_CHORD_SIZE) } export class InputManager implements Oni.Input.InputManager { @@ -100,7 +101,7 @@ export class InputManager implements Oni.Input.InputManager { public unbind(keyChord: string | string[]) { if (Array.isArray(keyChord)) { - keyChord.forEach(key => this.unbind(keyChord)) + keyChord.forEach(key => this.unbind(key)) return } @@ -138,10 +139,6 @@ export class InputManager implements Oni.Input.InputManager { ) } - public parseKeys(keys: string): IKeyChord { - return parseKeysFromVimString(keys) - } - /** * Internal Methods */ diff --git a/browser/src/Services/KeyDisplayer/KeyDisplayer.tsx b/browser/src/Services/KeyDisplayer/KeyDisplayer.tsx index d08b55e799..65d5f1b5c9 100644 --- a/browser/src/Services/KeyDisplayer/KeyDisplayer.tsx +++ b/browser/src/Services/KeyDisplayer/KeyDisplayer.tsx @@ -10,6 +10,7 @@ import { Store } from "redux" import { IDisposable } from "oni-types" +import { parseChordParts } from "./../../Input/KeyParser" import { Configuration } from "./../Configuration" import { EditorManager } from "./../EditorManager" import { InputManager } from "./../InputManager" @@ -53,7 +54,7 @@ export class KeyDisplayer { this._store.dispatch({ type: "ADD_KEY", - key: resolution, + key: parseChordParts(resolution).join("+"), timeInMilliseconds: new Date().getTime(), }) diff --git a/browser/src/Services/KeyDisplayer/KeyDisplayerView.tsx b/browser/src/Services/KeyDisplayer/KeyDisplayerView.tsx index 9db13aac8c..71c4e1b58d 100644 --- a/browser/src/Services/KeyDisplayer/KeyDisplayerView.tsx +++ b/browser/src/Services/KeyDisplayer/KeyDisplayerView.tsx @@ -29,22 +29,11 @@ export interface IKeyDisplayerViewProps { groupedKeys: IKeyPressInfo[][] } -const getStringForKey = (key: string) => { - if (key === "") { - return " " - } - - return key -} - export class KeyDisplayerView extends React.PureComponent { public render(): JSX.Element { const keyElements = this.props.groupedKeys.map((k, idx) => ( - {k.reduce( - (prev: string, cur: IKeyPressInfo) => prev + getStringForKey(cur.key), - "", - )} + {k.map(keyPress => keyPress.key).join(" ")} )) diff --git a/browser/src/Services/Language/CodeAction.ts b/browser/src/Services/Language/CodeAction.ts index 7be0e921e6..5baa8fb50f 100644 --- a/browser/src/Services/Language/CodeAction.ts +++ b/browser/src/Services/Language/CodeAction.ts @@ -6,6 +6,7 @@ // import * as os from "os" import * as types from "vscode-languageserver-types" +import * as Log from "oni-core-logging" // import { configuration } from "./../Configuration" // import * as UI from "./../../UI" @@ -13,7 +14,6 @@ import * as types from "vscode-languageserver-types" // import { contextMenuManager } from "./../ContextMenu" import * as LanguageManager from "./LanguageManager" -import * as Log from "./../../Log" import { editorManager } from "./../EditorManager" import * as Helpers from "./../../Plugins/Api/LanguageClient/LanguageClientHelpers" diff --git a/browser/src/Services/Language/DefinitionRequestor.ts b/browser/src/Services/Language/DefinitionRequestor.ts index 093e08fc90..80ca308403 100644 --- a/browser/src/Services/Language/DefinitionRequestor.ts +++ b/browser/src/Services/Language/DefinitionRequestor.ts @@ -7,8 +7,8 @@ import * as types from "vscode-languageserver-types" import * as Oni from "oni-api" +import * as Log from "oni-core-logging" -import * as Log from "./../../Log" import * as Helpers from "./../../Plugins/Api/LanguageClient/LanguageClientHelpers" import { LanguageManager } from "./LanguageManager" diff --git a/browser/src/Services/Language/Formatting.ts b/browser/src/Services/Language/Formatting.ts index c9b13e049b..c074ce9100 100644 --- a/browser/src/Services/Language/Formatting.ts +++ b/browser/src/Services/Language/Formatting.ts @@ -4,7 +4,8 @@ import * as types from "vscode-languageserver-types" -import * as Log from "./../../Log" +import * as Log from "oni-core-logging" + import * as Helpers from "./../../Plugins/Api/LanguageClient/LanguageClientHelpers" import { editorManager } from "./../EditorManager" diff --git a/browser/src/Services/Language/HoverRequestor.ts b/browser/src/Services/Language/HoverRequestor.ts index 234f358312..f06cfc2ba8 100644 --- a/browser/src/Services/Language/HoverRequestor.ts +++ b/browser/src/Services/Language/HoverRequestor.ts @@ -6,7 +6,8 @@ import * as types from "vscode-languageserver-types" -import * as Log from "./../../Log" +import * as Log from "oni-core-logging" + import * as Helpers from "./../../Plugins/Api/LanguageClient/LanguageClientHelpers" import { LanguageManager } from "./LanguageManager" diff --git a/browser/src/Services/Language/LanguageClient.ts b/browser/src/Services/Language/LanguageClient.ts index 74a2ea702b..1c495a8352 100644 --- a/browser/src/Services/Language/LanguageClient.ts +++ b/browser/src/Services/Language/LanguageClient.ts @@ -1,9 +1,8 @@ import * as rpc from "vscode-jsonrpc" +import * as Log from "oni-core-logging" import { Event } from "oni-types" -import * as Log from "./../../Log" - import { ILanguageClientProcess } from "./LanguageClientProcess" import { PromiseQueue } from "./PromiseQueue" import { IServerCapabilities } from "./ServerCapabilities" diff --git a/browser/src/Services/Language/LanguageClientProcess.ts b/browser/src/Services/Language/LanguageClientProcess.ts index 0969119fa1..43cd70747f 100644 --- a/browser/src/Services/Language/LanguageClientProcess.ts +++ b/browser/src/Services/Language/LanguageClientProcess.ts @@ -14,10 +14,9 @@ import * as path from "path" import { ChildProcess } from "child_process" import * as rpc from "vscode-jsonrpc" +import * as Log from "oni-core-logging" import { Event, IEvent } from "oni-types" -import * as Log from "./../../Log" - import { normalizePath } from "./../../Utility" import { LanguageClientLogger } from "./../../Plugins/Api/LanguageClient/LanguageClientLogger" diff --git a/browser/src/Services/Language/LanguageConfiguration.ts b/browser/src/Services/Language/LanguageConfiguration.ts index 46c29cad44..624fa7a9ff 100644 --- a/browser/src/Services/Language/LanguageConfiguration.ts +++ b/browser/src/Services/Language/LanguageConfiguration.ts @@ -4,7 +4,7 @@ * Helper for registering language client information from config */ -import * as Log from "./../../Log" +import * as Log from "oni-core-logging" import { LanguageClient } from "./LanguageClient" import { diff --git a/browser/src/Services/Language/LanguageManager.ts b/browser/src/Services/Language/LanguageManager.ts index 315d07ec10..38b13bb752 100644 --- a/browser/src/Services/Language/LanguageManager.ts +++ b/browser/src/Services/Language/LanguageManager.ts @@ -11,10 +11,9 @@ import * as os from "os" import * as path from "path" import * as Oni from "oni-api" +import * as Log from "oni-core-logging" import { Event, IDisposable } from "oni-types" -import * as Log from "./../../Log" - import { ILanguageClient } from "./LanguageClient" import * as LanguageClientTypes from "./LanguageClientTypes" import { IServerCapabilities } from "./ServerCapabilities" diff --git a/browser/src/Services/Language/PromiseQueue.ts b/browser/src/Services/Language/PromiseQueue.ts index 87df4f9c54..40d7a34dad 100644 --- a/browser/src/Services/Language/PromiseQueue.ts +++ b/browser/src/Services/Language/PromiseQueue.ts @@ -1,4 +1,4 @@ -import * as Log from "./../../Log" +import * as Log from "oni-core-logging" export class PromiseQueue { private _currentPromise: Promise = Promise.resolve(null) diff --git a/browser/src/Services/Language/SignatureHelp.ts b/browser/src/Services/Language/SignatureHelp.ts index feb956f320..92c291b9b5 100644 --- a/browser/src/Services/Language/SignatureHelp.ts +++ b/browser/src/Services/Language/SignatureHelp.ts @@ -7,8 +7,8 @@ import { Observable } from "rxjs/Observable" import * as types from "vscode-languageserver-types" import * as Oni from "oni-api" +import * as Log from "oni-core-logging" -import * as Log from "./../../Log" import * as Helpers from "./../../Plugins/Api/LanguageClient/LanguageClientHelpers" import { IToolTipsProvider } from "./../../Editor/NeovimEditor/ToolTipsProvider" diff --git a/browser/src/Services/Language/SignatureHelpView.tsx b/browser/src/Services/Language/SignatureHelpView.tsx index 4356f308b3..6fe776500f 100644 --- a/browser/src/Services/Language/SignatureHelpView.tsx +++ b/browser/src/Services/Language/SignatureHelpView.tsx @@ -2,20 +2,15 @@ import * as React from "react" import * as types from "vscode-languageserver-types" -import { QuickInfoDocumentation, Title } from "./../../UI/components/QuickInfo" +import { + QuickInfoDocumentation, + QuickInfoElement, + QuickInfoWrapper, + Title, +} from "./../../UI/components/QuickInfo" import { SelectedText, Text } from "./../../UI/components/Text" -export class SignatureHelpView extends React.PureComponent { - public render(): JSX.Element { - return ( -
    -
    {getElementsFromType(this.props)}
    -
    - ) - } -} - -export const getElementsFromType = (signatureHelp: types.SignatureHelp): JSX.Element[] => { +export const getElementsFromType = (signatureHelp: types.SignatureHelp): JSX.Element => { const elements = [] const currentItem = signatureHelp.signatures[signatureHelp.activeSignature] @@ -62,21 +57,27 @@ export const getElementsFromType = (signatureHelp: types.SignatureHelp): JSX.Ele elements.push() - const titleContents = [ - - {elements} - , - ] - const selectedIndex = Math.min(currentItem.parameters.length, signatureHelp.activeParameter) const selectedArgument = currentItem.parameters[selectedIndex] - if (selectedArgument && selectedArgument.documentation) { - titleContents.push() - } - return titleContents + return ( + + + {elements} + + {!!(selectedArgument && selectedArgument.documentation) && ( + + )} + + ) } -export const render = (signatureHelp: types.SignatureHelp) => { - return -} +export const SignatureHelpView = (props: types.SignatureHelp) => ( + + {getElementsFromType(props)} + +) + +export const render = (signatureHelp: types.SignatureHelp) => ( + +) diff --git a/browser/src/Services/Learning/Achievements/AchievementNotificationRenderer.tsx b/browser/src/Services/Learning/Achievements/AchievementNotificationRenderer.tsx index 872a205c2b..4a6053f828 100644 --- a/browser/src/Services/Learning/Achievements/AchievementNotificationRenderer.tsx +++ b/browser/src/Services/Learning/Achievements/AchievementNotificationRenderer.tsx @@ -37,8 +37,11 @@ export class AchievementNotificationRenderer { const AchievementsWrapper = styled.div` & .achievements { - width: 100%; - height: 100%; + position: absolute; + top: 0px; + left: 0px; + right: 0px; + bottom: 0px; display: flex; flex-direction: column; diff --git a/browser/src/Services/Learning/Achievements/AchievementsManager.ts b/browser/src/Services/Learning/Achievements/AchievementsManager.ts index 9151269416..83174d5958 100644 --- a/browser/src/Services/Learning/Achievements/AchievementsManager.ts +++ b/browser/src/Services/Learning/Achievements/AchievementsManager.ts @@ -107,10 +107,13 @@ export class AchievementsManager { } public clearAchievements(): void { - this._persistentStore.set({ + const clearedState: IPersistedAchievementState = { goalCounts: {}, achievedIds: [], - }) + } + + this._goalState = clearedState + this._persistentStore.set(clearedState) } public registerAchievement(definition: AchievementDefinition): void { diff --git a/browser/src/Services/Learning/Tutorial/Notes.tsx b/browser/src/Services/Learning/Tutorial/Notes.tsx index 383a21d300..f01917b0b5 100644 --- a/browser/src/Services/Learning/Tutorial/Notes.tsx +++ b/browser/src/Services/Learning/Tutorial/Notes.tsx @@ -112,6 +112,10 @@ export const OKey = (): JSX.Element => { ) } +export const UKey = (): JSX.Element => { + return Undo a single change} /> +} + export const GGKey = (): JSX.Element => { return ( { ) } +export const ChangeOperatorKey = (): JSX.Element => { + return ( + + + motion: Change text specified by a `motion`. Examples: + + } + /> + ) +} +export const ChangeWordKey = (): JSX.Element => { + return ( + Delete to the end of the current word and enter Insert mode.} + /> + ) +} + export const HJKLKeys = (): JSX.Element => { return ( @@ -324,3 +349,180 @@ export const PasteKey = (): JSX.Element => { Paste BEFORE the cursor} /> ) } + +export const VisualModeKey = (): JSX.Element => { + return ( + Move into Visual mode for selecting text} + /> + ) +} +export const VisualLineModeKey = (): JSX.Element => { + return ( + Move into line-wise Visual mode for selecting lines} + /> + ) +} + +export const Targetckey = (): JSX.Element => { + return ( + Delete AND INSERT between next pair characters} + /> + ) +} + +export const Targetdkey = (): JSX.Element => { + return ( + Delete between next pair characters} + /> + ) +} + +export const Targetikey = (): JSX.Element => { + return ( + Select first character inside of pair characters} + /> + ) +} + +export const Targetakey = (): JSX.Element => { + return ( + Select next pair including the pair characters} + /> + ) +} + +export const TargetIkey = (): JSX.Element => { + return ( + Select contents of pair characters} + /> + ) +} + +export const TargetAkey = (): JSX.Element => { + return ( + Select around the pair characters} + /> + ) +} + +export const Targetnkey = (): JSX.Element => { + return ( + Select the next pair characters} + /> + ) +} + +export const Targetlkey = (): JSX.Element => { + return ( + Select the previous pair characters} + /> + ) +} + +export const fKey = (): JSX.Element => { + return ( + + + char: Moves cursor to next occurence of [char]. + + } + /> + ) +} + +export const FKey = (): JSX.Element => { + return ( + + + char: Moves cursor to previous occurence of [char]. + + } + /> + ) +} + +export const tKey = (): JSX.Element => { + return ( + + + char: Moves cursor to before the next occurence of [char]. + + } + /> + ) +} + +export const TKey = (): JSX.Element => { + return ( + + + char: Moves cursor to after the previous occurence of [char]. + + } + /> + ) +} + +export const RepeatKey = (): JSX.Element => { + return ( + Repeats last f, t, F, or T.} + /> + ) +} + +export const RepeatOppositeKey = (): JSX.Element => { + return ( + Repeats last f, t, F, or T in the opposite direction.} + /> + ) +} + +export const innerTextObjectKey = (): JSX.Element => { + return ( + Select a Text Object within delimiter characters} + /> + ) +} + +export const aTextObjectKey = (): JSX.Element => { + return ( + Select a Text Object and its delimiter characters} + /> + ) +} diff --git a/browser/src/Services/Learning/Tutorial/TutorialBufferLayer.tsx b/browser/src/Services/Learning/Tutorial/TutorialBufferLayer.tsx index dae057e180..6c56aed169 100644 --- a/browser/src/Services/Learning/Tutorial/TutorialBufferLayer.tsx +++ b/browser/src/Services/Learning/Tutorial/TutorialBufferLayer.tsx @@ -337,13 +337,14 @@ const MainTutorialSectionWrapper = styled.div` flex: 1 1 auto; width: 100%; height: 100%; + min-height: 275px; display: flex; align-items: center; ` const PrimaryHeader = styled.div` - padding-top: 2em; + padding-top: 1em; font-size: 2em; ` @@ -361,6 +362,14 @@ const Section = styled.div` padding-bottom: 2em; ` +const DescriptionWrapper = styled.div` + display: none; + + @media (min-height: 800px) { + display: block; + } +` + export interface IModeStatusBarItemProps { mode: string } @@ -469,7 +478,7 @@ export class TutorialBufferLayerView extends React.PureComponent<
    - Description: -
    {description}
    + + Description: +
    {description}
    +
    Goals:
    {goalsToDisplay}
    diff --git a/browser/src/Services/Learning/Tutorial/Tutorials/BeginningsAndEndingsTutorial.tsx b/browser/src/Services/Learning/Tutorial/Tutorials/BeginningsAndEndingsTutorial.tsx index ef34601550..8b2dbec4bd 100644 --- a/browser/src/Services/Learning/Tutorial/Tutorials/BeginningsAndEndingsTutorial.tsx +++ b/browser/src/Services/Learning/Tutorial/Tutorials/BeginningsAndEndingsTutorial.tsx @@ -40,8 +40,8 @@ export class BeginningsAndEndingsTutorial implements ITutorial { 2, Line3.length - 1, ), - new Stages.MoveToGoalStage("Use '0' to move to the BEGINNING of the line", 2, 0), new Stages.MoveToGoalStage("Use '_' to move to the FIRST CHARACTER", 2, 4), + new Stages.MoveToGoalStage("Use '0' to move to the BEGINNING of the line", 2, 0), new Stages.MoveToGoalStage( "Use '$' to move to the END of the line", 2, @@ -55,8 +55,8 @@ export class BeginningsAndEndingsTutorial implements ITutorial { id: "oni.tutorials.beginnings_and_endings", name: "Motion: _, 0, $", description: - "Using `h` and `l` isn't always the most efficient way to get around a line. You can use the `0` key to move to the very beginning a line, and `$` to move to the end. `_` moves to the first character in the line, which is often more convenient than `0`.", - level: 160, + "You don't need to keep hitting `w` or `b` when you need to go all the way to the beginning or the end of a line. You can use the `0` key to move to the very beginning a line, and `$` to move to the end. Also, `_` moves to the first character in the line, which is often more convenient than `0`.", + level: 140, } } diff --git a/browser/src/Services/Learning/Tutorial/Tutorials/ChangeOperatorTutorial.tsx b/browser/src/Services/Learning/Tutorial/Tutorials/ChangeOperatorTutorial.tsx new file mode 100644 index 0000000000..5525967a5a --- /dev/null +++ b/browser/src/Services/Learning/Tutorial/Tutorials/ChangeOperatorTutorial.tsx @@ -0,0 +1,70 @@ +/** + * ChangeOperatorTutorial.tsx + * + * Tutorial that exercises the change operator + */ + +import * as React from "react" + +import { ITutorial, ITutorialMetadata, ITutorialStage } from "./../ITutorial" +import * as Notes from "./../Notes" +import * as Stages from "./../Stages" + +const Line1 = "The change operator can be used for quikcly fixing typos" +const Line1Marker = "The change operator can be used for ".length +const Line1Pending = "The change operator can be used for fixing typos" +const Line1Fixed = "The change operator can be used for quickly fixing typos" +const Line2 = "Learning Vim can be tedious and repetitive" +const Line2Fix1 = "Learning Vim can be fun and repetitive" +const Line2Fix2 = "Learning Vim can be fun and exciting" + +export class ChangeOperatorTutorial implements ITutorial { + private _stages: ITutorialStage[] + + constructor() { + this._stages = [ + new Stages.SetBufferStage([Line1]), + new Stages.MoveToGoalStage("Move to the goal marker", 0, Line1Marker), + new Stages.WaitForStateStage("Fix the typo by hitting 'cw'", [Line1Pending]), + new Stages.WaitForStateStage("Enter the word 'quickly'", [Line1Fixed]), + new Stages.WaitForModeStage("Exit Insert mode by hitting ", "normal"), + new Stages.SetBufferStage([Line1Fixed, Line2]), + new Stages.MoveToGoalStage("Move to the goal marker", 1, Line2.indexOf("tedious")), + new Stages.WaitForStateStage("Change 'tedious' to 'fun'", [Line1Fixed, Line2Fix1]), + new Stages.WaitForModeStage("Exit Insert mode by hitting ", "normal"), + new Stages.MoveToGoalStage( + "Move to the goal marker", + 1, + Line2Fix1.indexOf("repetitive"), + ), + new Stages.WaitForStateStage("Change 'repetitive' to 'exciting'", [ + Line1Fixed, + Line2Fix2, + ]), + new Stages.WaitForModeStage("Exit Insert mode by hitting ", "normal"), + ] + } + + public get metadata(): ITutorialMetadata { + return { + id: "oni.tutorials.change_operator", + name: "Change Operator: c", + description: + "Now that you know about operators and motions pairing like a noun and a verb, we can start learning more operators. The `c` operator allows you to _change_ text. It deletes the selected text and immediately enters Insert mode so you can enter new text. The text to be changed is defined by any motion just like the delete operator. It might not seem very impressive right now but `c` will become more useful as you learn more motions.", + level: 210, + } + } + + public get stages(): ITutorialStage[] { + return this._stages + } + + public get notes(): JSX.Element[] { + return [ + , + , + , + , + ] + } +} diff --git a/browser/src/Services/Learning/Tutorial/Tutorials/CopyPasteTutorial.tsx b/browser/src/Services/Learning/Tutorial/Tutorials/CopyPasteTutorial.tsx index 216ee39044..9106cc5a1d 100644 --- a/browser/src/Services/Learning/Tutorial/Tutorials/CopyPasteTutorial.tsx +++ b/browser/src/Services/Learning/Tutorial/Tutorials/CopyPasteTutorial.tsx @@ -16,7 +16,12 @@ const Line2YankMarker = "Any deleted ".length const Line2PasteMarker = "Any deleted text or yanked".length const Line2PostPaste1 = "Any deleted text or yanked text can then be pasted with 'p'" const Line2PostPaste2 = "text Any deleted text or yanked text can then be pasted with 'p'" -const Line1PostTranspose = "iLke the 'd' operator, 'y' can be used to yank (copy) text" + +const TransposeLine = "Sipmle tpyos can aslo be fiexd with 'xp'" +const TransposeLine1 = "Simple tpyos can aslo be fiexd with 'xp'" +const TransposeLine2 = "Simple typos can aslo be fiexd with 'xp'" +const TransposeLine3 = "Simple typos can also be fiexd with 'xp'" +const TransposeLine4 = "Simple typos can also be fixed with 'xp'" export class CopyPasteTutorial implements ITutorial { private _stages: ITutorialStage[] @@ -64,10 +69,22 @@ export class CopyPasteTutorial implements ITutorial { Line1, Line2PostPaste2, ]), + new Stages.WaitForStateStage( + "Copied text can be pasted multiple times, past again with 'p'", + [Line2PostPaste2, Line2PostPaste2, Line1, Line1, Line2PostPaste2], + ), + new Stages.SetBufferStage([TransposeLine]), + new Stages.MoveToGoalStage("Move to the first typo", 0, 2), new Stages.WaitForStateStage( "Since deleted text is also copied, transposing characters is simple. Try 'xp'", - [Line2PostPaste2, Line2PostPaste2, Line1PostTranspose, Line2PostPaste2], + [TransposeLine1], ), + new Stages.MoveToGoalStage("Move to the next typo", 0, 8), + new Stages.WaitForStateStage("Again, fix the typo with 'xp'", [TransposeLine2]), + new Stages.MoveToGoalStage("Move to the next typo", 0, 18), + new Stages.WaitForStateStage("Again, fix the typo with 'xp'", [TransposeLine3]), + new Stages.MoveToGoalStage("Move to the next typo", 0, 27), + new Stages.WaitForStateStage("Again, fix the typo with 'xp'", [TransposeLine4]), ] } @@ -76,8 +93,8 @@ export class CopyPasteTutorial implements ITutorial { id: "oni.tutorials.copy_paste", name: "Copy & Paste: y, p", description: - 'Now that you know about operators and motions pairing like a noun and a verb, we can start learning new operators. The `y` operator can be used to copy ("yank") text which can then be pasted with `p`. Using `p` pastes _after_ the cursor, and `P` pastes _before_ the cursor. The `y` operator behaves just like the `d` operator and can be paired with any motion.', - level: 210, + "Now that you know the delete and change operators, let's learn vim's final operator: `y`. The `y` operator can be used to copy (\"yank\") text which can then be pasted with `p`. Using `p` pastes _after_ the cursor, and `P` pastes _before_ the cursor. The `y` operator behaves just like the `d` and `c` operators and can be paired with any motion.", + level: 220, } } diff --git a/browser/src/Services/Learning/Tutorial/Tutorials/InlineFindingTutorial.tsx b/browser/src/Services/Learning/Tutorial/Tutorials/InlineFindingTutorial.tsx new file mode 100644 index 0000000000..aa33c50e66 --- /dev/null +++ b/browser/src/Services/Learning/Tutorial/Tutorials/InlineFindingTutorial.tsx @@ -0,0 +1,75 @@ +/** + * TutorialManager + */ + +import * as React from "react" + +import { ITutorial, ITutorialMetadata, ITutorialStage } from "./../ITutorial" +// import { InitializeBufferStage, MoveToGoalStage } from "./../Stages" + +import * as Notes from "./../Notes" +import * as Stages from "./../Stages" + +const Line1 = "Use 'f' to move to the next occurrence of a character within the same line." +const Line2 = "And use 'F' to move to the previous occurrence of a character." +const Line3 = "'t' is like 'f' except it moves to one spot before the character." +const Line4 = "And 'T' is like 'F' except it moves one spot after." +const Line5 = "Awesome! You can also use ';' to repeat the last f, t, F, or T." +const Line6 = "Now use ',' to repeat the last f, t, F, or T in the opposite direction." + +export class InlineFindingTutorial implements ITutorial { + private _stages: ITutorialStage[] + + constructor() { + this._stages = [ + new Stages.SetBufferStage([Line1]), + new Stages.MoveToGoalStage("Move to the 'n' in 'next' using 'fn'", 0, 23), + new Stages.SetBufferStage([Line1, Line2]), + new Stages.MoveToGoalStage("Use 'j' to move down a line", 1, 23), + new Stages.MoveToGoalStage("Use 'F' to move LEFT to the goal", 1, 15), + new Stages.SetBufferStage([Line1, Line2, Line3]), + new Stages.MoveToGoalStage("Use 'j' to move down a line", 2, 15), + new Stages.MoveToGoalStage("Move to the character before 'b' using 'tb'", 2, 43), + new Stages.SetBufferStage([Line1, Line2, Line3, Line4]), + new Stages.MoveToGoalStage("Use 'j' to move down a line", 3, 43), + new Stages.MoveToGoalStage("Use 'T' to move to the goal", 3, 12), + new Stages.SetBufferStage([Line1, Line2, Line3, Line4, Line5]), + new Stages.MoveToGoalStage("Use 'j' to move down a line", 4, 12), + new Stages.MoveToGoalStage("Use 'f' to move to the goal", 4, 24), + new Stages.MoveToGoalStage("Use ';' to move to the goal", 4, 34), + new Stages.MoveToGoalStage("Use ';' to move to the goal", 4, 36), + new Stages.MoveToGoalStage("Use ';' to move to the goal", 4, 42), + new Stages.SetBufferStage([Line1, Line2, Line3, Line4, Line5, Line6]), + new Stages.MoveToGoalStage("Use 'j' to move down a line", 5, 42), + new Stages.MoveToGoalStage("Use ',' to move to the goal", 5, 24), + new Stages.MoveToGoalStage("Use ',' to move to the goal", 5, 18), + new Stages.MoveToGoalStage("Use ',' to move to the goal", 5, 16), + new Stages.MoveToGoalStage("Use ',' to move to the goal", 5, 6), + ] + } + + public get metadata(): ITutorialMetadata { + return { + id: "oni.tutorials.inline_finding", + name: "Motion: f, F, t, T", + description: + "Sometimes you need to move faster than 'h' and 'l' allow you to but need more control than 'w', 'e', and 'b', especially when using different operators. 'f' moves to a specific character to the right of the cursor, 'F' moves to a specific character to the left, and ';' and ',' allow you to repeat these motions in different directions.", + level: 145, + } + } + + public get notes(): JSX.Element[] { + return [ + , + , + , + , + , + , + ] + } + + public get stages(): ITutorialStage[] { + return this._stages + } +} diff --git a/browser/src/Services/Learning/Tutorial/Tutorials/MoveAndInsertTutorial.tsx b/browser/src/Services/Learning/Tutorial/Tutorials/InsertAndUndoTutorial.tsx similarity index 61% rename from browser/src/Services/Learning/Tutorial/Tutorials/MoveAndInsertTutorial.tsx rename to browser/src/Services/Learning/Tutorial/Tutorials/InsertAndUndoTutorial.tsx index f334bd99ea..aade7e9cea 100644 --- a/browser/src/Services/Learning/Tutorial/Tutorials/MoveAndInsertTutorial.tsx +++ b/browser/src/Services/Learning/Tutorial/Tutorials/InsertAndUndoTutorial.tsx @@ -1,5 +1,5 @@ /** - * MoveAndInsertTutorial.tsx + * InsertAndUndoTutorial.tsx * * Tutorial that brings together moving and inserting */ @@ -10,10 +10,10 @@ import { ITutorial, ITutorialMetadata, ITutorialStage } from "./../ITutorial" import * as Notes from "./../Notes" import * as Stages from "./../Stages" -const TutorialLine1Original = "There is text msng this ." +const TutorialLine1Original = "There is text msing this ." const TutorialLine1Correct = "There is some text missing from this line." -export class MoveAndInsertTutorial implements ITutorial { +export class InsertAndUndoTutorial implements ITutorial { private _stages: ITutorialStage[] constructor() { @@ -22,16 +22,16 @@ export class MoveAndInsertTutorial implements ITutorial { new Stages.MoveToGoalStage("Move to the letter 't'", 0, 9), new Stages.WaitForModeStage("Press 'i' to enter insert mode", "insert"), new Stages.CorrectLineStage( - "Add the missing word 'some'", + "Add the missing word 'some '", 0, TutorialLine1Correct, "green", - "There is some", + "There is some ", ), new Stages.WaitForModeStage("Press '' to exit insert mode", "normal"), new Stages.MoveToGoalStage("Move to the letter 's'", 0, 20), new Stages.CorrectLineStage( - "Correct the word: `msng` should be `missing`", + "Correct the word: `msing` should be `missing`", 0, TutorialLine1Correct, "green", @@ -42,7 +42,7 @@ export class MoveAndInsertTutorial implements ITutorial { 0, TutorialLine1Correct, "green", - "There is some text missing from", + "There is some text missing from ", ), new Stages.CorrectLineStage( "Add the missing word 'line'", @@ -51,16 +51,29 @@ export class MoveAndInsertTutorial implements ITutorial { "green", "There is some text missing from this line.", ), + new Stages.WaitForModeStage("Press '' to exit insert mode", "normal"), + new Stages.WaitForStateStage("Press 'u' to undo the last change", [ + "There is some text missing from this .", + TutorialLine1Correct, + ]), + new Stages.WaitForStateStage("Press 'u' to undo another change", [ + "There is some text missing this .", + TutorialLine1Correct, + ]), + new Stages.WaitForStateStage("Press 'u' to undo yet another change", [ + "There is some text msing this .", + TutorialLine1Correct, + ]), ] } public get metadata(): ITutorialMetadata { return { - id: "oni.tutorial.move_and_insert", - name: "Moving and Inserting", + id: "oni.tutorial.insert_and_undo", + name: "Insert and Undo", description: - "It's important to be able to switch between normal and insert mode, in order to edit text! Let's put together the cursor motion and insert mode from the previous tutorials.", - level: 130, + "It's important to be able to switch between normal and insert mode, in order to edit text! Let's put together the cursor motion and insert mode from the previous tutorials. If you make any mistakes, you can undo inserted text with 'u'.", + level: 170, } } @@ -69,6 +82,6 @@ export class MoveAndInsertTutorial implements ITutorial { } public get notes(): JSX.Element[] { - return [, , ] + return [, , , ] } } diff --git a/browser/src/Services/Learning/Tutorial/Tutorials/SearchInBufferTutorial.tsx b/browser/src/Services/Learning/Tutorial/Tutorials/SearchInBufferTutorial.tsx index 0511ae155f..d62edfc656 100644 --- a/browser/src/Services/Learning/Tutorial/Tutorials/SearchInBufferTutorial.tsx +++ b/browser/src/Services/Learning/Tutorial/Tutorials/SearchInBufferTutorial.tsx @@ -33,7 +33,11 @@ export class SearchInBufferTutorial implements ITutorial { this._stages = [ // Forward search new Stages.SetBufferStage([Line1, Line2]), - new Stages.MoveToGoalStage("Use '/' to search for the word 'move'", 1, 28), + new Stages.MoveToGoalStage( + "Use '/' to enter search mode, type the word 'move', then hit ", + 1, + 28, + ), new Stages.SetBufferStage([Line1, Line2, Line3]), new Stages.MoveToGoalStage("Use 'n' to go to the next instance of 'move'", 2, 21), new Stages.SetBufferStage([Line1, Line2, Line3, EmptyLine, EmptyLine, Line4]), @@ -53,11 +57,19 @@ export class SearchInBufferTutorial implements ITutorial { // Backward search new Stages.SetBufferStage([Line7]), new Stages.SetCursorPositionStage(0, 48), - new Stages.MoveToGoalStage("Use '?' to search for the word 'you'", 0, 21), + new Stages.MoveToGoalStage("Use '?' to search backwards for the word 'you'", 0, 21), new Stages.SetBufferStage([Line7, Line8, Line9]), - new Stages.MoveToGoalStage("Use 'N' to go to the previous instance of 'you'", 2, 14), + new Stages.MoveToGoalStage( + "Use 'N' to go to the previous (backwards) instance of 'you'", + 2, + 14, + ), new Stages.SetBufferStage([Line7, Line8, Line9, Line10]), - new Stages.MoveToGoalStage("Use 'n' to go to the next instance of 'move'", 0, 21), + new Stages.MoveToGoalStage( + "Use 'n' to go to the next (backwards) instance of 'move'", + 0, + 21, + ), ] } @@ -67,7 +79,7 @@ export class SearchInBufferTutorial implements ITutorial { name: "Motion: /, ?, n, N", description: "To navigate a buffer efficiently, Oni lets you search for strings with `/` and `?`. `n` and `N` let you navigate quickly between the matches!", - level: 180, + level: 160, } } diff --git a/browser/src/Services/Learning/Tutorial/Tutorials/TargetsVimPluginTutorial.tsx b/browser/src/Services/Learning/Tutorial/Tutorials/TargetsVimPluginTutorial.tsx new file mode 100644 index 0000000000..7bfca9c0e8 --- /dev/null +++ b/browser/src/Services/Learning/Tutorial/Tutorials/TargetsVimPluginTutorial.tsx @@ -0,0 +1,153 @@ +/** + * + * Tutorial that exercises the targets.vim plugin + */ + +import * as React from "react" + +import { ITutorial, ITutorialMetadata, ITutorialStage } from "./../ITutorial" +import * as Notes from "./../Notes" +import * as Stages from "./../Stages" + +const Line1a = "The Targets.vim plugin is very useful!" +const Line1b = "It was created by Christian Wellenbrock." +const Line1c = "Targets.vim adds text objects for additional (operations)." + +const Line2a = "'cin(' changes inside the next pair of (parenthesis)." +const Line2b = "'can(' changes the next pair of (parenthesis)." +const Line2c = "Replacing 'n' with 'l' will change the previous pair." +const Line2d = "Omitting either will change the (current pair) or the (next)." + +const Line3a = "Quote objects can 'also' be used." +const Line3b = '\'cIn"\' changes the first characters inside of " quotes ".' +const Line3c = '\'cAn"\' changes around the "quotes" .' + +const Line4a = "'din,' will delete inside the next list, with, commas." +const Line4b = "Many different seperators are possible." +const Line4c = "Some applications you might consider not_a_list." + +const Line5a = "Replacing the text character with 'a' will find the next programming argument." +const Line5b = "'dina' will select inside the (next, argument)." +const Line5c = "'dana' deletes the (next, argument)" + +const Line6a = "These are only a brief overview of Targets.vim." +const Line6b = "More advanced features can be found on its github repository." +const Line6c = "https://github.com/wellle/targets.vim" + +export class TargetsVimPluginTutorial implements ITutorial { + private _stages: ITutorialStage[] + + constructor() { + this._stages = [ + new Stages.SetBufferStage([Line1a, Line1b, Line1c]), + new Stages.SetCursorPositionStage(0, 0), + new Stages.MoveToGoalStage("Use 'cin(' to change inside the next parenthesis", 2, 46), + new Stages.MoveToGoalStage("Type 'foo'", 2, 49), + new Stages.WaitForModeStage("Press ESC to go back to normal mode", "normal"), + Stages.combine( + null, + new Stages.FadeInLineStage(null, 1, Line2a), + new Stages.FadeInLineStage(null, 2, Line2b), + new Stages.FadeInLineStage(null, 3, Line2c), + new Stages.SetBufferStage([Line2a, Line2b, Line2c, Line2d]), + ), + new Stages.SetCursorPositionStage(0, 7), + new Stages.MoveToGoalStage("Use 'cin(' to change inside the next parenthesis", 0, 40), + new Stages.WaitForModeStage("Press ESC to go back to normal mode", "normal"), + new Stages.SetCursorPositionStage(1, 7), + new Stages.MoveToGoalStage("Use 'can(' to change the next parenthesis", 1, 32), + new Stages.MoveToGoalStage("Type 'bar'", 1, 35), + new Stages.WaitForModeStage("Press ESC to go back to normal mode", "normal"), + new Stages.MoveToGoalStage("Use 'cil(' to change inside the last parenthesis", 0, 40), + new Stages.WaitForModeStage("Press ESC to go back to normal mode", "normal"), + new Stages.SetCursorPositionStage(3, 34), + new Stages.MoveToGoalStage("Use 'ca(' to change the current parenthesis", 3, 32), + new Stages.WaitForModeStage("Press ESC to go back to normal mode", "normal"), + new Stages.MoveToGoalStage("Use 'ca(' to change the next parenthesis", 3, 40), + new Stages.WaitForModeStage("Press ESC to go back to normal mode", "normal"), + Stages.combine( + null, + new Stages.FadeInLineStage(null, 1, Line3a), + new Stages.FadeInLineStage(null, 2, Line3b), + new Stages.SetBufferStage([Line3a, Line3b, Line3c]), + ), + new Stages.SetCursorPositionStage(0, 0), + new Stages.MoveToGoalStage("Use 'cin'' to change inside the next single quotes", 0, 19), + new Stages.WaitForModeStage("Press ESC to go back to normal mode", "normal"), + new Stages.SetCursorPositionStage(1, 7), + new Stages.MoveToGoalStage( + "Use 'cIn\"' to change the first character inside the next double quotes", + 1, + 48, + ), + new Stages.WaitForModeStage("Press ESC to go back to normal mode", "normal"), + new Stages.SetCursorPositionStage(2, 7), + new Stages.MoveToGoalStage("Use 'cAn\"' to change the next double quotes", 2, 26), + new Stages.WaitForModeStage("Press ESC to go back to normal mode", "normal"), + Stages.combine( + null, + new Stages.FadeInLineStage(null, 1, Line4a), + new Stages.FadeInLineStage(null, 2, Line4b), + new Stages.SetBufferStage([Line4a, Line4b, Line4c]), + ), + new Stages.SetCursorPositionStage(0, 7), + new Stages.MoveToGoalStage("Use 'din,' to delete the next item in the list", 0, 40), + new Stages.MoveToGoalStage( + "Use 'din_' to delete inside the next underline in the variable", + 2, + 41, + ), + new Stages.WaitForModeStage("Press ESC to go back to normal mode", "normal"), + new Stages.MoveToGoalStage("Type gg to go to the beginning.", 0, 0), + Stages.combine( + null, + new Stages.FadeInLineStage(null, 1, Line5a), + new Stages.FadeInLineStage(null, 2, Line5b), + new Stages.SetBufferStage([Line5a, Line5b, Line5c]), + ), + new Stages.SetCursorPositionStage(0, 0), + new Stages.MoveToGoalStage( + "Use 'dina' to delete inside the next programming argument", + 1, + 31, + ), + new Stages.MoveToGoalStage("Use 'dana' to delete the next argument", 2, 20), + new Stages.MoveToGoalStage("Type gg to go to the beginning.", 0, 0), + Stages.combine( + null, + new Stages.FadeInLineStage(null, 1, Line6a), + new Stages.FadeInLineStage(null, 2, Line6b), + new Stages.SetBufferStage([Line6a, Line6b, Line6c]), + ), + new Stages.SetCursorPositionStage(2, 0), + new Stages.MoveToGoalStage("Type gg to go to the beginning.", 0, 0), + ] + } + + public get metadata(): ITutorialMetadata { + return { + id: "oni.tutorials.targets_plugin", + name: "Target.vim plugin", + description: + 'Target.vim is a plugin installed by default to help move between pairs of characters such as (), {}, or "". It does this by adding various text objects to operate on and expand simple commands like \'di"\'.', + level: 300, + } + } + + public get stages(): ITutorialStage[] { + return this._stages + } + + public get notes(): JSX.Element[] { + return [ + , + , + , + , + , + , + , + , + ] + } +} diff --git a/browser/src/Services/Learning/Tutorial/Tutorials/TextObjectsTutorial.tsx b/browser/src/Services/Learning/Tutorial/Tutorials/TextObjectsTutorial.tsx new file mode 100644 index 0000000000..8432bf390a --- /dev/null +++ b/browser/src/Services/Learning/Tutorial/Tutorials/TextObjectsTutorial.tsx @@ -0,0 +1,231 @@ +/** + * TextObjectsTutorial.tsx + * + * Tutorial to teach text objects + */ + +import * as React from "react" + +import { ITutorial, ITutorialMetadata, ITutorialStage } from "./../ITutorial" +import * as Notes from "./../Notes" +import * as Stages from "./../Stages" + +const stage1line1 = "Text objects typically have delimiters like ( ) and { }" +const stage1line2 = "This sentence has a phrase (within some parentheses) for testing" +const stage1line2a = "This sentence has a phrase () for testing" +const stage1line2b = "This sentence has a phrase for testing" +const stage1line2c = + "This sentence has a phrase (within some parentheses) for testingwithin some parentheses" +const stage1line2d = "This sentence has a phrase (this is a test) for testing" + +const stage2line1 = "Text objects can also span multiple lines" +const stage2line2 = "{" +const stage2line3 = " these are mostly useful" +const stage2line4 = " when editing code" +const stage2line5 = "}" + +const stage3line1 = + "Text Objects aren't limited to single character delimiters; they also work with HTML!" +const stage3line2 = "" +const stage3line3 = "

    " +const stage3line4 = " here is some text" +const stage3line4a = " " +const stage3line4b = " " +const stage3line5 = "

    " +const stage3line5a = " " +const stage3line6 = "" + +const stage4line1 = "There are many other Text Objects we can manipulate" +const stage4line2 = "[ there ] ( are ) { many } < text > objects ' to ' \" try \"" +const stage4line2a = "[] ( are ) { many } < text > objects ' to ' \" try \"" +const stage4line2b = "[] () { many } < text > objects ' to ' \" try \"" +const stage4line2c = "[] () {} < text > objects ' to ' \" try \"" +const stage4line2d = "[] () {} <> objects ' to ' \" try \"" +const stage4line2e = "[] () {} <> ' to ' \" try \"" +const stage4line2f = "[] () {} <> '' \" try \"" +const stage4line2g = "[] () {} <> '' \"\"" + +export class TextObjectsTutorial implements ITutorial { + private _stages: ITutorialStage[] + + constructor() { + this._stages = [ + new Stages.SetBufferStage([stage1line1, stage1line2]), + new Stages.MoveToGoalStage("Move inside the parentheses", 1, 41), + Stages.combine( + "Use 'di(' to delete everything within the parentheses", + new Stages.DeleteCharactersStage(null, 1, 28, "within some parentheses"), + new Stages.WaitForStateStage(null, [stage1line1, stage1line2a]), + ), + new Stages.WaitForStateStage("Notice it left the parentheses. Hit 'u' to undo", [ + stage1line1, + stage1line2, + ]), + new Stages.MoveToGoalStage("Move inside the parentheses", 1, 41), + Stages.combine( + "Use 'da(' to delete the parentheses and everything within", + new Stages.DeleteCharactersStage(null, 1, 27, "(within some parentheses)"), + new Stages.WaitForStateStage(null, [stage1line1, stage1line2b]), + ), + new Stages.WaitForStateStage("Hit 'u' to undo", [stage1line1, stage1line2]), + new Stages.MoveToGoalStage("Move inside the parentheses", 1, 41), + new Stages.WaitForRegisterStage( + "Use 'yi(' to yank everything with the parentheses", + "within some parentheses", + ), + new Stages.MoveToGoalStage("Move to the end of the line", 1, 63), + new Stages.WaitForStateStage("Paste from the clipboard with 'p'", [ + stage1line1, + stage1line2c, + ]), + new Stages.WaitForStateStage("Hit 'u' to undo", [stage1line1, stage1line2]), + new Stages.MoveToGoalStage("Move inside the parentheses", 1, 41), + Stages.combine( + "Use 'ci(' to change everything within the parentheses", + new Stages.WaitForModeStage(null, "insert"), + new Stages.WaitForStateStage(null, [stage1line1, stage1line2a]), + ), + new Stages.WaitForStateStage("Type 'this is a test'", [stage1line1, stage1line2d]), + new Stages.WaitForModeStage("Hit to exit insert mode", "normal"), + + new Stages.SetBufferStage([ + stage2line1, + stage2line2, + stage2line3, + stage2line4, + stage2line5, + ]), + new Stages.MoveToGoalStage("Move inside the curly brackets", 2, 14), + Stages.combine( + "Use 'vi{' to select everything within the curly brackets", + new Stages.MoveToGoalStage(null, 3, 19), + new Stages.WaitForModeStage(null, "visual"), + ), + new Stages.WaitForModeStage("Hit to exit visual mode", "normal"), + Stages.combine( + "Use 'va{' to select everything including the curly brackets", + new Stages.MoveToGoalStage(null, 4, 0), + new Stages.WaitForModeStage(null, "visual"), + ), + new Stages.WaitForModeStage("Hit to exit visual mode", "normal"), + + new Stages.SetBufferStage([ + stage3line1, + stage3line2, + stage3line3, + stage3line4, + stage3line5, + stage3line6, + ]), + new Stages.MoveToGoalStage("Move inside the HTML tag", 3, 20), + Stages.combine( + "Use 'dit' to delete everything within the tag", + new Stages.DeleteCharactersStage(null, 3, 12, "here is some text"), + new Stages.WaitForStateStage(null, [ + stage3line1, + stage3line2, + stage3line3, + stage3line4a, + stage3line5, + stage3line6, + ]), + ), + new Stages.WaitForStateStage("Hit 'u' to undo", [ + stage3line1, + stage3line2, + stage3line3, + stage3line4, + stage3line5, + stage3line6, + ]), + Stages.combine( + "Use 'dat' to delete the tag and its contents", + new Stages.DeleteCharactersStage(null, 3, 6, "here is some text"), + new Stages.WaitForStateStage(null, [ + stage3line1, + stage3line2, + stage3line3, + stage3line4b, + stage3line5, + stage3line6, + ]), + ), + Stages.combine( + "Use 'dat' to delete the

    tag and its contents", + new Stages.DeleteCharactersStage(null, 2, 3, "

    "), + new Stages.DeleteCharactersStage(null, 3, 0, " "), + new Stages.DeleteCharactersStage(null, 4, 0, "

    "), + new Stages.WaitForStateStage(null, [ + stage3line1, + stage3line2, + stage3line5a, + stage3line6, + ]), + ), + + new Stages.SetBufferStage([stage4line1, stage4line2]), + new Stages.MoveToGoalStage("Move to the next line", 1, 0), + Stages.combine( + "Try 'di['", + new Stages.DeleteCharactersStage(null, 1, 1, " there "), + new Stages.WaitForStateStage(null, [stage4line1, stage4line2a]), + ), + Stages.combine( + "Try 'di('", + new Stages.DeleteCharactersStage(null, 1, 4, " are "), + new Stages.WaitForStateStage(null, [stage4line1, stage4line2b]), + ), + Stages.combine( + "Try 'di{'", + new Stages.DeleteCharactersStage(null, 1, 7, " many "), + new Stages.WaitForStateStage(null, [stage4line1, stage4line2c]), + ), + Stages.combine( + "Try 'di<'", + new Stages.DeleteCharactersStage(null, 1, 10, " text "), + new Stages.WaitForStateStage(null, [stage4line1, stage4line2d]), + ), + new Stages.MoveToGoalStage("Move inside the word 'objects'", 1, 15), + Stages.combine( + "Try 'diw'", + new Stages.DeleteCharactersStage(null, 1, 12, "objects"), + new Stages.WaitForStateStage(null, [stage4line1, stage4line2e]), + ), + Stages.combine( + "Try di'", + new Stages.DeleteCharactersStage(null, 1, 14, " to "), + new Stages.WaitForStateStage(null, [stage4line1, stage4line2f]), + ), + Stages.combine( + 'Try di"', + new Stages.DeleteCharactersStage(null, 1, 17, " try "), + new Stages.WaitForStateStage(null, [stage4line1, stage4line2g]), + ), + ] + } + + public get metadata(): ITutorialMetadata { + return { + id: "oni.tutorial.text_objects", + name: "Text Objects: i, a", + description: + 'Everything you\'ve learned so far has involved motions from the current cursor position to a destination of some kind. Now it\'s time to learn about Text Objects, blocks of text with their own starting and ending characters. Text Objects can be manipulated with the operators you\'ve already learned such as `y`, `c`, `d`, and `v`. In general, defining a text obect with `i` will be the "inner" object, ignoring the text object boundary characters. Defining a text object with `a` will be "an" object, including the text object boundaries.', + level: 240, + } + } + + public get stages(): ITutorialStage[] { + return this._stages + } + + public get notes(): JSX.Element[] { + return [ + , + , + , + , + , + , + ] + } +} diff --git a/browser/src/Services/Learning/Tutorial/Tutorials/VisualModeTutorial.tsx b/browser/src/Services/Learning/Tutorial/Tutorials/VisualModeTutorial.tsx new file mode 100644 index 0000000000..f7111346f4 --- /dev/null +++ b/browser/src/Services/Learning/Tutorial/Tutorials/VisualModeTutorial.tsx @@ -0,0 +1,109 @@ +/** + * VisualModeTutorial.tsx + * + * Tutorial for learning how to select text in visual mode. + */ + +import * as React from "react" + +import { ITutorial, ITutorialMetadata, ITutorialStage } from "./../ITutorial" +import * as Notes from "./../Notes" +import * as Stages from "./../Stages" + +const Line1 = "Text can be selectedselected with 'v'." +const Line2 = "Selected text can then be (y)anked, (c)hanged, or (d)eleted with a single keypress" +const Line3 = "To select entire lines, use 'V' to include line-ending characters." +const Line4 = "Selected lines can also be with a single keypress" +const Line1Marker = "Text can be ".length +const Line1Marker2 = "Text can be selecte".length +const Line1Change = "Text can be selected with 'v'." +const Line2Marker = "Selected text can then be ".length +const Line2Marker2 = "Selected text can then be (y)anked, (c)hanged, or (d)elete".length +const Line4Marker = "Selected lines can also be".length +const Line4PostPaste = + "Selected lines can also be (y)anked, (c)hanged, or (d)eleted with a single keypress" +const Line3Marker = "To select entire lines, use 'V' to include ".length +const Line3Marker2 = "To select entire lines, use 'V' to include line-endin".length +const Line3Pending = "To select entire lines, use 'V' to include characters." +const Line3Change = "To select entire lines, use 'V' to include newline characters." +const Line3Marker3 = "To select entire lines, use 'V' to include newlin".length + +export class VisualModeTutorial implements ITutorial { + private _stages: ITutorialStage[] + + constructor() { + this._stages = [ + new Stages.SetBufferStage([Line1, Line2, Line3, Line4]), + new Stages.MoveToGoalStage("Move to the goal marker", 0, Line1Marker), + new Stages.WaitForModeStage("Change to Visual mode with 'v'", "visual"), + new Stages.MoveToGoalStage("Move to the goal marker", 0, Line1Marker2), + new Stages.WaitForStateStage("Hit 'd' to delete the selected text", [ + Line1Change, + Line2, + Line3, + Line4, + ]), + new Stages.MoveToGoalStage("Move to the goal marker", 1, Line2Marker), + new Stages.WaitForModeStage("Change to Visual mode with 'v'", "visual"), + new Stages.MoveToGoalStage("Move to the goal marker", 1, Line2Marker2), + new Stages.WaitForRegisterStage( + "Yank the selection with 'y'", + "(y)anked, (c)hanged, or (d)eleted", + ), + new Stages.MoveToGoalStage("Move to the goal marker", 3, Line4Marker), + new Stages.WaitForStateStage("Paste the yanked text with 'p'", [ + Line1Change, + Line2, + Line3, + Line4PostPaste, + ]), + new Stages.MoveToGoalStage("Move to the goal marker", 2, Line3Marker), + new Stages.WaitForModeStage("Change to Visual mode with 'v'", "visual"), + new Stages.MoveToGoalStage("Move to the goal marker", 2, Line3Marker2), + new Stages.WaitForStateStage("Change the selected text with 'c'", [ + Line1Change, + Line2, + Line3Pending, + Line4PostPaste, + ]), + new Stages.WaitForStateStage("Enter the word 'newline'", [ + Line1Change, + Line2, + Line3Change, + Line4PostPaste, + ]), + new Stages.WaitForModeStage("Exit Insert mode by hitting ", "normal"), + new Stages.WaitForModeStage("Move into Visual Line mode with 'V'", "visual"), + new Stages.MoveToGoalStage("Move to the next line", 3, Line3Marker3), + new Stages.WaitForStateStage("Delete the selected lines with 'd'", [ + Line1Change, + Line2, + ]), + ] + } + + public get metadata(): ITutorialMetadata { + return { + id: "oni.tutorials.visual_mode", + name: "Visual Select: v, V", + description: + "Sometimes the text you want to modify isn't on a simple word boundary. We often need to change, yank, or delete any arbitrary text. Rather than performing a cursor movement and hoping you affected the correct characters, you can visually select text. Using 'v' will select characters as you move, and 'V' will select lines", + level: 230, + } + } + + public get stages(): ITutorialStage[] { + return this._stages + } + + public get notes(): JSX.Element[] { + return [ + , + , + , + , + , + , + ] + } +} diff --git a/browser/src/Services/Learning/Tutorial/Tutorials/WordMotionTutorial.tsx b/browser/src/Services/Learning/Tutorial/Tutorials/WordMotionTutorial.tsx index b137cd77b4..b9dc0ec616 100644 --- a/browser/src/Services/Learning/Tutorial/Tutorials/WordMotionTutorial.tsx +++ b/browser/src/Services/Learning/Tutorial/Tutorials/WordMotionTutorial.tsx @@ -27,46 +27,36 @@ export class WordMotionTutorial implements ITutorial { new Stages.SetBufferStage([Line1, Line2]), new Stages.MoveToGoalStage("Use the 'j' key to move down a line", 1, 10 /* todo */), new Stages.MoveToGoalStage( - "Use the '0' key to move to the beginning of the line", + "Use the 'e' key to move to the end of the current word", 1, - 0, + 12, ), new Stages.MoveToGoalStage( - "Use the 'e' key to move to the end of the first word", + "Use the 'e' key to move to the end of the next word", 1, - 2, - ), - new Stages.MoveToGoalStage( - "Use the 'e' key to move to the end of the second word", - 1, - 6, + 15, ), new Stages.MoveToGoalStage( - "Use the 'e' key to move to the end of the third word", + "Use the 'e' key to move to the end of the next word", 1, - 8, + 20, ), new Stages.SetBufferStage([Line1, Line2, Line3]), - new Stages.MoveToGoalStage("Use the 'j' key to move down a line", 2, 8 /* todo */), - new Stages.MoveToGoalStage( - "Use the '$' key to move to the end of the line", - 2, - Line3.length - 1, - ), + new Stages.MoveToGoalStage("Use the 'j' key to move down a line", 2, 20 /* todo */), new Stages.MoveToGoalStage( - "Use the 'b' key to move to the beginning of the last word", + "Use the 'b' key to move to the beginning of the current word", 2, - Line3.length - "word.".length, + 17, ), new Stages.MoveToGoalStage( - "Use the 'b' key to move to the beginning of the second-to-last word", + "Use the 'b' key to move to the beginning of the previous word", 2, - Line3.length - "PREVIOUS word.".length, + 14, ), new Stages.MoveToGoalStage( - "Use the 'b' key to move to the beginning of the third-to-last word", + "Use the 'b' key to move to the beginning of the previous word", 2, - Line3.length - "the PREVIOUS word.".length, + 10, ), ] } @@ -76,8 +66,8 @@ export class WordMotionTutorial implements ITutorial { id: "oni.tutorials.word_motion", name: "Motion: w, e, b", description: - "Often, `h` and `l` aren't the fastest way to move in a line. Word motions can be useful here - and even more useful when coupled with operators (we'll explore those later)! The `w` key moves to the first letter of the next word, the `b` key moves to the beginning letter of the previous word, and the `e` key moves to the end of the next word.", - level: 170, + "Often, `h` and `l` aren't the fastest way to move in a line. Word motions can be useful here - and even more useful when coupled with operators (we'll explore those later)! The `w` key moves to the first letter of the next word, the `e` key moves to the end of the next word, and the `b` key moves to the beginning letter of the previous word.", + level: 130, } } @@ -86,6 +76,6 @@ export class WordMotionTutorial implements ITutorial { } public get notes(): JSX.Element[] { - return [, , , ] + return [, , , ] } } diff --git a/browser/src/Services/Learning/Tutorial/Tutorials/index.tsx b/browser/src/Services/Learning/Tutorial/Tutorials/index.tsx index 1f679b8785..66ad29f9aa 100644 --- a/browser/src/Services/Learning/Tutorial/Tutorials/index.tsx +++ b/browser/src/Services/Learning/Tutorial/Tutorials/index.tsx @@ -6,13 +6,18 @@ import { ITutorial } from "./../ITutorial" import { BasicMovementTutorial } from "./BasicMovementTutorial" import { BeginningsAndEndingsTutorial } from "./BeginningsAndEndingsTutorial" +import { ChangeOperatorTutorial } from "./ChangeOperatorTutorial" import { CopyPasteTutorial } from "./CopyPasteTutorial" import { DeleteCharacterTutorial } from "./DeleteCharacterTutorial" import { DeleteOperatorTutorial } from "./DeleteOperatorTutorial" -import { MoveAndInsertTutorial } from "./MoveAndInsertTutorial" +import { InlineFindingTutorial } from "./InlineFindingTutorial" +import { InsertAndUndoTutorial } from "./InsertAndUndoTutorial" import { SearchInBufferTutorial } from "./SearchInBufferTutorial" import { SwitchModeTutorial } from "./SwitchModeTutorial" +import { TargetsVimPluginTutorial } from "./TargetsVimPluginTutorial" +import { TextObjectsTutorial } from "./TextObjectsTutorial" import { VerticalMovementTutorial } from "./VerticalMovementTutorial" +import { VisualModeTutorial } from "./VisualModeTutorial" import { WordMotionTutorial } from "./WordMotionTutorial" export * from "./DeleteCharacterTutorial" @@ -24,9 +29,14 @@ export const AllTutorials: ITutorial[] = [ new BasicMovementTutorial(), new DeleteCharacterTutorial(), new DeleteOperatorTutorial(), - new MoveAndInsertTutorial(), + new InsertAndUndoTutorial(), new VerticalMovementTutorial(), new WordMotionTutorial(), new SearchInBufferTutorial(), new CopyPasteTutorial(), + new ChangeOperatorTutorial(), + new VisualModeTutorial(), + new TargetsVimPluginTutorial(), + new InlineFindingTutorial(), + new TextObjectsTutorial(), ] diff --git a/browser/src/Services/Notifications/NotificationsView.tsx b/browser/src/Services/Notifications/NotificationsView.tsx index f993eeaac7..5bf7cf6a2f 100644 --- a/browser/src/Services/Notifications/NotificationsView.tsx +++ b/browser/src/Services/Notifications/NotificationsView.tsx @@ -35,10 +35,10 @@ const Transition = (props: { children: React.ReactNode }) => { const NotificationsWrapper = styled.div` position: absolute; - top: 16px; - right: 16px; + top: 1em; + right: 1em; max-height: 90%; - max-width: 25rem; + max-width: 33vw; pointer-events: all; overflow: auto; diff --git a/browser/src/Services/Notifications/index.ts b/browser/src/Services/Notifications/index.ts index e6b769ebae..96bdf54b43 100644 --- a/browser/src/Services/Notifications/index.ts +++ b/browser/src/Services/Notifications/index.ts @@ -2,7 +2,7 @@ * index.ts */ -import * as Log from "./../../Log" +import * as Log from "oni-core-logging" import { OverlayManager } from "./../Overlay" diff --git a/browser/src/Services/QuickOpen/FinderProcess.ts b/browser/src/Services/QuickOpen/FinderProcess.ts index 4fd64b8f9b..104ff26546 100644 --- a/browser/src/Services/QuickOpen/FinderProcess.ts +++ b/browser/src/Services/QuickOpen/FinderProcess.ts @@ -8,7 +8,7 @@ import { ChildProcess, spawn } from "child_process" import { Event, IEvent } from "oni-types" -import * as Log from "./../../Log" +import * as Log from "oni-core-logging" import { getInstance } from "./../Workspace" export class FinderProcess { @@ -48,22 +48,17 @@ export class FinderProcess { cwd: this._workspace.activeWorkspace, }) this._process.stdout.on("data", data => { - if (!data) { - return - } - - const dataString = data.toString() - const isCleanEnd = dataString.endsWith(this._splitCharacter) - const splitData = dataString.split(this._splitCharacter) + const { didExtract, remnant, splitData } = extractSplitData( + data, + this._splitCharacter, + this._lastData, + ) - if (this._lastData && splitData.length > 0) { - splitData[0] = this._lastData + splitData[0] - this._lastData = "" + if (!didExtract) { + return } - if (!isCleanEnd) { - this._lastData = splitData.pop() - } + this._lastData = remnant this._onData.dispatch(splitData) }) @@ -81,3 +76,32 @@ export class FinderProcess { this._process.kill() } } + +export const extractSplitData = ( + data: string | Buffer, + splitCharacter: string, + lastRemnant: string, +) => { + if (!data) { + return { + didExtract: false, + remnant: "", + splitData: [], + } + } + + const dataString = lastRemnant + data.toString() + const isCleanEnd = dataString.endsWith(splitCharacter) + const splitData = dataString.split(splitCharacter) + + let remnant = "" + + if (!isCleanEnd) { + remnant = splitData.pop() + } else { + // split leaves behind an empty string in the array if the string to split ends with the delimiter + splitData.splice(-1, 1) + } + + return { didExtract: true, remnant, splitData } +} diff --git a/browser/src/Services/QuickOpen/QuickOpen.ts b/browser/src/Services/QuickOpen/QuickOpen.ts index fd5895295d..6b2d5e4fe5 100644 --- a/browser/src/Services/QuickOpen/QuickOpen.ts +++ b/browser/src/Services/QuickOpen/QuickOpen.ts @@ -41,7 +41,7 @@ export class QuickOpen { ) { this._menu = menuManager.create() this._menu.onItemSelected.subscribe((selectedItem: any) => { - this._onItemSelected(selectedItem) + this.openFileWithDefaultAction(selectedItem) }) this._menu.onHide.subscribe(() => { @@ -70,6 +70,26 @@ export class QuickOpen { } } + public openFileWithDefaultAction(selectedItem: Oni.Menu.MenuOption): void { + if (!selectedItem) { + return + } + + const defaultOpenMode: Oni.FileOpenMode = configuration.getValue( + "editor.quickOpen.defaultOpenMode", + ) + + this._onItemSelected(selectedItem, defaultOpenMode) + } + + public openFileWithAltAction(): void { + const alternativeOpenMode: Oni.FileOpenMode = configuration.getValue( + "editor.quickOpen.alternativeOpenMode", + ) + + this.openFile(alternativeOpenMode) + } + public async show() { // reset list and show loading indicator this._loadedItems = [] diff --git a/browser/src/Services/QuickOpen/index.ts b/browser/src/Services/QuickOpen/index.ts index b3b5b3796c..fc49985c2a 100644 --- a/browser/src/Services/QuickOpen/index.ts +++ b/browser/src/Services/QuickOpen/index.ts @@ -50,10 +50,10 @@ export const activate = ( }) commandManager.registerCommand({ - command: "quickOpen.openFileExistingTab", + command: "quickOpen.openFileAlternative", name: null, detail: null, - execute: () => _quickOpen.openFile(Oni.FileOpenMode.ExistingTab), + execute: () => _quickOpen.openFileWithAltAction(), enabled: isOpen, }) diff --git a/browser/src/Services/Recorder.ts b/browser/src/Services/Recorder.ts index 9632fb2078..390b912066 100644 --- a/browser/src/Services/Recorder.ts +++ b/browser/src/Services/Recorder.ts @@ -12,7 +12,7 @@ import * as path from "path" import * as Oni from "oni-api" -import * as Log from "./../Log" +import * as Log from "oni-core-logging" import { configuration } from "./Configuration" import { getInstance as getNotificationsInstance } from "./Notifications" diff --git a/browser/src/Services/Search/index.tsx b/browser/src/Services/Search/index.tsx index 465b56061a..f6227edb87 100644 --- a/browser/src/Services/Search/index.tsx +++ b/browser/src/Services/Search/index.tsx @@ -8,6 +8,7 @@ import * as React from "react" import { Subject } from "rxjs/Subject" +import * as Log from "oni-core-logging" import { Event, IEvent } from "oni-types" import { CommandManager } from "./../CommandManager" @@ -15,8 +16,6 @@ import { EditorManager } from "./../EditorManager" import { SidebarManager } from "./../Sidebar" import { Workspace } from "./../Workspace" -import * as Log from "./../../Log" - export * from "./SearchProvider" import { diff --git a/browser/src/Services/Sidebar/SidebarContentSplit.tsx b/browser/src/Services/Sidebar/SidebarContentSplit.tsx index 7720da8083..0171097e79 100644 --- a/browser/src/Services/Sidebar/SidebarContentSplit.tsx +++ b/browser/src/Services/Sidebar/SidebarContentSplit.tsx @@ -64,6 +64,7 @@ export class SidebarContentSplit { export interface ISidebarContentViewProps extends ISidebarContentContainerProps { activeEntry: ISidebarEntry + width: string } export interface ISidebarContentContainerProps { @@ -82,7 +83,7 @@ const EntranceKeyframes = keyframes` export const SidebarContentWrapper = withProps<{}>(styled.div)` ${enableMouse} - width: 200px; + width: ${props => props.width}; color: ${props => props.theme["editor.foreground"]}; background-color: ${props => props.theme["editor.background"]}; height: 100%; @@ -173,7 +174,7 @@ export class SidebarContentView extends React.PureComponent< const header = activeEntry && activeEntry.pane ? activeEntry.pane.title : null return ( - + {activeEntry.pane.render()} @@ -196,6 +197,7 @@ export const mapStateToProps = ( return { ...containerProps, activeEntry, + width: state.width, } } diff --git a/browser/src/Services/Sidebar/SidebarStore.ts b/browser/src/Services/Sidebar/SidebarStore.ts index 61029044e9..0b61719a9b 100644 --- a/browser/src/Services/Sidebar/SidebarStore.ts +++ b/browser/src/Services/Sidebar/SidebarStore.ts @@ -7,6 +7,8 @@ import { Reducer, Store } from "redux" import { createStore as createReduxStore } from "./../../Redux" +import { Configuration } from "../Configuration" +import { DefaultConfiguration } from "../Configuration/DefaultConfiguration" import { WindowManager, WindowSplitHandle } from "./../WindowManager" import { SidebarContentSplit } from "./SidebarContentSplit" import { SidebarSplit } from "./SidebarSplit" @@ -20,6 +22,8 @@ export interface ISidebarState { activeEntryId: string isActive: boolean + + width: string } export type SidebarIcon = string @@ -62,9 +66,20 @@ export class SidebarManager { return this._store } - constructor(private _windowManager: WindowManager = null) { + constructor( + private _windowManager: WindowManager = null, + private _configuration: Configuration, + ) { this._store = createStore() + this._configuration.onConfigurationChanged.subscribe(val => { + if (typeof val["sidebar.width"] === "string") { + this.setWidth(val["sidebar.width"]) + } + }) + + this.setWidth(this._configuration.getValue("sidebar.width")) + if (_windowManager) { this._iconSplit = this._windowManager.createSplit("left", new SidebarSplit(this)) this._contentSplit = this._windowManager.createSplit( @@ -74,6 +89,27 @@ export class SidebarManager { } } + public increaseWidth(): void { + if (this._contentSplit.isVisible) { + this.store.dispatch({ type: "INCREASE_WIDTH" }) + } + } + + public decreaseWidth(): void { + if (this._contentSplit.isVisible) { + this.store.dispatch({ type: "DECREASE_WIDTH" }) + } + } + + public setWidth(width: string): void { + if (width) { + this._store.dispatch({ + type: "SET_WIDTH", + width, + }) + } + } + public setNotification(id: string): void { if (id) { this._store.dispatch({ @@ -85,13 +121,19 @@ export class SidebarManager { public setActiveEntry(id: string): void { if (id) { + const oldId = this._store.getState().activeEntryId + this._store.dispatch({ type: "SET_ACTIVE_ID", activeEntryId: id, }) - if (!this._contentSplit.isVisible) { + if (oldId !== id) { this._contentSplit.show() + } else if (!this._contentSplit.isVisible) { + this._contentSplit.show() + } else if (this._contentSplit.isVisible) { + this._contentSplit.hide() } } } @@ -139,6 +181,7 @@ const DefaultSidebarState: ISidebarState = { entries: [], activeEntryId: null, isActive: false, + width: null, } export type SidebarActions = @@ -154,12 +197,51 @@ export type SidebarActions = type: "SET_NOTIFICATION" id: string } + | { + type: "SET_WIDTH" + width: string + } | { type: "ENTER" } | { type: "LEAVE" } + | { + type: "INCREASE_WIDTH" + } + | { + type: "DECREASE_WIDTH" + } + +export const changeSize = (change: "increase" | "decrease") => ( + size: string, + defaultValue = DefaultConfiguration["sidebar.width"], +): string => { + const [numberString, letters = "em"] = size.match(/[a-zA-Z]+|[0-9]+/g) + const isAllowedUnit = ["em", "px", "vw"].includes(letters) + const unitsToUse = isAllowedUnit ? letters : "em" + const convertedNumber = Number(numberString) + if (isNaN(convertedNumber)) { + return defaultValue + } + + // If too small don't allow a decrease and vice versa + const tooSmall = convertedNumber - 1 < 1 && change === "decrease" + const tooBig = convertedNumber + 1 > 50 && change === "increase" + + const changed = + tooBig || tooSmall + ? convertedNumber + : change === "increase" + ? convertedNumber + 1 + : convertedNumber - 1 + + return `${changed}${unitsToUse}` +} + +export const increaseWidth = changeSize("increase") +export const decreaseWidth = changeSize("decrease") export const sidebarReducer: Reducer = ( state: ISidebarState = DefaultSidebarState, @@ -181,6 +263,11 @@ export const sidebarReducer: Reducer = ( ...newState, isActive: false, } + case "SET_WIDTH": + return { + ...newState, + width: action.width, + } case "SET_ACTIVE_ID": return { ...newState, @@ -195,6 +282,16 @@ export const sidebarReducer: Reducer = ( } else { return newState } + case "DECREASE_WIDTH": + return { + ...newState, + width: decreaseWidth(newState.width), + } + case "INCREASE_WIDTH": + return { + ...newState, + width: increaseWidth(newState.width), + } default: return newState } diff --git a/browser/src/Services/Sidebar/SidebarView.tsx b/browser/src/Services/Sidebar/SidebarView.tsx index c43c92feed..d5fae42a90 100644 --- a/browser/src/Services/Sidebar/SidebarView.tsx +++ b/browser/src/Services/Sidebar/SidebarView.tsx @@ -17,6 +17,7 @@ import { withProps } from "./../../UI/components/common" import { Sneakable } from "./../../UI/components/Sneakable" export interface ISidebarIconProps { + id: string active: boolean focused: boolean iconName: string @@ -90,7 +91,7 @@ export class SidebarIcon extends React.PureComponent { public render(): JSX.Element { const notification = this.props.hasNotification ? : null return ( - + @@ -154,6 +155,7 @@ export class SidebarView extends React.PureComponent { const isFocused = e.id === selectedId && this.props.isActive return ( { if (configuration.getValue("sidebar.enabled")) { - _sidebarManager = new SidebarManager(windowManager) + _sidebarManager = new SidebarManager(windowManager, configuration) if (!configuration.getValue("sidebar.default.open")) { _sidebarManager.toggleSidebarVisibility() } + commandManager.registerCommand({ + command: "sidebar.increaseWidth", + name: "Sidebar: Increase Width", + detail: "Increase the width of the sidebar pane", + execute: () => _sidebarManager.increaseWidth(), + }) + + commandManager.registerCommand({ + command: "sidebar.decreaseWidth", + name: "Sidebar: Decrease Width", + detail: "Decrease the width of the sidebar pane", + execute: () => _sidebarManager.decreaseWidth(), + }) + commandManager.registerCommand({ command: "sidebar.toggle", name: "Sidebar: Toggle", @@ -23,7 +37,7 @@ export const activate = (configuration: Configuration, workspace: Workspace) => execute: () => _sidebarManager.toggleSidebarVisibility(), }) } else { - _sidebarManager = new SidebarManager() + _sidebarManager = new SidebarManager(null, configuration) } } diff --git a/browser/src/Services/Sneak/Sneak.tsx b/browser/src/Services/Sneak/Sneak.tsx index 0d1fa0c08a..df921e2952 100644 --- a/browser/src/Services/Sneak/Sneak.tsx +++ b/browser/src/Services/Sneak/Sneak.tsx @@ -12,7 +12,12 @@ import { Event, IDisposable, IEvent } from "oni-types" import { Overlay, OverlayManager } from "./../Overlay" -import { createStore as createSneakStore, ISneakInfo, ISneakState } from "./SneakStore" +import { + createStore as createSneakStore, + IAugmentedSneakInfo, + ISneakInfo, + ISneakState, +} from "./SneakStore" import { ConnectedSneakView } from "./SneakView" export type SneakProvider = () => Promise @@ -41,6 +46,21 @@ export class Sneak { return { dispose } } + // Get the first sneak with a 'tag' matching the passed in tag + public getSneakMatchingTag(tag: string): IAugmentedSneakInfo | null { + if (!this.isActive) { + return null + } + + const sneaks = this._store.getState().sneaks + + if (!sneaks || sneaks.length === 0) { + return null + } + + return sneaks.find(s => s.tag && s.tag === tag) + } + public show(): void { if (this._activeOverlay) { this._activeOverlay.hide() diff --git a/browser/src/Services/Sneak/SneakStore.ts b/browser/src/Services/Sneak/SneakStore.ts index 9d4370c561..beea70b835 100644 --- a/browser/src/Services/Sneak/SneakStore.ts +++ b/browser/src/Services/Sneak/SneakStore.ts @@ -13,6 +13,9 @@ import { createStore as createReduxStore } from "./../../Redux" export interface ISneakInfo { rectangle: Shapes.Rectangle callback: () => void + + // `tag` is an optional string used to identify the sneak + tag?: string } export interface IAugmentedSneakInfo extends ISneakInfo { diff --git a/browser/src/Services/Snippets/SnippetCompletionProvider.ts b/browser/src/Services/Snippets/SnippetCompletionProvider.ts index 08c6a0583d..b8762c70c0 100644 --- a/browser/src/Services/Snippets/SnippetCompletionProvider.ts +++ b/browser/src/Services/Snippets/SnippetCompletionProvider.ts @@ -5,11 +5,10 @@ */ import * as Oni from "oni-api" +import * as Log from "oni-core-logging" import * as types from "vscode-languageserver-types" -import * as Log from "./../../Log" - import { CompletionsRequestContext, ICompletionsRequestor } from "./../Completion" import { SnippetManager } from "./SnippetManager" diff --git a/browser/src/Services/Snippets/SnippetManager.ts b/browser/src/Services/Snippets/SnippetManager.ts index 73f46b3623..c33ab5786b 100644 --- a/browser/src/Services/Snippets/SnippetManager.ts +++ b/browser/src/Services/Snippets/SnippetManager.ts @@ -5,10 +5,9 @@ */ import * as Oni from "oni-api" +import * as Log from "oni-core-logging" import { IDisposable } from "oni-types" -import * as Log from "./../../Log" - import "rxjs/add/operator/auditTime" import { Subject } from "rxjs/Subject" diff --git a/browser/src/Services/Snippets/SnippetProvider.ts b/browser/src/Services/Snippets/SnippetProvider.ts index a42ea28e55..18ec7947b2 100644 --- a/browser/src/Services/Snippets/SnippetProvider.ts +++ b/browser/src/Services/Snippets/SnippetProvider.ts @@ -8,13 +8,13 @@ import * as fs from "fs" import * as os from "os" import * as Oni from "oni-api" +import * as Log from "oni-core-logging" import { PluginManager } from "./../../Plugins/PluginManager" import { Configuration } from "./../Configuration" -import * as Log from "./../../Log" -import { flatMap } from "./../../Utility" +import * as Utility from "./../../Utility" export class CompositeSnippetProvider implements Oni.Snippets.SnippetProvider { private _providers: Oni.Snippets.SnippetProvider[] = [] @@ -64,9 +64,10 @@ export class PluginSnippetProvider implements Oni.Snippets.SnippetProvider { p => p.metadata && p.metadata.contributes && p.metadata.contributes.snippets, ) - const snippets = flatMap(filteredPlugins, pc => pc.metadata.contributes.snippets).filter( - s => s.language === language, - ) + const snippets = Utility.flatMap( + filteredPlugins, + pc => pc.metadata.contributes.snippets, + ).filter(s => s.language === language) const snippetLoadPromises = snippets.map(s => this._loadSnippetsFromFile(s.path)) const loadedSnippets = await Promise.all(snippetLoadPromises) @@ -99,24 +100,35 @@ export const loadSnippetsFromFile = async ( }) }) + const snippets = loadSnippetsFromText(contents) + + Log.verbose( + `[loadSnippetsFromFile] - Loaded ${snippets.length} snippets from ${snippetFilePath}`, + ) + + return snippets +} + +interface KeyToSnippet { + [key: string]: ISnippetPluginContribution +} + +export const loadSnippetsFromText = (contents: string): Oni.Snippets.Snippet[] => { let snippets: ISnippetPluginContribution[] = [] try { - snippets = Object.values(JSON.parse(contents)) as ISnippetPluginContribution[] + const snippetObject = Utility.parseJson5(contents) + snippets = Object.values(snippetObject) } catch (ex) { Log.error(ex) snippets = [] } - Log.verbose( - `[loadSnippetsFromFile] - Loaded ${snippets.length} snippets from ${snippetFilePath}`, - ) - const normalizedSnippets = snippets.map( (snip: ISnippetPluginContribution): Oni.Snippets.Snippet => { return { prefix: snip.prefix, description: snip.description, - body: snip.body.join(os.EOL), + body: typeof snip.body === "string" ? snip.body : snip.body.join(os.EOL), } }, ) diff --git a/browser/src/Services/Snippets/SnippetSession.ts b/browser/src/Services/Snippets/SnippetSession.ts index 5118ad64cd..e1202a1bd5 100644 --- a/browser/src/Services/Snippets/SnippetSession.ts +++ b/browser/src/Services/Snippets/SnippetSession.ts @@ -7,10 +7,9 @@ import * as types from "vscode-languageserver-types" import * as Oni from "oni-api" +import * as Log from "oni-core-logging" import { Event, IEvent } from "oni-types" -import * as Log from "./../../Log" - import { OniSnippet, OniSnippetPlaceholder } from "./OniSnippet" import { BufferIndentationInfo, IBuffer } from "./../../Editor/BufferManager" diff --git a/browser/src/Services/Snippets/UserSnippetProvider.ts b/browser/src/Services/Snippets/UserSnippetProvider.ts index 46127227e3..06d7cc6df6 100644 --- a/browser/src/Services/Snippets/UserSnippetProvider.ts +++ b/browser/src/Services/Snippets/UserSnippetProvider.ts @@ -9,6 +9,7 @@ import * as path from "path" import * as mkdirp from "mkdirp" import * as Oni from "oni-api" +import * as Log from "oni-core-logging" import { loadSnippetsFromFile } from "./SnippetProvider" @@ -16,8 +17,6 @@ import { CommandManager } from "./../CommandManager" import { Configuration, getUserConfigFolderPath } from "./../Configuration" import { EditorManager } from "./../EditorManager" -import * as Log from "./../../Log" - const GLOBAL_SNIPPET_NAME = "global_snippets" const SnippetTemplate = [ diff --git a/browser/src/Services/SyntaxHighlighting/GrammarLoader.ts b/browser/src/Services/SyntaxHighlighting/GrammarLoader.ts index 612ed200fa..528917aaf7 100644 --- a/browser/src/Services/SyntaxHighlighting/GrammarLoader.ts +++ b/browser/src/Services/SyntaxHighlighting/GrammarLoader.ts @@ -1,8 +1,8 @@ import { IGrammar, Registry } from "vscode-textmate" -import { configuration } from "./../Configuration" +import * as Log from "oni-core-logging" -import * as Log from "./../../Log" +import { configuration } from "./../Configuration" export interface IGrammarLoader { getGrammarForLanguage(language: string, extension: string): Promise @@ -38,7 +38,7 @@ export class GrammarLoader implements IGrammarLoader { return null } - if (this._grammarCache[language]) { + if (language in this._grammarCache) { return this._grammarCache[language] } diff --git a/browser/src/Services/SyntaxHighlighting/SyntaxHighlightReconciler.ts b/browser/src/Services/SyntaxHighlighting/SyntaxHighlightReconciler.ts index 730afd010a..0c25b9cfba 100644 --- a/browser/src/Services/SyntaxHighlighting/SyntaxHighlightReconciler.ts +++ b/browser/src/Services/SyntaxHighlighting/SyntaxHighlightReconciler.ts @@ -4,6 +4,8 @@ * Handles enhanced syntax highlighting */ +import * as Log from "oni-core-logging" + import { TokenColor, TokenColors } from "./../TokenColors" import { NeovimEditor } from "./../../Editor/NeovimEditor" @@ -17,8 +19,6 @@ import { import * as Selectors from "./SyntaxHighlightSelectors" -import * as Log from "./../../Log" - // SyntaxHighlightReconciler // // Essentially a renderer / reconciler, that will push diff --git a/browser/src/Services/SyntaxHighlighting/SyntaxHighlighting.ts b/browser/src/Services/SyntaxHighlighting/SyntaxHighlighting.ts index dbac0edd83..ff3afec237 100644 --- a/browser/src/Services/SyntaxHighlighting/SyntaxHighlighting.ts +++ b/browser/src/Services/SyntaxHighlighting/SyntaxHighlighting.ts @@ -12,6 +12,7 @@ import { Subject } from "rxjs/Subject" import * as types from "vscode-languageserver-types" import * as Oni from "oni-api" +import * as Log from "oni-core-logging" import { Store, Unsubscribe } from "redux" @@ -30,7 +31,6 @@ import { ISyntaxHighlighter } from "./ISyntaxHighlighter" import { SyntaxHighlightReconciler } from "./SyntaxHighlightReconciler" import { getLineFromBuffer } from "./SyntaxHighlightSelectors" -import * as Log from "./../../Log" import * as Utility from "./../../Utility" export class SyntaxHighlighter implements ISyntaxHighlighter { diff --git a/browser/src/Services/SyntaxHighlighting/SyntaxHighlightingPeriodicJob.ts b/browser/src/Services/SyntaxHighlighting/SyntaxHighlightingPeriodicJob.ts index c509ba57b5..dbb1093bee 100644 --- a/browser/src/Services/SyntaxHighlighting/SyntaxHighlightingPeriodicJob.ts +++ b/browser/src/Services/SyntaxHighlighting/SyntaxHighlightingPeriodicJob.ts @@ -7,16 +7,15 @@ import { Store } from "redux" import * as types from "vscode-languageserver-types" - import { IGrammar } from "vscode-textmate" +import * as Log from "oni-core-logging" + import * as SyntaxHighlighting from "./SyntaxHighlightingStore" import * as Selectors from "./SyntaxHighlightSelectors" import { IPeriodicJob } from "./../../PeriodicJobs" -import * as Log from "./../../Log" - export const SYNTAX_JOB_BUDGET = 10 // Budget in milliseconds - time to allow the job to run for export class SyntaxHighlightingPeriodicJob implements IPeriodicJob { diff --git a/browser/src/Services/SyntaxHighlighting/SyntaxHighlightingStore.ts b/browser/src/Services/SyntaxHighlighting/SyntaxHighlightingStore.ts index 7eba48f44a..c22ea3a083 100644 --- a/browser/src/Services/SyntaxHighlighting/SyntaxHighlightingStore.ts +++ b/browser/src/Services/SyntaxHighlighting/SyntaxHighlightingStore.ts @@ -8,7 +8,8 @@ import { Store } from "redux" import * as types from "vscode-languageserver-types" import { StackElement } from "vscode-textmate" -import * as Log from "./../../Log" +import * as Log from "oni-core-logging" + import * as PeriodicJobs from "./../../PeriodicJobs" import { createStore } from "./../../Redux" import { configuration } from "./../Configuration" diff --git a/browser/src/Services/SyntaxHighlighting/TokenThemeProvider.tsx b/browser/src/Services/SyntaxHighlighting/TokenThemeProvider.tsx index 3700f0e513..1cee7bcb97 100644 --- a/browser/src/Services/SyntaxHighlighting/TokenThemeProvider.tsx +++ b/browser/src/Services/SyntaxHighlighting/TokenThemeProvider.tsx @@ -32,9 +32,9 @@ const constructClassName = (token: string) => (theme: INewTheme) => { const tokenStyle = cssToken(theme, token) const cssClass = ` .${tokenAsClass} { - color: ${tokenStyle("foregroundColor")}; - ${tokenStyle("bold") && notPunctuation && "font-weight: bold"}; - ${tokenStyle("italic") && "font-style: italic"}; + color: ${tokenStyle("foregroundColor") || ""}; + ${tokenStyle("bold") && notPunctuation ? "font-weight: bold" : ""}; + ${tokenStyle("italic") ? "font-style: italic" : ""}; } ` return cssClass diff --git a/browser/src/Services/UnhandledErrorMonitor.ts b/browser/src/Services/UnhandledErrorMonitor.ts index 6b00c530a7..c4824ff2fa 100644 --- a/browser/src/Services/UnhandledErrorMonitor.ts +++ b/browser/src/Services/UnhandledErrorMonitor.ts @@ -9,7 +9,7 @@ import { Event, IEvent } from "oni-types" import { Configuration } from "./Configuration" import { Notifications } from "./Notifications" -import * as Log from "./../Log" +import * as Log from "oni-core-logging" export class UnhandledErrorMonitor { private _onUnhandledErrorEvent = new Event() diff --git a/browser/src/Services/WindowManager/LinearSplitProvider.ts b/browser/src/Services/WindowManager/LinearSplitProvider.ts index ff976dd738..012b76d2b0 100644 --- a/browser/src/Services/WindowManager/LinearSplitProvider.ts +++ b/browser/src/Services/WindowManager/LinearSplitProvider.ts @@ -160,10 +160,14 @@ export class LinearSplitProvider implements IWindowSplitProvider { } public getState(): ISplitInfo { + if (this._splitProviders.length === 0) { + return null + } + return { type: "Split", direction: this._direction, - splits: this._splitProviders.map(sp => sp.getState()), + splits: this._splitProviders.map(sp => sp.getState()).filter(s => s !== null), } } diff --git a/browser/src/Services/WindowManager/WindowManager.ts b/browser/src/Services/WindowManager/WindowManager.ts index 5793f30c11..886135c3da 100644 --- a/browser/src/Services/WindowManager/WindowManager.ts +++ b/browser/src/Services/WindowManager/WindowManager.ts @@ -137,7 +137,11 @@ export class WindowManager { return this._store } - public get activeSplit(): IAugmentedSplitInfo { + get activeSplitHandle(): WindowSplitHandle { + return new WindowSplitHandle(this._store, this, this.activeSplit.id) + } + + private get activeSplit(): IAugmentedSplitInfo { const focusedSplit = this._store.getState().focusedSplitId if (!focusedSplit) { diff --git a/browser/src/Services/Workspace/Workspace.ts b/browser/src/Services/Workspace/Workspace.ts index 2d42148a9a..c87764520f 100644 --- a/browser/src/Services/Workspace/Workspace.ts +++ b/browser/src/Services/Workspace/Workspace.ts @@ -19,9 +19,9 @@ import { Observable } from "rxjs/Observable" import * as types from "vscode-languageserver-types" import * as Oni from "oni-api" +import * as Log from "oni-core-logging" import { Event, IEvent } from "oni-types" -import * as Log from "./../../Log" import * as Helpers from "./../../Plugins/Api/LanguageClient/LanguageClientHelpers" import { Configuration } from "./../Configuration" @@ -183,12 +183,16 @@ export class Workspace implements IWorkspace { let _workspace: Workspace = null let _workspaceConfiguration: WorkspaceConfiguration = null -export const activate = (configuration: Configuration, editorManager: EditorManager): void => { +export const activate = ( + configuration: Configuration, + editorManager: EditorManager, + workspaceToLoad?: string, +): void => { _workspace = new Workspace(editorManager, configuration) _workspaceConfiguration = new WorkspaceConfiguration(configuration, _workspace) - const defaultWorkspace = configuration.getValue("workspace.defaultWorkspace") + const defaultWorkspace = workspaceToLoad || configuration.getValue("workspace.defaultWorkspace") if (defaultWorkspace) { _workspace.changeDirectory(defaultWorkspace) diff --git a/browser/src/Services/Workspace/WorkspaceConfiguration.ts b/browser/src/Services/Workspace/WorkspaceConfiguration.ts index 0e09ad6dbf..1a834dc63a 100644 --- a/browser/src/Services/Workspace/WorkspaceConfiguration.ts +++ b/browser/src/Services/Workspace/WorkspaceConfiguration.ts @@ -7,7 +7,7 @@ import * as fs from "fs" import * as path from "path" -import * as Log from "./../../Log" +import * as Log from "oni-core-logging" import { Configuration } from "./../Configuration" import { IWorkspace } from "./Workspace" diff --git a/browser/src/UI/Shell/Shell.tsx b/browser/src/UI/Shell/Shell.tsx index 4e92b0ce2f..3b02bdac95 100644 --- a/browser/src/UI/Shell/Shell.tsx +++ b/browser/src/UI/Shell/Shell.tsx @@ -77,4 +77,19 @@ export const render = (state: State.IState): void => { // tslint:disable-next-line if (global["window"]) { document.body.addEventListener("click", () => focusManager.enforceFocus()) + + // This is necessary to prevent electron's default behaviour on drag and dropping + // which replaces the webContent aka the entire editor with the text, NOT Good + // also DO Not Stop Propagation as this breaks other drag drop functionality + document.addEventListener("dragover", ev => { + ev.preventDefault() + }) + + document.addEventListener("dragenter", ev => { + ev.preventDefault() + }) + + document.addEventListener("drop", ev => { + ev.preventDefault() + }) } diff --git a/browser/src/UI/Shell/ShellView.tsx b/browser/src/UI/Shell/ShellView.tsx index f698e4229a..0c7fda57fb 100644 --- a/browser/src/UI/Shell/ShellView.tsx +++ b/browser/src/UI/Shell/ShellView.tsx @@ -30,13 +30,30 @@ interface IShellViewComponentProps { const titleBarVisible = Platform.isMac() -export class ShellView extends React.PureComponent { +interface IShellViewState { + /** + * Tracks if composition is occurring (ie, an IME is active) + */ + isComposing: boolean +} + +export class ShellView extends React.PureComponent { + constructor(props: IShellViewComponentProps) { + super(props) + + this.state = { + isComposing: false, + } + } + public render() { return (
    this._onRootKeyDown(evt)} + onCompositionEndCapture={evt => this._onCompositionEnd(evt)} + onCompositionStartCapture={evt => this._onCompositionStart(evt)} >
    @@ -66,12 +83,32 @@ export class ShellView extends React.PureComponent } private _onRootKeyDown(evt: React.KeyboardEvent): void { + // onCompositionStart can't detect composing mode for the first character + // because it is fired after onKeyDown. + // keyCode is deprecated but it seems this is the only method to detect + // composing mode for the first character for now. + let isComposing = false + if (evt.keyCode === 229) { + isComposing = true + } const vimKey = inputManager.resolvers.resolveKeyEvent(evt.nativeEvent) - if (inputManager.handleKey(vimKey)) { + if (!this.state.isComposing && !isComposing && inputManager.handleKey(vimKey)) { evt.stopPropagation() evt.preventDefault() } else { focusManager.enforceFocus() } } + + private _onCompositionStart(evt: React.CompositionEvent) { + this.setState({ + isComposing: true, + }) + } + + private _onCompositionEnd(evt: React.CompositionEvent) { + this.setState({ + isComposing: false, + }) + } } diff --git a/browser/src/UI/components/CursorPositioner.tsx b/browser/src/UI/components/CursorPositioner.tsx index 3adb1128c5..4d35f0b9c4 100644 --- a/browser/src/UI/components/CursorPositioner.tsx +++ b/browser/src/UI/components/CursorPositioner.tsx @@ -13,6 +13,7 @@ import * as Oni from "oni-api" import { IState } from "./../../Editor/NeovimEditor/NeovimEditorStore" import { Arrow, ArrowDirection } from "./Arrow" +import styled, { pixel, withProps } from "./common" export enum OpenDirection { Up = 1, @@ -40,31 +41,77 @@ export interface ICursorPositionerViewProps extends ICursorPositionerProps { backgroundColor: string } -export interface ICursorPositionerViewState { +interface ContainerProps { + adjustedY: number + containerWidth: number isMeasured: boolean +} +interface ArrowContainerProps { + x: number + fontPixelWidth: number + hideArrow: boolean + shouldOpenDownwards: boolean +} + +interface ChildProps { + adjustedX: number + isFullWidth: boolean + shouldOpenDownwards: boolean +} + +type VisibilityProperty = "hidden" | "visible" + +const PositionerContainer = withProps(styled.div).attrs({ + style: ({ adjustedY, containerWidth, isMeasured }: ContainerProps) => ({ + top: pixel(adjustedY), + width: pixel(containerWidth), + // Wait until we've measured the bounds to show.. + visibility: (isMeasured ? "visible" : "hidden") as VisibilityProperty, + }), +})` + position: absolute; + left: 0px; + max-width: 45vw; +` + +const openFromBottomStyle = { bottom: "0px" } +const openFromTopStyle = { top: "0px" } + +const PositionerChild = withProps(styled.div).attrs({ + style: (props: ChildProps) => ({ + ...(props.shouldOpenDownwards ? openFromTopStyle : openFromBottomStyle), + left: props.isFullWidth ? "8px" : pixel(Math.abs(props.adjustedX)), + right: props.isFullWidth ? "8px" : null, + }), +})` + position: absolute; + width: fit-content; +` + +const ArrowContainer = withProps(styled.div).attrs({ + style: (props: ArrowContainerProps) => ({ + ...(props.shouldOpenDownwards ? openFromBottomStyle : openFromTopStyle), + left: pixel(props.x + props.fontPixelWidth / 2), + visibility: (props.hideArrow ? "hidden" : "visible") as VisibilityProperty, + }), +})` + position: absolute; + width: fit-content; +` + +export interface ICursorPositionerViewState { + isMeasured: boolean isFullWidth: boolean shouldOpenDownward: boolean adjustedX: number + adjustedY: number lastMeasuredX: number lastMeasuredY: number lastMeasuredHeight: number lastMeasuredWidth: number } -const InitialState = { - isMeasured: false, - - isFullWidth: false, - shouldOpenDownward: false, - adjustedX: 0, - - lastMeasuredX: -1, - lastMeasuredY: -1, - lastMeasuredHeight: 0, - lastMeasuredWidth: 0, -} - /** * Helper component to position an element relative to the current cursor position */ @@ -72,15 +119,20 @@ export class CursorPositionerView extends React.PureComponent< ICursorPositionerViewProps, ICursorPositionerViewState > { + public state = { + isMeasured: false, + isFullWidth: false, + shouldOpenDownward: false, + adjustedX: 0, + adjustedY: 0, + lastMeasuredX: -1, + lastMeasuredY: -1, + lastMeasuredHeight: 0, + lastMeasuredWidth: 0, + } + private _element: HTMLElement private _resizeObserver: any - private _timeout: any - - constructor(props: ICursorPositionerViewProps) { - super(props) - - this.state = InitialState - } public componentDidMount(): void { if (this._element) { @@ -101,14 +153,7 @@ export class CursorPositionerView extends React.PureComponent< return } - if (this._timeout) { - window.clearTimeout(this._timeout) - } - - this._timeout = window.setTimeout(() => { - this._measureElement(this._element) - this._timeout = null - }, 80) + this._measureElement(this._element) }) this._resizeObserver.observe(this._element) @@ -123,64 +168,37 @@ export class CursorPositionerView extends React.PureComponent< } public render(): JSX.Element { - const adjustedX = this.state.adjustedX const adjustedY = this.state.shouldOpenDownward ? this.props.y + this.props.lineHeight * 2.5 : this.props.y - const containerStyle: React.CSSProperties = { - position: "absolute", - top: adjustedY.toString() + "px", - left: "0px", - width: this.props.containerWidth.toString() + "px", - maxWidth: "45vw", - visibility: this.state.isMeasured ? "visible" : "hidden", // Wait until we've measured the bounds to show.. - } - - const openFromBottomStyle: React.CSSProperties = { - position: "absolute", - bottom: "0px", - width: "fit-content", - } - - const openFromTopStyle: React.CSSProperties = { - position: "absolute", - top: "0px", - width: "fit-content", - } - - const childStyle = this.state.shouldOpenDownward ? openFromTopStyle : openFromBottomStyle - const arrowStyle = this.state.shouldOpenDownward ? openFromBottomStyle : openFromTopStyle - - const arrowStyleWithAdjustments: React.CSSProperties = { - ...arrowStyle, - left: (this.props.x + this.props.fontPixelWidth / 2).toString() + "px", - visibility: this.props.hideArrow ? "hidden" : "visible", - } - - const childStyleWithAdjustments: React.CSSProperties = this.state.isMeasured - ? { - ...childStyle, - left: this.state.isFullWidth ? "8px" : Math.abs(adjustedX).toString() + "px", - right: this.state.isFullWidth ? "8px" : null, - } - : childStyle + const arrowDirection = this.state.shouldOpenDownward + ? ArrowDirection.Up + : ArrowDirection.Down return ( -
    -
    + +
    (this._element = elem)}>{this.props.children}
    -
    -
    - -
    -
    + + + + + ) } @@ -200,13 +218,13 @@ export class CursorPositionerView extends React.PureComponent< const margin = this.props.lineHeight * 2 const canOpenUpward = this.props.y - rect.height > margin const bottomScreenPadding = 50 - const canOpenDownard = + const canOpenDownwards = this.props.y + rect.height + this.props.lineHeight * 3 < this.props.containerHeight - margin - bottomScreenPadding const shouldOpenDownward = (this.props.openDirection !== OpenDirection.Down && !canOpenUpward) || - (this.props.openDirection === OpenDirection.Down && canOpenDownard) + (this.props.openDirection === OpenDirection.Down && canOpenDownwards) const rightBounds = this.props.x + rect.width diff --git a/browser/src/UI/components/Error.tsx b/browser/src/UI/components/Error.tsx index 464c362409..1ca2e19ee3 100644 --- a/browser/src/UI/components/Error.tsx +++ b/browser/src/UI/components/Error.tsx @@ -12,7 +12,7 @@ import * as Oni from "oni-api" import { getColorFromSeverity } from "./../../Services/Diagnostics" import { Icon } from "./../Icon" -import { bufferScrollBarSize, styled, withProps } from "./common" +import { bufferScrollBarSize, pixel, styled, withProps } from "./common" export interface IErrorsProps { errors: types.Diagnostic[] @@ -102,9 +102,16 @@ const ErrorMarker = (props: IErrorMarkerProps) => ( ) -const ErrorMarkerWrapper = withProps<{ topOffset: number }>(styled.div)` +interface ErrorMarkerProps { + topOffset: number +} + +const ErrorMarkerWrapper = withProps(styled.div).attrs({ + style: (props: ErrorMarkerProps) => ({ + top: pixel(props.topOffset), + }), +})` position: absolute; - top: ${props => props.topOffset}px; right: ${bufferScrollBarSize}; opacity: 0.5; background-color: rgb(80, 80, 80); @@ -132,13 +139,15 @@ interface IErrorSquiggleProps { width: number color: string } -const ErrorSquiggle = withProps(styled.div)` + +const ErrorSquiggle = withProps(styled.div).attrs({ + style: (props: IErrorSquiggleProps) => ({ + top: pixel(props.y), + left: pixel(props.x), + height: pixel(props.height), + width: pixel(props.width), + borderBottom: `1px dashed ${props.color}`, + }), +})` position: absolute; - ${props => ` - top: ${props.y}px; - left: ${props.x}px; - height: ${props.height}px; - width: ${props.width}px; - border-bottom: 1px dashed ${props.color}; - `} ` diff --git a/browser/src/UI/components/ErrorInfo.tsx b/browser/src/UI/components/ErrorInfo.tsx index b197a0185c..cfa84e27a9 100644 --- a/browser/src/UI/components/ErrorInfo.tsx +++ b/browser/src/UI/components/ErrorInfo.tsx @@ -1,37 +1,50 @@ import * as React from "react" import * as types from "vscode-languageserver-types" +import styled from "./common" import { ErrorIcon } from "./Error" import { getColorFromSeverity } from "./../../Services/Diagnostics" export interface IErrorInfoProps { - style: React.CSSProperties + hasQuickInfo: boolean errors: types.Diagnostic[] } +export const DiagnosticMessage = styled.span` + margin-left: 1em; +` + +type StyleProps = Pick + +const DiagnosticContainer = styled("div")` + user-select: none; + cursor: default; + border-bottom: ${p => (p.hasQuickInfo ? `1px solid ${p.theme["toolTip.border"]}` : "")}; +` + +export const Diagnostic = styled.div` + margin: 8px; + display: flex; + flex-direction: row; +` + /** * Helper component to render errors in the QuickInfo bubble */ -export class ErrorInfo extends React.PureComponent { - public render(): null | JSX.Element { - if (!this.props.errors) { - return null - } - - const errs = this.props.errors.map(e => ( -
    - - {e.message} -
    - )) - - const style = this.props.style || {} - - return ( -
    - {errs} -
    +export const ErrorInfo = (props: IErrorInfoProps) => { + return ( + props.errors && ( + + {props.errors.map((e, idx) => ( + + + + {e.message} + + + ))} + ) - } + ) } diff --git a/browser/src/UI/components/KeyBindingInfo.tsx b/browser/src/UI/components/KeyBindingInfo.tsx index 7552f87b0f..ad47b6c5bf 100644 --- a/browser/src/UI/components/KeyBindingInfo.tsx +++ b/browser/src/UI/components/KeyBindingInfo.tsx @@ -8,6 +8,7 @@ import styled from "styled-components" import * as React from "react" +import { parseChordParts } from "./../../Input/KeyParser" import { inputManager } from "./../../Services/InputManager" export interface IKeyBindingInfoProps { @@ -31,39 +32,17 @@ export class KeyBindingInfo extends React.PureComponent{"meta"}) - elems.push({"+"}) - } - - if (firstChord.control) { - elems.push({"control"}) - elems.push({"+"}) - } - - if (firstChord.alt) { - elems.push({"alt"}) - elems.push({"+"}) - } - - if (firstChord.shift) { - elems.push({"shift"}) - elems.push({"+"}) - } - - elems.push({firstChord.character}) - - return {elems} + // 1. Get the key(s) in the chord binding + // 2. Intersperse with "+" + // 3. Create KeyWrappers for each segment + return ( + + {parseChordParts(boundKeys[0]) + .reduce((acc, chordKey) => acc.concat(chordKey, "+"), []) + .slice(0, -1) + .map((chordPart, index) => {chordPart})} + + ) } } diff --git a/browser/src/UI/components/QuickInfo.less b/browser/src/UI/components/QuickInfo.less deleted file mode 100644 index 57fb8482ce..0000000000 --- a/browser/src/UI/components/QuickInfo.less +++ /dev/null @@ -1,27 +0,0 @@ -@import (reference) "./common.less"; - -.quickinfo-container { - -webkit-user-select: none; - cursor: default; - - .quickinfo { - text-overflow: ellipsis; - overflow: hidden; - - .diagnostic { - margin: 8px; - - display: flex; - flex-direction: row; - - .icon-container { - margin-right: 8px; - } - } - - .selected { - font-style: italic; - text-decoration: underline; - } - } -} diff --git a/browser/src/UI/components/QuickInfo.tsx b/browser/src/UI/components/QuickInfo.tsx index 2267540852..15a94331c8 100644 --- a/browser/src/UI/components/QuickInfo.tsx +++ b/browser/src/UI/components/QuickInfo.tsx @@ -3,6 +3,16 @@ import * as os from "os" import * as React from "react" import styled, { boxShadowInset, css, fontSizeSmall, withProps } from "./common" +export const QuickInfoWrapper = styled.div` + user-select: none; + cursor: default; +` + +export const QuickInfoElement = styled.div` + text-overflow: ellipsis; + overflow: hidden; +` + const codeBlockStyle = css` color: ${p => p.theme.foreground}; padding: 0.4em 0.4em 0.4em 0.4em; diff --git a/browser/src/UI/components/QuickInfoContainer.tsx b/browser/src/UI/components/QuickInfoContainer.tsx index 08679cb681..936c28a211 100644 --- a/browser/src/UI/components/QuickInfoContainer.tsx +++ b/browser/src/UI/components/QuickInfoContainer.tsx @@ -6,6 +6,7 @@ import TokenThemeProvider from "./../../Services/SyntaxHighlighting/TokenThemePr interface IQuickInfoProps { titleAndContents: ITitleAndContents + isVisible: boolean } interface ITitleAndContents { @@ -19,7 +20,7 @@ interface ITitleAndContents { class QuickInfoHoverContainer extends React.Component { public render() { - const { titleAndContents } = this.props + const { titleAndContents, isVisible } = this.props const hasTitle = !!(titleAndContents && titleAndContents.title.__html) const hasDocs = hasTitle && @@ -30,23 +31,25 @@ class QuickInfoHoverContainer extends React.Component { ) return ( - ( - - - {titleAndContents.description && ( - ( + + - )} - - )} - /> + {titleAndContents.description && ( + + )} + + )} + /> + ) ) } } diff --git a/browser/src/UI/components/Sneakable.tsx b/browser/src/UI/components/Sneakable.tsx index c342cce1e3..21d7711c8f 100644 --- a/browser/src/UI/components/Sneakable.tsx +++ b/browser/src/UI/components/Sneakable.tsx @@ -14,6 +14,7 @@ import { /* SneakProvider, Sneak,*/ getInstance as getSneak } from "./../../Serv import { EmptyArray } from "./../../Utility" export interface ISneakableProps { + tag?: string callback?: (evt?: any) => void } @@ -39,6 +40,7 @@ export class Sneakable extends React.PureComponent { rect.width, rect.height, ), + tag: this.props.tag || null, }, ] } else { diff --git a/browser/src/UI/components/Tabs.tsx b/browser/src/UI/components/Tabs.tsx index d804ee78d2..2d99bda599 100644 --- a/browser/src/UI/components/Tabs.tsx +++ b/browser/src/UI/components/Tabs.tsx @@ -7,7 +7,7 @@ import * as path from "path" import * as React from "react" import { connect } from "react-redux" -import * as classNames from "classnames" +import classNames from "classnames" import { keyframes } from "styled-components" import * as BufferSelectors from "./../../Editor/NeovimEditor/NeovimEditorSelectors" @@ -40,8 +40,8 @@ export interface ITabContainerProps { } export interface ITabsProps { - onSelect?: (id: number) => void - onClose?: (id: number) => void + onTabSelect?: (id: number) => void + onTabClose?: (id: number) => void visible: boolean tabs: ITabProps[] @@ -88,8 +88,8 @@ export class Tabs extends React.PureComponent { this._onSelect(t.id)} - onClickClose={() => this._onClickClose(t.id)} + onSelect={this.props.onTabSelect} + onClose={this.props.onTabClose} backgroundColor={this.props.backgroundColor} foregroundColor={this.props.foregroundColor} height={this.props.height} @@ -104,19 +104,11 @@ export class Tabs extends React.PureComponent {
    ) } - - private _onSelect(id: number): void { - this.props.onSelect(id) - } - - private _onClickClose(id: number): void { - this.props.onClose(id) - } } export interface ITabPropsWithClick extends ITabProps { - onClickName: () => void - onClickClose: () => void + onSelect: (id: number) => void + onClose: (id: number) => void backgroundColor: string foregroundColor: string @@ -138,19 +130,17 @@ interface IChromeDivElement extends HTMLDivElement { scrollIntoViewIfNeeded: (args: { behavior: string; block: string; inline: string }) => void } -export class Tab extends React.Component { +export class Tab extends React.PureComponent { private _tab: IChromeDivElement - public componentWillReceiveProps(next: ITabPropsWithClick) { - if (next.isSelected && this._tab) { - if (this._tab.scrollIntoViewIfNeeded) { - this._tab.scrollIntoViewIfNeeded({ - behavior: "smooth", - block: "center", - inline: "center", - }) - } - } + + public componentDidUpdate() { + this._checkIfShouldScroll() } + + public componentDidMount(): void { + this._checkIfShouldScroll() + } + public render() { const cssClasses = classNames("tab", { selected: this.props.isSelected, @@ -167,25 +157,28 @@ export class Tab extends React.Component { borderTop: "2px solid " + this.props.highlightColor, } + const handleTitleClick = this._handleTitleClick.bind(this) + const handleCloseButtonClick = this._handleCloseButtonClick.bind(this) + return ( - this.props.onClickName()}> + this.props.onSelect(this.props.id)} tag={this.props.name}> (this._tab = e)} className={cssClasses} title={this.props.description} style={style} > -
    +
    -
    +
    {this.props.name}
    -
    +
    @@ -197,6 +190,38 @@ export class Tab extends React.Component { ) } + + private _checkIfShouldScroll(): void { + if (this.props.isSelected && this._tab) { + if (this._tab.scrollIntoViewIfNeeded) { + this._tab.scrollIntoViewIfNeeded({ + behavior: "smooth", + block: "center", + inline: "center", + }) + } + } + } + + private _handleTitleClick(event: React.MouseEvent): void { + if (this._isLeftClick(event)) { + this.props.onSelect(this.props.id) + } else if (this._isMiddleClick(event)) { + this.props.onClose(this.props.id) + } + } + + private _handleCloseButtonClick(): void { + this.props.onClose(this.props.id) + } + + private _isMiddleClick(event: React.MouseEvent): boolean { + return event.button === 1 + } + + private _isLeftClick(event: React.MouseEvent): boolean { + return event.button === 0 + } } export const getTabName = (name: string, isDuplicate?: boolean): string => { @@ -342,8 +367,8 @@ const mapStateToProps = (state: State.IState, ownProps: ITabContainerProps): ITa fontSize: addDefaultUnitIfNeeded(state.configuration["ui.fontSize"]), backgroundColor: state.colors["tabs.background"], foregroundColor: state.colors["tabs.foreground"], - onSelect: selectFunc, - onClose: closeFunc, + onTabSelect: selectFunc, + onTabClose: closeFunc, height, maxWidth, shouldWrap, diff --git a/browser/src/UI/components/Text.tsx b/browser/src/UI/components/Text.tsx index 4ca8764f34..2a131999f1 100644 --- a/browser/src/UI/components/Text.tsx +++ b/browser/src/UI/components/Text.tsx @@ -1,19 +1,16 @@ import * as React from "react" +import styled from "./common" + export interface ITextProps { text: string } -export class TextComponent extends React.PureComponent {} +const Selected = styled.span` + font-style: italic; + text-decoration: underline; +` -export class Text extends TextComponent { - public render(): JSX.Element { - return {this.props.text} - } -} +export const Text = (props: ITextProps) => {props.text} -export class SelectedText extends TextComponent { - public render(): JSX.Element { - return {this.props.text} - } -} +export const SelectedText = (props: ITextProps) => {props.text} diff --git a/browser/src/UI/components/VimNavigator.tsx b/browser/src/UI/components/VimNavigator.tsx index 7cc3d17409..b4711f9243 100644 --- a/browser/src/UI/components/VimNavigator.tsx +++ b/browser/src/UI/components/VimNavigator.tsx @@ -12,6 +12,7 @@ import * as React from "react" +import * as Log from "oni-core-logging" import { Event } from "oni-types" import { KeyboardInputView } from "./../../Input/KeyboardInput" @@ -19,8 +20,6 @@ import { getInstance, IMenuBinding } from "./../../neovim/SharedNeovimInstance" import { CallbackCommand, commandManager } from "./../../Services/CommandManager" -import * as Log from "./../../Log" - export interface IVimNavigatorProps { // activateOnMount: boolean ids: string[] diff --git a/browser/src/UI/components/WindowTitle.tsx b/browser/src/UI/components/WindowTitle.tsx index 824bfaaa40..26a5ceb143 100644 --- a/browser/src/UI/components/WindowTitle.tsx +++ b/browser/src/UI/components/WindowTitle.tsx @@ -10,42 +10,38 @@ import { connect } from "react-redux" import { commandManager } from "./../../Services/CommandManager" import * as State from "./../Shell/ShellState" +import styled from "./common" export interface IWindowTitleViewProps { visible: boolean title: string - backgroundColor: string - foregroundColor: string } -export class WindowTitleView extends React.PureComponent { - public render(): null | JSX.Element { - if (!this.props.visible) { - return null - } - - const style: React.CSSProperties = { - height: "22px", - lineHeight: "22px", - zoom: 1, // Don't allow this to be impacted by zoom - backgroundColor: this.props.backgroundColor, - color: this.props.foregroundColor, - textAlign: "center", - WebkitAppRegion: "drag", - WebkitUserSelect: "none", - pointerEvents: "all", - } +const WindowTitleContainer = styled.div` + height: 22px; + padding: 3px 0; + line-height: 22px; + zoom: 1; /* Dont allow this to be impacted by zoom */ + background-color: ${p => p.theme["title.background"]}; + color: ${p => p.theme["title.foreground"]}; + text-align: center; + -webkit-app-region: drag; + user-select: none; + pointer-events: all; +` + +const onDoubleClick = () => { + commandManager.executeCommand("oni.editor.maximize") +} - return ( -
    - {this.props.title} -
    +export const WindowTitleView = (props: IWindowTitleViewProps) => { + return ( + props.visible && ( + + {props.title} + ) - } - - private onDoubleClick() { - commandManager.executeCommand("oni.editor.maximize") - } + ) } export interface IWindowTitleProps { @@ -59,8 +55,6 @@ export const mapStateToProps = ( return { visible: props.visible && !state.isFullScreen, title: state.windowTitle, - backgroundColor: state.colors["title.background"], - foregroundColor: state.colors["title.foreground"], } } diff --git a/browser/src/UI/components/common.ts b/browser/src/UI/components/common.ts index 95b2548821..1821b8c4a0 100644 --- a/browser/src/UI/components/common.ts +++ b/browser/src/UI/components/common.ts @@ -58,6 +58,8 @@ export function withProps( return styledFunction } +export const pixel = (v: string | number): string => `${v}px` + const darken = (c: string, deg = 0.15) => Color(c) .darken(deg) diff --git a/browser/src/Utility.ts b/browser/src/Utility.ts index c496436b87..9a6ba0db62 100644 --- a/browser/src/Utility.ts +++ b/browser/src/Utility.ts @@ -15,7 +15,8 @@ import * as reduce from "lodash/reduce" import { Observable } from "rxjs/Observable" import { Subject } from "rxjs/Subject" -import { IDisposable } from "oni-types" +import * as JSON5 from "json5" +import { IDisposable, IEvent } from "oni-types" import * as types from "vscode-languageserver-types" @@ -40,6 +41,14 @@ export class Disposable implements IDisposable { } } +export const asObservable = (event: IEvent): Observable => { + const subject = new Subject() + + event.subscribe((val: T) => subject.next(val)) + + return subject +} + /** * Use a `node` require instead of a `webpack` require * The difference is that `webpack` require will bake the javascript @@ -245,3 +254,7 @@ export function ignoreWhilePendingPromise( return ret } + +export const parseJson5 = (text: string): T => { + return JSON5.parse(text) as T +} diff --git a/browser/src/neovim/NeovimInstance.ts b/browser/src/neovim/NeovimInstance.ts index 53ea8f940f..6f4c9dbaf6 100644 --- a/browser/src/neovim/NeovimInstance.ts +++ b/browser/src/neovim/NeovimInstance.ts @@ -4,9 +4,9 @@ import * as path from "path" import * as mkdirp from "mkdirp" import * as Oni from "oni-api" +import * as Log from "oni-core-logging" import { Event, IDisposable, IEvent } from "oni-types" -import * as Log from "./../Log" import * as Performance from "./../Performance" import { CommandContext } from "./CommandContext" import { EventContext } from "./EventContext" @@ -182,7 +182,7 @@ export interface INeovimInstance { // - Refactor remaining events into strongly typed events, as part of the interface on(event: string, handler: NeovimEventHandler): void - setFont(fontFamily: string, fontSize: string, linePadding: number): void + setFont(fontFamily: string, fontSize: string, fontWeight: string, linePadding: number): void getBufferIds(): Promise @@ -209,6 +209,7 @@ export class NeovimInstance extends EventEmitter implements INeovimInstance { private _fontFamily: string private _fontSize: string + private _fontWeight: string private _fontWidthInPixels: number private _fontHeightInPixels: number @@ -369,6 +370,7 @@ export class NeovimInstance extends EventEmitter implements INeovimInstance { this._configuration = configuration this._fontFamily = this._configuration.getValue("editor.fontFamily") this._fontSize = addDefaultUnitIfNeeded(this._configuration.getValue("editor.fontSize")) + this._fontWeight = this._configuration.getValue("editor.fontWeight") this._lastWidthInPixels = widthInPixels this._lastHeightInPixels = heightInPixels @@ -384,7 +386,12 @@ export class NeovimInstance extends EventEmitter implements INeovimInstance { this._bufferUpdateManager.notifyModeChanged(newMode) }) - this._disposables = [s1] + const dispatchScroll = () => this._dispatchScrollEvent() + + const s2 = this._autoCommands.onCursorMoved.subscribe(dispatchScroll) + const s3 = this._autoCommands.onCursorMovedI.subscribe(dispatchScroll) + + this._disposables = [s1, s2, s3] } public dispose(): void { @@ -513,13 +520,20 @@ export class NeovimInstance extends EventEmitter implements INeovimInstance { return ret } - public setFont(fontFamily: string, fontSize: string, linePadding: number): void { + public setFont( + fontFamily: string, + fontSize: string, + fontWeight: string, + linePadding: number, + ): void { this._fontFamily = fontFamily this._fontSize = fontSize + this._fontWeight = fontWeight const { width, height, isBoldAvailable, isItalicAvailable } = measureFont( this._fontFamily, this._fontSize, + this._fontWeight, ) this._fontWidthInPixels = width @@ -530,6 +544,7 @@ export class NeovimInstance extends EventEmitter implements INeovimInstance { Actions.setFont({ fontFamily, fontSize, + fontWeight, fontWidthInPixels: width, fontHeightInPixels: height + linePadding, linePaddingInPixels: linePadding, @@ -656,22 +671,6 @@ export class NeovimInstance extends EventEmitter implements INeovimInstance { return versionInfo[1].version as any } - public dispatchScrollEvent(): void { - if (this._pendingScrollTimeout || this._isDisposed) { - return - } - - this._pendingScrollTimeout = window.setTimeout(async () => { - if (this._isDisposed) { - return - } - - const evt = await this.getContext() - this._onScroll.dispatch(evt) - this._pendingScrollTimeout = null - }) - } - public async quit(): Promise { // This command won't resolve the promise (since it's quitting), // so we're not awaiting.. @@ -701,6 +700,22 @@ export class NeovimInstance extends EventEmitter implements INeovimInstance { } } + private _dispatchScrollEvent(): void { + if (this._pendingScrollTimeout || this._isDisposed) { + return + } + + this._pendingScrollTimeout = window.setTimeout(async () => { + if (this._isDisposed) { + return + } + + const evt = await this.getContext() + this._onScroll.dispatch(evt) + this._pendingScrollTimeout = null + }) + } + private _resizeInternal(rows: number, columns: number): void { if (this._configuration.hasValue("debug.fixedSize")) { const fixedSize = this._configuration.getValue("debug.fixedSize") @@ -760,6 +775,7 @@ export class NeovimInstance extends EventEmitter implements INeovimInstance { break case "scroll": this.emit("action", Actions.scroll(a[0][0])) + this._dispatchScrollEvent() break case "highlight_set": const highlightInfo = a[a.length - 1][0] diff --git a/browser/src/neovim/NeovimProcessSpawner.ts b/browser/src/neovim/NeovimProcessSpawner.ts index 580c7b0a8c..ff5808a82f 100644 --- a/browser/src/neovim/NeovimProcessSpawner.ts +++ b/browser/src/neovim/NeovimProcessSpawner.ts @@ -2,13 +2,13 @@ import { ChildProcess } from "child_process" import * as net from "net" import * as path from "path" +import * as Log from "oni-core-logging" + import * as Platform from "./../Platform" import Process from "./../Plugins/Api/Process" import { Session } from "./Session" -import * as Log from "./../Log" - // Most of the paths coming in the packaged binary reference the `app.asar`, // but the binaries (Neovim) as well as the .vim files are unpacked, // so these need to be mapped to the `app.asar.unpacked` directory @@ -93,7 +93,9 @@ export const startNeovim = async ( let nvimProcessPath = Platform.isWindows() ? nvimWindowsProcessPath - : Platform.isMac() ? nvimMacProcessPath : nvimLinuxPath + : Platform.isMac() + ? nvimMacProcessPath + : nvimLinuxPath nvimProcessPath = remapPathToUnpackedAsar(nvimProcessPath) diff --git a/browser/src/neovim/NeovimTokenColorSynchronizer.ts b/browser/src/neovim/NeovimTokenColorSynchronizer.ts index aea1f29e1e..0d2b82af91 100644 --- a/browser/src/neovim/NeovimTokenColorSynchronizer.ts +++ b/browser/src/neovim/NeovimTokenColorSynchronizer.ts @@ -6,12 +6,13 @@ */ import * as Color from "color" + +import * as Log from "oni-core-logging" + import { TokenColor } from "./../Services/TokenColors" import { NeovimInstance } from "./NeovimInstance" -import * as Log from "./../Log" - const getGuiStringFromTokenColor = (color: TokenColor): string => { if (color.settings.bold && color.settings.italic) { return "gui=bold,italic" diff --git a/browser/src/neovim/NeovimWindowManager.ts b/browser/src/neovim/NeovimWindowManager.ts index 7d20e5aef3..59294eacc5 100644 --- a/browser/src/neovim/NeovimWindowManager.ts +++ b/browser/src/neovim/NeovimWindowManager.ts @@ -13,13 +13,14 @@ import "rxjs/add/operator/distinctUntilChanged" import * as isEqual from "lodash/isEqual" import * as Oni from "oni-api" +import * as Log from "oni-core-logging" import { Event, IEvent } from "oni-types" + import * as types from "vscode-languageserver-types" import { EventContext } from "./EventContext" import { NeovimInstance } from "./index" -import * as Log from "./../Log" import * as Utility from "./../Utility" export interface NeovimTabPageState { @@ -39,6 +40,8 @@ export interface NeovimActiveWindowState { topBufferLine: number bottomBufferLine: number + visibleLines: string[] + bufferToScreen: Oni.Coordinates.BufferToScreen dimensions: Oni.Shapes.Rectangle } @@ -104,8 +107,9 @@ export class NeovimWindowManager extends Utility.Disposable { return Observable.defer(() => this._remeasure(evt)) }) .subscribe((tabState: NeovimTabPageState) => { - this._onWindowStateChangedEvent.dispatch(tabState) - this._neovimInstance.dispatchScrollEvent() + if (tabState) { + this._onWindowStateChangedEvent.dispatch(tabState) + } }) } @@ -235,6 +239,7 @@ export class NeovimWindowManager extends Utility.Disposable { bottomBufferLine: context.windowBottomLine - 1, topBufferLine: context.windowTopLine, dimensions, + visibleLines: lines || [], bufferToScreen: getBufferToScreenFromRanges(offset, expandedWidthRanges), } diff --git a/browser/src/neovim/Screen.ts b/browser/src/neovim/Screen.ts index 7fe7642741..57f98e4939 100644 --- a/browser/src/neovim/Screen.ts +++ b/browser/src/neovim/Screen.ts @@ -27,6 +27,7 @@ export interface IScreen { fontFamily: null | string fontHeightInPixels: number fontSize: null | string + fontWeight: null | string fontWidthInPixels: number foregroundColor: string height: number @@ -84,6 +85,7 @@ export class NeovimScreen implements IScreen { private _fontFamily: null | string = null private _fontHeightInPixels: number private _fontSize: null | string = null + private _fontWeight: null | string = null private _fontWidthInPixels: number private _foregroundColor: string = "#000000" private _grid: Grid = new Grid() @@ -109,6 +111,10 @@ export class NeovimScreen implements IScreen { return this._fontSize } + public get fontWeight(): null | string { + return this._fontWeight + } + public get fontWidthInPixels(): number { return this._fontWidthInPixels } @@ -249,6 +255,7 @@ export class NeovimScreen implements IScreen { case Actions.SET_FONT: this._fontFamily = action.fontFamily this._fontSize = action.fontSize + this._fontWeight = action.fontWeight this._fontWidthInPixels = action.fontWidthInPixels this._fontHeightInPixels = action.fontHeightInPixels this._linePaddingInPixels = action.linePaddingInPixels diff --git a/browser/src/neovim/Session.ts b/browser/src/neovim/Session.ts index 5e7760c161..5d4452fcf3 100644 --- a/browser/src/neovim/Session.ts +++ b/browser/src/neovim/Session.ts @@ -2,7 +2,7 @@ import * as msgpackLite from "msgpack-lite" import { EventEmitter } from "events" -import * as Log from "./../Log" +import * as Log from "oni-core-logging" import { configuration } from "./../Services/Configuration" diff --git a/browser/src/neovim/SharedNeovimInstance.ts b/browser/src/neovim/SharedNeovimInstance.ts index 290522fec9..a18cb3f5fa 100644 --- a/browser/src/neovim/SharedNeovimInstance.ts +++ b/browser/src/neovim/SharedNeovimInstance.ts @@ -8,6 +8,7 @@ * - Enabling Neovim keybindings in text input elements */ +import * as Log from "oni-core-logging" import { Event, IDisposable, IEvent } from "oni-types" import { CommandContext } from "./CommandContext" @@ -21,7 +22,6 @@ import { Configuration } from "./../Services/Configuration" import { PromiseQueue } from "./../Services/Language/PromiseQueue" import * as App from "./../App" -import * as Log from "./../Log" export interface IBinding { input(key: string): Promise diff --git a/browser/src/neovim/actions.ts b/browser/src/neovim/actions.ts index 9cb4811d69..8560f6513c 100644 --- a/browser/src/neovim/actions.ts +++ b/browser/src/neovim/actions.ts @@ -42,6 +42,7 @@ export interface IKeyboardInputAction extends IAction { interface ISetFontArguments { fontFamily: string fontSize: string + fontWeight: string fontWidthInPixels: number fontHeightInPixels: number linePaddingInPixels: number @@ -52,6 +53,7 @@ interface ISetFontArguments { export interface ISetFontAction extends IAction { fontFamily: string fontSize: string + fontWeight: string fontWidthInPixels: number fontHeightInPixels: number linePaddingInPixels: number @@ -204,6 +206,7 @@ export function changeMode(mode: string): IChangeModeAction { export function setFont({ fontFamily, fontSize, + fontWeight, fontWidthInPixels, fontHeightInPixels, linePaddingInPixels, @@ -214,6 +217,7 @@ export function setFont({ type: SET_FONT, fontFamily, fontSize, + fontWeight, fontWidthInPixels, fontHeightInPixels, linePaddingInPixels, diff --git a/browser/test/Input/InputManagerTests.ts b/browser/test/Input/InputManagerTests.ts index d478655957..5591b55a81 100644 --- a/browser/test/Input/InputManagerTests.ts +++ b/browser/test/Input/InputManagerTests.ts @@ -53,6 +53,15 @@ describe("InputManager", () => { assert.strictEqual(handled, false) }) + it("can unbind an array of keys", () => { + const im = new InputManager() + im.bind(["a", "b"], "test.command") + im.unbind(["a", "b"]) + + const boundKeys = im.getBoundKeys("test.command") + assert.deepEqual(boundKeys, [], "Validate no bound keys are returned") + }) + describe("getBoundKeys", () => { it("returns empty array if no key bound to command", () => { const im = new InputManager() diff --git a/browser/test/Input/KeyParserTests.ts b/browser/test/Input/KeyParserTests.ts index 0a98be824e..2377db0e03 100644 --- a/browser/test/Input/KeyParserTests.ts +++ b/browser/test/Input/KeyParserTests.ts @@ -31,4 +31,26 @@ describe("KeyParser", () => { ]) }) }) + + describe("parseChordParts", () => { + it("parses modifier keys", () => { + const tests: Array<[string, string[]]> = [ + ["a", ["a"]], + ["", ["control", "a"]], + ["", ["meta", "a"]], + ["", ["alt", "a"]], + ["", ["shift", "a"]], + ["", ["meta", "control", "alt", "shift", "a"]], + ] + + tests.forEach(test => { + assert.deepEqual(KeyParser.parseChordParts(test[0]), test[1]) + }) + }) + + it("ignores keys beyond the first chord", () => { + const result = KeyParser.parseChordParts("b") + assert.deepEqual(result, ["control", "a"]) + }) + }) }) diff --git a/browser/test/Mocks/neovim.ts b/browser/test/Mocks/neovim.ts index 5007bc3948..d06c325252 100644 --- a/browser/test/Mocks/neovim.ts +++ b/browser/test/Mocks/neovim.ts @@ -34,6 +34,9 @@ export class MockScreen implements Neovim.IScreen { public get fontSize(): null | string { return null } + public get fontWeight(): null | string { + return null + } public get fontWidthInPixels(): number { return null } diff --git a/browser/test/Services/Explorer/ExplorerFileSystemTests.ts b/browser/test/Services/Explorer/ExplorerFileSystemTests.ts index 15daaa1cd7..a9875d01e6 100644 --- a/browser/test/Services/Explorer/ExplorerFileSystemTests.ts +++ b/browser/test/Services/Explorer/ExplorerFileSystemTests.ts @@ -100,3 +100,59 @@ describe("File System tests", async () => { } }) }) + +describe("readdir", () => { + it("should not error if directory contains a broken symlink", async () => { + const goodDir = path.join("fake_dir", "good_dir") + const goodFile = path.join("fake_dir", "good_file") + const badFile = path.join("fake_dir", "bad_file") + + const fileSystem = new FileSystem({ + readdir(dirPath: string, callback: (err?: Error, result?: string[]) => void) { + assert.strictEqual(dirPath, "fake_dir") + callback(null, ["good_dir", "good_file", "bad_file"]) + }, + stat(targetPath: string, callback: (err?: Error, result?: any) => void) { + switch (targetPath) { + case goodDir: + return callback(null, { + isDirectory() { + return true + }, + }) + case goodFile: + return callback(null, { + isDirectory() { + return false + }, + }) + default: + // On Linux at least, an fs.stat call to a missing file and a broken symlink both result in this error: + return callback( + new Error(`ENOENT: no such file or directory, stat ${targetPath}`), + ) + } + }, + exists(targetPath: string, callback: any) { + assert.fail("Should not be used") + }, + } as any) + + const expected = [ + { + type: "folder", + fullPath: goodDir, + }, + { + type: "file", + fullPath: goodFile, + }, + { + type: "file", + fullPath: badFile, + }, + ] + const result = await fileSystem.readdir("fake_dir") + assert.deepEqual(result, expected) + }) +}) diff --git a/browser/test/Services/Explorer/ExplorerSelectorsTests.ts b/browser/test/Services/Explorer/ExplorerSelectorsTests.ts index 333c68c4b2..d2bfdc8a90 100644 --- a/browser/test/Services/Explorer/ExplorerSelectorsTests.ts +++ b/browser/test/Services/Explorer/ExplorerSelectorsTests.ts @@ -88,4 +88,41 @@ describe("ExplorerSelectors", () => { assert.deepEqual(result, expectedResult) }) }) + + describe("mapStateToNodeList", () => { + it("expands the root container", () => { + const state: ExplorerState.IExplorerState = { + ...ExplorerState.DefaultExplorerState, + rootFolder: { + type: "folder", + fullPath: "rootPath", + }, + expandedFolders: { + rootPath: [], + }, + } + + const result = ExplorerSelectors.mapStateToNodeList(state) + + const container = result[0] as ExplorerSelectors.IContainerNode + assert.strictEqual(container.type, "container") + assert.strictEqual(container.expanded, true) + }) + + it("collapses the root container", () => { + const state: ExplorerState.IExplorerState = { + ...ExplorerState.DefaultExplorerState, + rootFolder: { + type: "folder", + fullPath: "rootPath", + }, + expandedFolders: {}, + } + const result = ExplorerSelectors.mapStateToNodeList(state) + + const container = result[0] as ExplorerSelectors.IContainerNode + assert.strictEqual(container.type, "container") + assert.strictEqual(container.expanded, false) + }) + }) }) diff --git a/browser/test/Services/QuickOpen/FinderProcessTests.ts b/browser/test/Services/QuickOpen/FinderProcessTests.ts new file mode 100644 index 0000000000..1ea1e975e8 --- /dev/null +++ b/browser/test/Services/QuickOpen/FinderProcessTests.ts @@ -0,0 +1,60 @@ +/** + * FinderProcessTests.ts + */ + +import * as assert from "assert" +import { extractSplitData } from "../../../src/Services/QuickOpen/FinderProcess" + +describe("extractSplitData", () => { + it("Splits the data by the delimiter", () => { + const data = "file1\nfile2\nfile3\n" + const delimiter = "\n" + const lastRemnant = "" + + const { splitData } = extractSplitData(data, delimiter, lastRemnant) + + assert.equal(splitData.length, 3) + }) + + it("Ignores empty input", () => { + const data = "" + const delimiter = "\n" + const lastRemnant = "" + + const { didExtract } = extractSplitData(data, delimiter, lastRemnant) + + assert.equal(didExtract, false) + }) + + it("Returns a remnant if the data doesn't end with the delimiter", () => { + const data = "file1\nfile2" + const delimiter = "\n" + const lastRemnant = "" + + const { remnant, splitData } = extractSplitData(data, delimiter, lastRemnant) + + assert.equal(remnant, "file2") + assert.equal(splitData.length, 1) + }) + + it("Returns an empty remnant if the data does end with the delimiter", () => { + const data = "file1\nfile2\n" + const delimiter = "\n" + const lastRemnant = "" + + const { remnant } = extractSplitData(data, delimiter, lastRemnant) + + assert.equal(remnant, "") + }) + + it("Prepends the last remnant if there was one", () => { + const data = "e1\nfile2\n" + const delimiter = "\n" + const lastRemnant = "fil" + + const { splitData } = extractSplitData(data, delimiter, lastRemnant) + + assert.equal(splitData.length, 2) + assert.equal(splitData[0], "file1") + }) +}) diff --git a/browser/test/Services/Snippets/SnippetProviderTests.ts b/browser/test/Services/Snippets/SnippetProviderTests.ts new file mode 100644 index 0000000000..925fd3be63 --- /dev/null +++ b/browser/test/Services/Snippets/SnippetProviderTests.ts @@ -0,0 +1,73 @@ +/** + * SnippetProviderTests.ts + */ + +import * as assert from "assert" +import * as os from "os" + +import { loadSnippetsFromText } from "./../../../src/Services/Snippets/SnippetProvider" + +const ArraySnippet = ` +{ + "if": { + "prefix": "test", + "body": [ + "line1", + "line2" + ], + "description": "Code snippet for an if statement" + } +} +` + +const SingleLineSnippet = ` +{ + "if": { + "prefix": "test", + "body": "line1", + "description": "Code snippet for an if statement" + } +} +` + +const TrailingCommaSnippet = ` +{ + "if": { + "prefix": "test", + "body": ["line1"], + "description": "Code snippet for an if statement" + }, +} +` + +describe("SnippetProviderTests", () => { + describe("loadSnippetsFromText", () => { + it("parses a basic snippet", async () => { + const [parsedArraySnippet] = loadSnippetsFromText(ArraySnippet) + + assert.strictEqual( + parsedArraySnippet.body, + "line1" + os.EOL + "line2", + "Validate body was parsed correctly", + ) + }) + + it("parses single-line snippet", async () => { + const [parsedSingleLineSnippet] = loadSnippetsFromText(SingleLineSnippet) + assert.strictEqual( + parsedSingleLineSnippet.body, + "line1", + "Validate body was parsed correctly", + ) + }) + + it("parses snippet with trailing comma", async () => { + const [parsedTrailingCommaSnippet] = loadSnippetsFromText(TrailingCommaSnippet) + assert.strictEqual( + parsedTrailingCommaSnippet.body, + "line1", + "Validate body was parsed correctly", + ) + }) + }) +}) diff --git a/browser/test/Services/WindowManager/WindowManagerTests.ts b/browser/test/Services/WindowManager/WindowManagerTests.ts index 1055f0f4b9..589d658892 100644 --- a/browser/test/Services/WindowManager/WindowManagerTests.ts +++ b/browser/test/Services/WindowManager/WindowManagerTests.ts @@ -22,18 +22,18 @@ describe("WindowManagerTests", () => { const handle1 = windowManager.createSplit("horizontal", split1) const handle2 = windowManager.createSplit("vertical", split2, split1) - assert.strictEqual(windowManager.activeSplit.id, handle2.id) + assert.strictEqual(windowManager.activeSplitHandle.id, handle2.id) handle2.close() - assert.strictEqual(windowManager.activeSplit.id, handle1.id) + assert.strictEqual(windowManager.activeSplitHandle.id, handle1.id) const handle3 = windowManager.createSplit("horizontal", split3, split1) - assert.strictEqual(windowManager.activeSplit.id, handle3.id) + assert.strictEqual(windowManager.activeSplitHandle.id, handle3.id) handle3.close() - assert.strictEqual(windowManager.activeSplit.id, handle1.id) + assert.strictEqual(windowManager.activeSplitHandle.id, handle1.id) }) it("can get split after a split is closed", async () => { @@ -74,4 +74,42 @@ describe("WindowManagerTests", () => { ) assert.strictEqual(firstChild.splits.length, 2, "Validate both windows are in this split") }) + + it("#2147 - doesn't leave a split container after closing", async () => { + const split1 = new MockWindowSplit("window1") + const split2 = new MockWindowSplit("window2") + const split3 = new MockWindowSplit("window3") + + windowManager.createSplit("horizontal", split1) + + const split2Handle = windowManager.createSplit("vertical", split2, split1) + const split3Handle = windowManager.createSplit("horizontal", split3, split2) + + split2Handle.close() + split3Handle.close() + + const splitRoot = windowManager.splitRoot + const firstChild = splitRoot.splits[0] as ISplitInfo + + assert.strictEqual(splitRoot.type, "Split") + assert.strictEqual(splitRoot.direction, "vertical") + assert.strictEqual(splitRoot.splits.length, 1) + + assert.strictEqual(firstChild.type, "Split") + assert.strictEqual( + firstChild.direction, + "horizontal", + "Validate the splits are arranged horizontally (it's confusing... but this means they are vertical splits)", + ) + assert.strictEqual( + firstChild.splits.length, + 1, + "Validate there is only a single child in the 'horizontal' split.", + ) + assert.strictEqual( + firstChild.splits[0].type, + "Leaf", + "Validate the child of the 'horizontal' split is a leaf node.", + ) + }) }) diff --git a/browser/tsconfig.json b/browser/tsconfig.json index d8157e6cc0..c2be110923 100644 --- a/browser/tsconfig.json +++ b/browser/tsconfig.json @@ -26,6 +26,6 @@ "sourceMap": true, "types": ["mocha", "webgl2"] }, - "include": ["src/**/*.ts", "test/**/*.ts"], + "include": ["../@types/**/*.d.ts", "src/**/*.ts", "test/**/*.ts"], "exclude": ["node_modules"] } diff --git a/browser/tsconfig.test.json b/browser/tsconfig.test.json index f2488b6c90..ce0db2c4e5 100644 --- a/browser/tsconfig.test.json +++ b/browser/tsconfig.test.json @@ -22,6 +22,6 @@ "sourceMap": true, "types": ["mocha", "webgl2"] }, - "include": ["src/**/*.ts", "src/**/*.tsx", "test/**/*.ts"], + "include": ["../@types/**/*.d.ts", "src/**/*.ts", "src/**/*.tsx", "test/**/*.ts"], "exclude": ["node_modules"] } diff --git a/extensions/oni-plugin-markdown-preview/package.json b/extensions/oni-plugin-markdown-preview/package.json index d27bf36027..a890d11ac3 100644 --- a/extensions/oni-plugin-markdown-preview/package.json +++ b/extensions/oni-plugin-markdown-preview/package.json @@ -21,7 +21,7 @@ } }, "tbd-dependencies": { - "marked": "^0.3.6", + "marked": "^0.4.0", "dompurify": "1.0.2", "oni-types": "^0.0.4", "oni-api": "^0.0.9" diff --git a/extensions/oni-plugin-markdown-preview/src/index.tsx b/extensions/oni-plugin-markdown-preview/src/index.tsx index 516b4efe12..cf5479ecf3 100644 --- a/extensions/oni-plugin-markdown-preview/src/index.tsx +++ b/extensions/oni-plugin-markdown-preview/src/index.tsx @@ -46,6 +46,7 @@ class MarkdownPreview extends React.PureComponent this.onBufferChanged(args)) // TODO: Subscribe "onFocusChanged" - this.subscribe(activeEditor.onBufferScrolled, args => this.onBufferScrolled(args)) + + if (this.props.oni.configuration.getValue("experimental.markdownPreview.autoScroll")) { + this.subscribe(activeEditor.onBufferScrolled, args => this.onBufferScrolled(args)) + } this.previewBuffer(activeEditor.activeBuffer) } @@ -153,6 +157,10 @@ class MarkdownPreview extends React.PureComponent this.onBufferEnter(args)) + this._oni.editors.activeEditor.onBufferLeave.subscribe(args => this.onBufferLeave(args)) } public isPaneOpen(): boolean { @@ -203,7 +213,7 @@ class MarkdownPreviewEditor implements Oni.IWindowSplit { public toggle(): void { if (this._open) { - this.close() + this.close(true) } else { this.open() } @@ -212,14 +222,19 @@ class MarkdownPreviewEditor implements Oni.IWindowSplit { public open(): void { if (!this._open) { this._open = true + this._manuallyClosed = false + const editorSplit = this._oni.windows.activeSplitHandle + // TODO: Update API this._split = this._oni.windows.createSplit("vertical", this) + editorSplit.focus() } } - public close(): void { + public close(manuallyClosed = false): void { if (this._open) { this._open = false + this._manuallyClosed = manuallyClosed this._split.close() } } @@ -229,10 +244,14 @@ class MarkdownPreviewEditor implements Oni.IWindowSplit { } private onBufferEnter(bufferInfo: Oni.EditorBufferEventArgs): void { - if (bufferInfo.language === "markdown") { + if (bufferInfo.language === "markdown" && this._manuallyClosed === false) { this.open() } } + + private onBufferLeave(bufferInfo: Oni.EditorBufferEventArgs): void { + this.close() + } } export function activate(oni: any): any { @@ -259,7 +278,7 @@ export function activate(oni: any): any { "Close Markdown Preview", "Close the Markdown preview pane if it is not already closed", () => { - preview.close() + preview.close(true) }, ), ) diff --git a/jest.config.js b/jest.config.js index b2dd906656..1ab99e87ed 100644 --- a/jest.config.js +++ b/jest.config.js @@ -10,7 +10,6 @@ module.exports = { PersistentSettings: "/ui-tests/mocks/PersistentSettings.ts", Utility: "/ui-tests/mocks/Utility.ts", Configuration: "/ui-tests/mocks/Configuration.ts", - classnames: "/ui-tests/mocks/classnames.ts", KeyboardLayout: "/ui-tests/mocks/keyboardLayout.ts", }, snapshotSerializers: ["enzyme-to-json/serializer"], diff --git a/main/src/main.ts b/main/src/main.ts index 3b68a2404a..8774469fd6 100644 --- a/main/src/main.ts +++ b/main/src/main.ts @@ -242,6 +242,11 @@ export function createWindow( Log.info("...closed event completed") }) + currentWindow.webContents.on("will-navigate", (event, url) => { + event.preventDefault() + currentWindow.webContents.send("open-oni-browser", url) + }) + windows.push(currentWindow) return currentWindow diff --git a/main/src/menu.ts b/main/src/menu.ts index 3f65de3f67..11e49f4d0d 100644 --- a/main/src/menu.ts +++ b/main/src/menu.ts @@ -67,6 +67,7 @@ export const buildMenu = (mainWindow, loadInit) => { submenu: [ { label: "Edit Oni config", + accelerator: "CmdOrCtrl+,", click(item, focusedWindow) { executeOniCommand(focusedWindow, "oni.config.openConfigJs") }, @@ -77,6 +78,7 @@ export const buildMenu = (mainWindow, loadInit) => { if (loadInit) { preferences.submenu.push({ label: "Edit Neovim config", + accelerator: null, click(item, focusedWindow) { executeOniCommand(focusedWindow, "oni.config.openInitVim") }, diff --git a/package.json b/package.json index 0b8802ad25..47e9c40b38 100644 --- a/package.json +++ b/package.json @@ -3,9 +3,16 @@ "author": "", "email": "bryphe@outlook.com", "homepage": "https://www.onivim.io", - "version": "0.3.4", + "version": "0.3.5", "description": "Code editor with a modern twist on modal editing - powered by neovim.", - "keywords": ["vim", "neovim", "text", "editor", "ide", "vim"], + "keywords": [ + "vim", + "neovim", + "text", + "editor", + "ide", + "vim" + ], "main": "./lib/main/src/main.js", "bin": { "oni": "./cli/oni", @@ -44,23 +51,39 @@ "mac": { "artifactName": "${productName}-${version}-osx.${ext}", "category": "public.app-category.developer-tools", - "target": ["dmg"], - "files": ["bin/osx/**/*"] + "target": [ + "dmg" + ], + "files": [ + "bin/osx/**/*" + ] }, "linux": { "artifactName": "${productName}-${version}-${arch}-linux.${ext}", "maintainer": "bryphe@outlook.com", - "target": ["tar.gz", "deb", "rpm"] + "target": [ + "tar.gz", + "deb", + "rpm" + ] }, "win": { - "target": ["zip", "dir"], - "files": ["bin/x86/**/*"] + "target": [ + "zip", + "dir" + ], + "files": [ + "bin/x86/**/*" + ] }, "fileAssociations": [ { "name": "ADA source", "role": "Editor", - "ext": ["adb", "ads"] + "ext": [ + "adb", + "ads" + ] }, { "name": "Compiled AppleScript", @@ -80,12 +103,20 @@ { "name": "ASP document", "role": "Editor", - "ext": ["asp", "asa"] + "ext": [ + "asp", + "asa" + ] }, { "name": "ASP.NET document", "role": "Editor", - "ext": ["aspx", "ascx", "asmx", "ashx"] + "ext": [ + "aspx", + "ascx", + "asmx", + "ashx" + ] }, { "name": "BibTeX bibliography", @@ -100,7 +131,13 @@ { "name": "C++ source", "role": "Editor", - "ext": ["cc", "cp", "cpp", "cxx", "c++"] + "ext": [ + "cc", + "cp", + "cpp", + "cxx", + "c++" + ] }, { "name": "C# source", @@ -125,7 +162,10 @@ { "name": "Clojure source", "role": "Editor", - "ext": ["clj", "cljs"] + "ext": [ + "clj", + "cljs" + ] }, { "name": "Comma separated values", @@ -140,12 +180,20 @@ { "name": "CGI script", "role": "Editor", - "ext": ["cgi", "fcgi"] + "ext": [ + "cgi", + "fcgi" + ] }, { "name": "Configuration file", "role": "Editor", - "ext": ["cfg", "conf", "config", "htaccess"] + "ext": [ + "cfg", + "conf", + "config", + "htaccess" + ] }, { "name": "Cascading style sheet", @@ -170,7 +218,10 @@ { "name": "Erlang source", "role": "Editor", - "ext": ["erl", "hrl"] + "ext": [ + "erl", + "hrl" + ] }, { "name": "F-Script source", @@ -180,17 +231,32 @@ { "name": "Fortran source", "role": "Editor", - "ext": ["f", "for", "fpp", "f77", "f90", "f95"] + "ext": [ + "f", + "for", + "fpp", + "f77", + "f90", + "f95" + ] }, { "name": "Header", "role": "Editor", - "ext": ["h", "pch"] + "ext": [ + "h", + "pch" + ] }, { "name": "C++ header", "role": "Editor", - "ext": ["hh", "hpp", "hxx", "h++"] + "ext": [ + "hh", + "hpp", + "hxx", + "h++" + ] }, { "name": "Go source", @@ -200,17 +266,28 @@ { "name": "GTD document", "role": "Editor", - "ext": ["gtd", "gtdlog"] + "ext": [ + "gtd", + "gtdlog" + ] }, { "name": "Haskell source", "role": "Editor", - "ext": ["hs", "lhs"] + "ext": [ + "hs", + "lhs" + ] }, { "name": "HTML document", "role": "Editor", - "ext": ["htm", "html", "phtml", "shtml"] + "ext": [ + "htm", + "html", + "phtml", + "shtml" + ] }, { "name": "Include file", @@ -250,7 +327,10 @@ { "name": "JavaScript source", "role": "Editor", - "ext": ["js", "htc"] + "ext": [ + "js", + "htc" + ] }, { "name": "Java Server Page", @@ -275,7 +355,14 @@ { "name": "Lisp source", "role": "Editor", - "ext": ["lisp", "cl", "l", "lsp", "mud", "el"] + "ext": [ + "lisp", + "cl", + "l", + "lsp", + "mud", + "el" + ] }, { "name": "Log file", @@ -295,7 +382,12 @@ { "name": "Markdown document", "role": "Editor", - "ext": ["markdown", "mdown", "markdn", "md"] + "ext": [ + "markdown", + "mdown", + "markdn", + "md" + ] }, { "name": "Makefile source", @@ -305,17 +397,29 @@ { "name": "Mediawiki document", "role": "Editor", - "ext": ["wiki", "wikipedia", "mediawiki"] + "ext": [ + "wiki", + "wikipedia", + "mediawiki" + ] }, { "name": "MIPS assembler source", "role": "Editor", - "ext": ["s", "mips", "spim", "asm"] + "ext": [ + "s", + "mips", + "spim", + "asm" + ] }, { "name": "Modula-3 source", "role": "Editor", - "ext": ["m3", "cm3"] + "ext": [ + "m3", + "cm3" + ] }, { "name": "MoinMoin document", @@ -335,17 +439,28 @@ { "name": "OCaml source", "role": "Editor", - "ext": ["ml", "mli", "mll", "mly"] + "ext": [ + "ml", + "mli", + "mll", + "mly" + ] }, { "name": "Mustache document", "role": "Editor", - "ext": ["mustache", "hbs"] + "ext": [ + "mustache", + "hbs" + ] }, { "name": "Pascal source", "role": "Editor", - "ext": ["pas", "p"] + "ext": [ + "pas", + "p" + ] }, { "name": "Patch file", @@ -355,7 +470,11 @@ { "name": "Perl source", "role": "Editor", - "ext": ["pl", "pod", "perl"] + "ext": [ + "pl", + "pod", + "perl" + ] }, { "name": "Perl module", @@ -365,47 +484,80 @@ { "name": "PHP source", "role": "Editor", - "ext": ["php", "php3", "php4", "php5"] + "ext": [ + "php", + "php3", + "php4", + "php5" + ] }, { "name": "PostScript source", "role": "Editor", - "ext": ["ps", "eps"] + "ext": [ + "ps", + "eps" + ] }, { "name": "Property list", "role": "Editor", - "ext": ["dict", "plist", "scriptSuite", "scriptTerminology"] + "ext": [ + "dict", + "plist", + "scriptSuite", + "scriptTerminology" + ] }, { "name": "Python source", "role": "Editor", - "ext": ["py", "rpy", "cpy", "python"] + "ext": [ + "py", + "rpy", + "cpy", + "python" + ] }, { "name": "R source", "role": "Editor", - "ext": ["r", "s"] + "ext": [ + "r", + "s" + ] }, { "name": "Ragel source", "role": "Editor", - "ext": ["rl", "ragel"] + "ext": [ + "rl", + "ragel" + ] }, { "name": "Remind document", "role": "Editor", - "ext": ["rem", "remind"] + "ext": [ + "rem", + "remind" + ] }, { "name": "reStructuredText document", "role": "Editor", - "ext": ["rst", "rest"] + "ext": [ + "rst", + "rest" + ] }, { "name": "HTML with embedded Ruby", "role": "Editor", - "ext": ["rhtml", "erb"] + "ext": [ + "rhtml", + "erb" + ] }, { "name": "SQL with embedded Ruby", @@ -415,17 +567,28 @@ { "name": "Ruby source", "role": "Editor", - "ext": ["rb", "rbx", "rjs", "rxml"] + "ext": [ + "rb", + "rbx", + "rjs", + "rxml" + ] }, { "name": "Sass source", "role": "Editor", - "ext": ["sass", "scss"] + "ext": [ + "sass", + "scss" + ] }, { "name": "Scheme source", "role": "Editor", - "ext": ["scm", "sch"] + "ext": [ + "scm", + "sch" + ] }, { "name": "Setext document", @@ -473,7 +636,10 @@ { "name": "SWIG source", "role": "Editor", - "ext": ["i", "swg"] + "ext": [ + "i", + "swg" + ] }, { "name": "Tcl source", @@ -483,12 +649,20 @@ { "name": "TeX document", "role": "Editor", - "ext": ["tex", "sty", "cls"] + "ext": [ + "tex", + "sty", + "cls" + ] }, { "name": "Plain text document", "role": "Editor", - "ext": ["text", "txt", "utf8"] + "ext": [ + "text", + "txt", + "utf8" + ] }, { "name": "Textile document", @@ -508,17 +682,32 @@ { "name": "XML document", "role": "Editor", - "ext": ["xml", "xsd", "xib", "rss", "tld", "pt", "cpt", "dtml"] + "ext": [ + "xml", + "xsd", + "xib", + "rss", + "tld", + "pt", + "cpt", + "dtml" + ] }, { "name": "XSL stylesheet", "role": "Editor", - "ext": ["xsl", "xslt"] + "ext": [ + "xsl", + "xslt" + ] }, { "name": "Electronic business card", "role": "Editor", - "ext": ["vcf", "vcard"] + "ext": [ + "vcf", + "vcard" + ] }, { "name": "Visual Basic source", @@ -528,7 +717,10 @@ { "name": "YAML document", "role": "Editor", - "ext": ["yaml", "yml"] + "ext": [ + "yaml", + "yml" + ] }, { "name": "Text document", @@ -589,96 +781,66 @@ }, "scripts": { "precommit": "pretty-quick --staged", - "prepush": "npm run build && npm run lint", - "build": - "npm run build:browser && npm run build:webview_preload && npm run build:main && npm run build:plugins", - "build-debug": "npm run build:browser-debug && npm run build:main && npm run build:plugins", + "prepush": "yarn run build && yarn run lint", + "build": "yarn run build:browser && yarn run build:webview_preload && yarn run build:main && yarn run build:plugins", + "build-debug": "yarn run build:browser-debug && yarn run build:main && yarn run build:plugins", "build:browser": "webpack --config browser/webpack.production.config.js", "build:browser-debug": "webpack --config browser/webpack.debug.config.js", "build:main": "cd main && tsc -p tsconfig.json", - "build:plugins": - "npm run build:plugin:oni-plugin-typescript && npm run build:plugin:oni-plugin-markdown-preview", - "build:plugin:oni-plugin-typescript": "cd vim/core/oni-plugin-typescript && npm run build", - "build:plugin:oni-plugin-markdown-preview": - "cd extensions/oni-plugin-markdown-preview && npm run build", - "build:test": "npm run build:test:unit && npm run build:test:integration", + "build:plugins": "yarn run build:plugin:oni-plugin-typescript && yarn run build:plugin:oni-plugin-markdown-preview", + "build:plugin:oni-plugin-typescript": "cd vim/core/oni-plugin-typescript && yarn run build", + "build:plugin:oni-plugin-markdown-preview": "cd extensions/oni-plugin-markdown-preview && yarn run build", + "build:test": "yarn run build:test:unit && yarn run build:test:integration", "build:test:integration": "cd test && tsc -p tsconfig.json", - "build:test:unit": "npm run build:test:unit:browser && npm run build:test:unit:main", - "build:test:unit:browser": - "rimraf lib_test/browser && cd browser && tsc -p tsconfig.test.json", + "build:test:unit": "yarn run build:test:unit:browser && yarn run build:test:unit:main", + "build:test:unit:browser": "rimraf lib_test/browser && cd browser && tsc -p tsconfig.test.json", "build:test:unit:main": "rimraf lib_test/main && cd main && tsc -p tsconfig.test.json", "build:webview_preload": "cd webview_preload && tsc -p tsconfig.json", "check-cached-binaries": "node build/script/CheckBinariesForBuild.js", "copy-icons": "node build/CopyIcons.js", - "debug:test:unit:browser": - "cd browser && tsc -p tsconfig.test.json && electron-mocha --interactive --debug --renderer --require testHelpers.js --recursive ../lib_test/browser/test", - "demo:screenshot": - "npm run build:test && cross-env DEMO_TEST=HeroScreenshot mocha -t 30000000 lib_test/test/Demo.js", - "demo:video": - "npm run build:test && cross-env DEMO_TEST=HeroDemo mocha -t 30000000 lib_test/test/Demo.js", - "dist:win:x86": - "cross-env ELECTRON_BUILDER_ALLOW_UNRESOLVED_DEPENDENCIES=1 build --arch ia32 --publish never", - "dist:win:x64": - "cross-env ELECTRON_BUILDER_ALLOW_UNRESOLVED_DEPENDENCIES=1 build --arch x64 --publish never", - "pack:win": - "node build/BuildSetupTemplate.js && innosetup-compiler dist/setup.iss --verbose --O=dist", - "test": "npm run test:unit && npm run test:integration", - "test:integration": "npm run build:test && mocha -t 120000 lib_test/test/CiTests.js --bail", + "debug:test:unit:browser": "cd browser && tsc -p tsconfig.test.json && electron-mocha --interactive --debug --renderer --require testHelpers.js --recursive ../lib_test/browser/test", + "demo:screenshot": "yarn run build:test && cross-env DEMO_TEST=HeroScreenshot mocha -t 30000000 lib_test/test/Demo.js", + "demo:video": "yarn run build:test && cross-env DEMO_TEST=HeroDemo mocha -t 30000000 lib_test/test/Demo.js", + "dist:win:x86": "cross-env ELECTRON_BUILDER_ALLOW_UNRESOLVED_DEPENDENCIES=1 build --arch ia32 --publish never", + "dist:win:x64": "cross-env ELECTRON_BUILDER_ALLOW_UNRESOLVED_DEPENDENCIES=1 build --arch x64 --publish never", + "pack:win": "node build/BuildSetupTemplate.js && innosetup-compiler dist/setup.iss --verbose --O=dist", + "test": "yarn run test:unit && yarn run test:integration", + "test:integration": "yarn run build:test && mocha -t 120000 lib_test/test/CiTests.js --bail", "test:react": "jest --config ./jest.config.js ./ui-tests", "test:react:watch": "jest --config ./jest.config.js ./ui-tests --watch", "test:react:coverage": "jest --config ./jest.config.js ./ui-tests --coverage", - "test:unit:browser": - "npm run build:test:unit:browser && cd browser && electron-mocha --renderer --require testHelpers.js --recursive ../lib_test/browser/test", - "test:unit": "npm run test:unit:browser && npm run test:unit:main && npm run test:react", - "test:unit:main": - "npm run build:test:unit:main && cd main && electron-mocha --renderer --recursive ../lib_test/main/test", + "test:unit:browser": "yarn run build:test:unit:browser && cd browser && electron-mocha --renderer --require testHelpers.js --recursive ../lib_test/browser/test", + "test:unit": "yarn run test:unit:browser && yarn run test:unit:main && yarn run test:react", + "test:unit:main": "yarn run build:test:unit:main && cd main && electron-mocha --renderer --recursive ../lib_test/main/test", "upload:dist": "node build/script/UploadDistributionBuildsToAzure", - "fix-lint": "npm run fix-lint:browser && npm run fix-lint:main && npm run fix-lint:test", - "fix-lint:browser": - "tslint --fix --project browser/tsconfig.json --exclude **/node_modules/**/* --config tslint.json && tslint --fix --project vim/core/oni-plugin-typescript/tsconfig.json --config tslint.json && tslint --fix --project extensions/oni-plugin-markdown-preview/tsconfig.json --config tslint.json", + "fix-lint": "yarn run fix-lint:browser && yarn run fix-lint:main && yarn run fix-lint:test", + "fix-lint:browser": "tslint --fix --project browser/tsconfig.json --exclude **/node_modules/**/* --config tslint.json && tslint --fix --project vim/core/oni-plugin-typescript/tsconfig.json --config tslint.json && tslint --fix --project extensions/oni-plugin-markdown-preview/tsconfig.json --config tslint.json", "fix-lint:main": "tslint --fix --project main/tsconfig.json --config tslint.json", "fix-lint:test": "tslint --fix --project test/tsconfig.json --config tslint.json", - "lint": "npm run lint:browser && npm run lint:main && npm run lint:test", - "lint:browser": - "tslint --project browser/tsconfig.json --config tslint.json && tslint --project vim/core/oni-plugin-typescript/tsconfig.json --config tslint.json && tslint --project extensions/oni-plugin-markdown-preview/tsconfig.json --config tslint.json", + "lint": "yarn run lint:browser && yarn run lint:main && yarn run lint:test", + "lint:browser": "tslint --project browser/tsconfig.json --config tslint.json && tslint --project vim/core/oni-plugin-typescript/tsconfig.json --config tslint.json && tslint --project extensions/oni-plugin-markdown-preview/tsconfig.json --config tslint.json", "lint:main": "tslint --project main/tsconfig.json --config tslint.json", - "lint:test": - "tslint --project test/tsconfig.json --config tslint.json && tslint vim/core/oni-plugin-typescript/src/**/*.ts && tslint extensions/oni-plugin-markdown-preview/src/**/*.ts", + "lint:test": "tslint --project test/tsconfig.json --config tslint.json && tslint vim/core/oni-plugin-typescript/src/**/*.ts && tslint extensions/oni-plugin-markdown-preview/src/**/*.ts", "pack": "cross-env ELECTRON_BUILDER_ALLOW_UNRESOLVED_DEPENDENCIES=1 build --publish never", - "ccov:instrument": - "nyc instrument --all true --sourceMap false lib_test/browser/src lib_test/browser/src_ccov", - "ccov:test:browser": - "cross-env ONI_CCOV=1 electron-mocha --renderer --require browser/testHelpers.js -R browser/testCoverageReporter --recursive lib_test/browser/test", - "ccov:remap:browser:html": - "cd lib_test/browser/src && remap-istanbul --input ../../../coverage/coverage-final.json --output html-report --type html", - "ccov:remap:browser:lcov": - "cd lib_test/browser/src && remap-istanbul --input ../../../coverage/coverage-final.json --output lcov.info --type lcovonly", + "ccov:instrument": "nyc instrument --all true --sourceMap false lib_test/browser/src lib_test/browser/src_ccov", + "ccov:test:browser": "cross-env ONI_CCOV=1 electron-mocha --renderer --require browser/testHelpers.js -R browser/testCoverageReporter --recursive lib_test/browser/test", + "ccov:remap:browser:html": "cd lib_test/browser/src && remap-istanbul --input ../../../coverage/coverage-final.json --output html-report --type html", + "ccov:remap:browser:lcov": "cd lib_test/browser/src && remap-istanbul --input ../../../coverage/coverage-final.json --output lcov.info --type lcovonly", "ccov:clean": "rimraf coverage", "ccov:upload": "codecov", "launch": "electron lib/main/src/main.js", - "start": - "concurrently --kill-others \"npm run start-hot\" \"npm run watch:browser\" \"npm run watch:plugins\"", - "start-hot": - "cross-env ONI_WEBPACK_LOAD=1 NODE_ENV=development electron lib/main/src/main.js", + "start": "concurrently --kill-others \"yarn run start-hot\" \"yarn run watch:browser\" \"yarn run watch:plugins\"", + "start-hot": "cross-env ONI_WEBPACK_LOAD=1 NODE_ENV=development electron lib/main/src/main.js", "start-not-dev": "cross-env electron main.js", - "watch:browser": - "webpack-dev-server --config browser/webpack.development.config.js --host localhost --port 8191", - "watch:plugins": - "npm run watch:plugins:oni-plugin-typescript && npm run watch:plugins:oni-plugin-markdown-preview", + "watch:browser": "webpack-dev-server --config browser/webpack.development.config.js --host localhost --port 8191", + "watch:plugins": "yarn run watch:plugins:oni-plugin-typescript && yarn run watch:plugins:oni-plugin-markdown-preview", "watch:plugins:oni-plugin-typescript": "cd vim/core/oni-plugin-typescript && tsc --watch", - "watch:plugins:oni-plugin-markdown-preview": - "cd extensions/oni-plugin-markdown-preview && tsc --watch", - "uninstall-global": "npm rm -g oni-vim", - "install-global": "npm install -g oni-vim", - "install:plugins": - "npm run install:plugins:oni-plugin-markdown-preview && npm run install:plugins:oni-plugin-prettier", - "install:plugins:oni-plugin-markdown-preview": - "cd extensions/oni-plugin-markdown-preview && npm install --prod", - "install:plugins:oni-plugin-prettier": - "cd extensions/oni-plugin-prettier && npm install --prod", - "postinstall": "npm run install:plugins && electron-rebuild && opencollective postinstall", - "profile:webpack": - "webpack --config browser/webpack.production.config.js --profile --json > stats.json && webpack-bundle-analyzer browser/stats.json" + "watch:plugins:oni-plugin-markdown-preview": "cd extensions/oni-plugin-markdown-preview && tsc --watch", + "install:plugins": "yarn run install:plugins:oni-plugin-markdown-preview && yarn run install:plugins:oni-plugin-prettier", + "install:plugins:oni-plugin-markdown-preview": "cd extensions/oni-plugin-markdown-preview && yarn install --prod", + "install:plugins:oni-plugin-prettier": "cd extensions/oni-plugin-prettier && yarn install --prod", + "postinstall": "yarn run install:plugins && electron-rebuild && opencollective postinstall", + "profile:webpack": "webpack --config browser/webpack.production.config.js --profile --json > stats.json && webpack-bundle-analyzer browser/stats.json" }, "repository": { "type": "git", @@ -692,23 +854,24 @@ "electron-settings": "^3.1.4", "find-up": "2.1.0", "fs-extra": "^5.0.0", + "json5": "^1.0.1", "keyboard-layout": "^2.0.13", - "marked": "^0.3.6", + "marked": "^0.4.0", "minimist": "1.2.0", "msgpack-lite": "0.1.26", "ocaml-language-server": "^1.0.27", - "oni-api": "^0.0.42", - "oni-neovim-binaries": "0.1.1", + "oni-api": "^0.0.46", + "oni-neovim-binaries": "0.1.2", "oni-ripgrep": "0.0.4", - "oni-types": "0.0.4", - "react": "16.0.0", + "oni-types": "^0.0.8", + "react": "^16.3.2", "react-dnd": "^2.5.4", "react-dnd-html5-backend": "^2.5.4", - "react-dom": "16.0.0", + "react-dom": "^16.3.2", "redux-batched-subscribe": "^0.1.6", "shell-env": "^0.3.0", "shelljs": "0.7.7", - "styled-components": "^2.3.0", + "styled-components": "^3.2.6", "typescript": "^2.8.1", "vscode-css-languageserver-bin": "^1.2.1", "vscode-html-languageserver-bin": "^1.1.0", @@ -719,15 +882,15 @@ }, "devDependencies": { "@types/chokidar": "^1.7.5", - "@types/fs-extra": "^5.0.2", - "@types/classnames": "0.0.32", "@types/color": "2.0.0", "@types/detect-indent": "^5.0.0", "@types/dompurify": "^0.0.31", "@types/electron-settings": "^3.1.1", "@types/enzyme": "^3.1.8", + "@types/fs-extra": "^5.0.2", "@types/jest": "^22.1.3", "@types/jsdom": "11.0.0", + "@types/json5": "^0.0.29", "@types/lodash": "4.14.38", "@types/lolex": "2.1.0", "@types/marked": "^0.3.0", @@ -737,9 +900,10 @@ "@types/mocha": "2.2.33", "@types/msgpack-lite": "0.1.4", "@types/node": "8.0.53", + "@types/react": "^16.3.16", "@types/react-dnd": "^2.0.34", "@types/react-dnd-html5-backend": "^2.1.8", - "@types/react-dom": "16.0.3", + "@types/react-dom": "^16.0.5", "@types/react-motion": "0.0.23", "@types/react-redux": "5.0.12", "@types/react-test-renderer": "^16.0.0", @@ -757,18 +921,18 @@ "babel-minify-webpack-plugin": "^0.3.1", "babel-plugin-dynamic-import-node": "^1.2.0", "bs-platform": "2.1.0", - "classnames": "2.2.5", + "classnames": "JedWatson/classnames", "codecov": "^3.0.0", "color": "2.0.0", "concurrently": "3.1.0", "cross-env": "3.1.3", "css-loader": "0.28.4", "detect-indent": "^5.0.0", - "electron": "^1.8.4", + "electron": "2.0.2", "electron-builder": "^20.5.1", "electron-devtools-installer": "^2.2.3", "electron-mocha": "5.0.0", - "electron-rebuild": "1.6.0", + "electron-rebuild": "^1.7.3", "enzyme": "^3.3.0", "enzyme-adapter-react-16": "^1.1.1", "enzyme-to-json": "^3.3.1", @@ -792,11 +956,13 @@ "minimatch": "3.0.4", "mkdirp": "0.5.1", "mocha": "3.1.2", + "node-abi": "^2.4.1", "nyc": "^11.4.1", + "oni-core-logging": "^1.0.0", "oni-release-downloader": "^0.0.10", "opencollective": "1.0.3", - "prettier": "^1.10.2", - "pretty-quick": "^1.2.2", + "prettier": "^1.12.1", + "pretty-quick": "^1.5.0", "react-hot-loader": "^4.0.1", "react-motion": "0.5.2", "react-redux": "5.0.6", diff --git a/test/CiTests.ts b/test/CiTests.ts index 6ace1b9df0..e125f713bb 100644 --- a/test/CiTests.ts +++ b/test/CiTests.ts @@ -18,15 +18,19 @@ const CiTests = [ "AutoCompletionTest-HTML", "AutoCompletionTest-TypeScript", + "Browser.LocationTest", + "Configuration.JavaScriptEditorTest", "Configuration.TypeScriptEditor.NewConfigurationTest", "Configuration.TypeScriptEditor.CompletionTest", + "TabBarSneakTest", "initVimPromptNotificationTest", "Editor.BuffersCursorTest", "Editor.ExternalCommandLineTest", "Editor.BufferModifiedState", "Editor.OpenFile.PathWithSpacesTest", + "Editor.ScrollEventTest", "Editor.TabModifiedState", "Editor.CloseTabWithTabModesTabsTest", "MarkdownPreviewTest", @@ -48,6 +52,7 @@ const CiTests = [ "Regression.1296.SettingColorsTest", "Regression.1295.UnfocusedWindowTest", "Regression.1799.MacroApplicationTest", + "Regression.2047.VerifyCanvasIsIntegerSize", "TextmateHighlighting.DebugScopesTest", "TextmateHighlighting.ScopesOnEnterTest", @@ -85,7 +90,9 @@ const FGYELLOW = "\x1b[33m" describe("ci tests", function() { const tests = Platform.isWindows() ? [...CiTests, ...WindowsOnlyTests] - : Platform.isMac() ? [...CiTests, ...OSXOnlyTests] : CiTests + : Platform.isMac() + ? [...CiTests, ...OSXOnlyTests] + : CiTests const testFailures: IFailedTest[] = [] tests.forEach(test => { diff --git a/test/Demo.ts b/test/Demo.ts index 44d1535b67..2429a98190 100644 --- a/test/Demo.ts +++ b/test/Demo.ts @@ -10,5 +10,5 @@ const TestToRun = process.env["DEMO_TEST"] // tslint:disable-line console.log("Running test: " + TestToRun) describe("demo tests", () => { - runInProcTest(path.join(__dirname, "demo"), TestToRun) + runInProcTest(path.join(__dirname, "demo"), TestToRun, 50000) }) diff --git a/test/ci/Api.Buffer.AddLayer.tsx b/test/ci/Api.Buffer.AddLayer.tsx index 5e8541ac90..da342b3b6e 100644 --- a/test/ci/Api.Buffer.AddLayer.tsx +++ b/test/ci/Api.Buffer.AddLayer.tsx @@ -26,7 +26,7 @@ export class TestLayer implements Oni.BufferLayer { className += "inactive" } - return
    + return
    {context.visibleLines.join(os.EOL)}
    } } @@ -45,13 +45,18 @@ const getInactiveLayerElements = () => { export const test = async (oni: Oni.Plugin.Api) => { await oni.automation.waitForEditors() - await createNewFile("js", oni) + await createNewFile("js", oni, "line1\nline2") oni.editors.activeEditor.activeBuffer.addLayer(new TestLayer()) // Wait for layer to appear await oni.automation.waitFor(() => getLayerElements().length === 1) + // Validate the buffer layer has rendered the 'visibleLines' + const element = getLayerElements()[0] + assert.ok(element.textContent.indexOf("line1") >= 0, "Validate line1 is present in the layer") + assert.ok(element.textContent.indexOf("line2") >= 0, "Validate line2 is present in the layer") + // Validate elements assert.strictEqual(getActiveLayerElements().length, 1) assert.strictEqual(getInactiveLayerElements().length, 0) diff --git a/test/ci/Browser.LocationTest.ts b/test/ci/Browser.LocationTest.ts new file mode 100644 index 0000000000..410aa0bc3a --- /dev/null +++ b/test/ci/Browser.LocationTest.ts @@ -0,0 +1,64 @@ +/** + * Test scripts for Auto Complete for a Typescript file. + */ + +import * as assert from "assert" + +import * as Oni from "oni-api" + +import { WebviewTag } from "electron" + +import { getElementsBySelector } from "./Common" + +export const test = async (oni: Oni.Plugin.Api) => { + await oni.automation.waitForEditors() + + const getWebView = (): WebviewTag | null => { + const elems = getElementsBySelector("webview") + return elems.length > 0 ? elems[0] : null + } + + const waitForWebViewUrl = (urlPart: string): boolean => { + const webview = getWebView() + + if (!webview) { + return false + } + + const url = webview.getURL() + + return url.indexOf(urlPart) >= 0 + } + + oni.commands.executeCommand("browser.openUrl.verticalSplit", "https://github.com/onivim/oni") + + await oni.automation.waitFor(() => getWebView() !== null) + await oni.automation.waitFor(() => waitForWebViewUrl("github.com")) + + await oni.automation.sendKeys("") + await oni.automation.sleep(500) + + // We'll sneak to the browser address and load a new site + const anyOni = oni as any + const sneak = anyOni.sneak.getSneakMatchingTag("browser.address") + + const keys: string = sneak.triggerKeys.toLowerCase() + await anyOni.automation.sendKeysV2(keys) + + await oni.automation.sleep(500) + + await anyOni.automation.sendKeysV2("https://www.onivim.io") + + await oni.automation.sleep(500) + + await anyOni.automation.sendKeysV2("") + + await oni.automation.waitFor(() => waitForWebViewUrl("onivim.io")) + + assert.ok( + getWebView() + .getURL() + .indexOf("onivim.io") >= 0, + "Successfully navigated to onivim.io", + ) +} diff --git a/test/ci/Editor.ScrollEventTest.ts b/test/ci/Editor.ScrollEventTest.ts new file mode 100644 index 0000000000..30883dea1f --- /dev/null +++ b/test/ci/Editor.ScrollEventTest.ts @@ -0,0 +1,79 @@ +/** + * Test script to validate the modified status for tabs. + */ + +import * as assert from "assert" +import * as Oni from "oni-api" + +import { createNewFile, getElementByClassName } from "./Common" + +import * as os from "os" +const createLines = (num: number): string => { + const ret = [] + + for (let i = 0; i < num; i++) { + ret.push(i) + } + + return ret.join(os.EOL) +} + +const assertValue = (actual: number, expected: number, msg: string, oni: Oni.Plugin.Api) => { + const passed = actual === expected + + const notification = oni.notifications.createItem() + const title = passed ? "Assertion Passed" : "Assertion Failed" + notification.setContents(title, `${msg}\nActual: ${actual}\nExpected:${expected}`) + ;(notification as any).setLevel(passed ? "success" : "error") + notification.show() + + assert.strictEqual(actual, expected, msg) +} + +export const test = async (oni: Oni.Plugin.Api) => { + await oni.automation.waitForEditors() + + await createNewFile("js", oni, createLines(500)) + + let scrollEventHitCount = 0 + + oni.editors.activeEditor.onBufferScrolled.subscribe(() => { + scrollEventHitCount++ + }) + + await oni.automation.sendKeys("G") + + await oni.automation.waitFor(() => scrollEventHitCount === 1) + assertValue(scrollEventHitCount, 1, "A single scroll event should've been triggered by G", oni) + + await oni.automation.sendKeys("gg") + await oni.automation.waitFor(() => scrollEventHitCount === 2) + assertValue(scrollEventHitCount, 2, "Another scroll event should've been triggered by gg", oni) + + await oni.automation.sendKeys(":50") + await oni.automation.waitFor(() => scrollEventHitCount === 3) + assertValue( + scrollEventHitCount, + 3, + "Another scroll event should've been triggered by navigating to a line", + oni, + ) + + await oni.automation.sendKeys("") + await oni.automation.waitFor(() => scrollEventHitCount === 4) + assertValue( + scrollEventHitCount, + 4, + "Another scroll event should've been triggered by scrolling up one line", + oni, + ) + + await oni.automation.sendKeys("") + await oni.automation.waitFor(() => scrollEventHitCount === 5) + assertValue( + scrollEventHitCount, + 5, + "Another scroll event should've been triggered by scrolling down one line", + oni, + ) +} diff --git a/test/ci/Regression.2047.VerifyCanvasIsIntegerSize.ts b/test/ci/Regression.2047.VerifyCanvasIsIntegerSize.ts new file mode 100644 index 0000000000..4788ab6c00 --- /dev/null +++ b/test/ci/Regression.2047.VerifyCanvasIsIntegerSize.ts @@ -0,0 +1,33 @@ +/** + * Regression test for #2047 + */ + +import * as assert from "assert" + +import * as Oni from "oni-api" + +const isInteger = (num: number) => { + return Math.round(num) === num +} + +const assertCanvasIsIntegerSize = (canvasElement: HTMLElement) => { + const rect = canvasElement.getBoundingClientRect() + const measuredWidth = rect.width + const measuredHeight = rect.height + + assert.ok(isInteger(measuredWidth), "Validate the canvas's width is an integer value") + assert.ok(isInteger(measuredHeight), "Validate the canvas's height is an integer value") +} + +export const test = async (oni: Oni.Plugin.Api) => { + await oni.automation.waitForEditors() + + const allCanvasElements = document.getElementsByTagName("canvas") + + assert.ok(allCanvasElements.length > 0, "Verify there is at least one canvas element") + + // tslint:disable-next-line + for (let i = 0; i < allCanvasElements.length; i++) { + assertCanvasIsIntegerSize(allCanvasElements[i]) + } +} diff --git a/test/ci/TabBarSneakTest.ts b/test/ci/TabBarSneakTest.ts new file mode 100644 index 0000000000..b220231721 --- /dev/null +++ b/test/ci/TabBarSneakTest.ts @@ -0,0 +1,53 @@ +/** + * Tab Bar Sneak Test + * + * This test ensures that a user can trigger the sneak functionality and + * navigate to a different buffer + */ + +import * as assert from "assert" + +import * as Oni from "oni-api" + +import { + createNewFile, + getElementByClassName, + getElementsBySelector, + getSingleElementBySelector, + getTemporaryFilePath, +} from "./Common" + +export const test = async (oni: Oni.Plugin.Api) => { + await oni.automation.waitForEditors() + + // Next, open a split in the current tab, and check the tab still remains dirty + oni.automation.sendKeys(":") + oni.automation.sendKeys("e! buffer1") + oni.automation.sendKeys("") + await oni.automation.sleep(1500) + + oni.automation.sendKeys(":") + oni.automation.sendKeys("e! buffer2") + oni.automation.sendKeys("") + await oni.automation.sleep(1500) + + await oni.automation.sendKeys("") + await oni.automation.sleep(500) + + const anyOni = oni as any + const sneak = anyOni.sneak.getSneakMatchingTag("buffer1") + const keys: string = sneak.triggerKeys.toLowerCase() + await anyOni.automation.sendKeysV2(keys) + await oni.automation.sleep(2500) + + const path = oni.editors.activeEditor.activeBuffer.filePath + assert.ok(path.includes("buffer1")) +} + +export const settings = { + config: { + "tabs.mode": "buffers", + "oni.loadInitVim": false, + }, + allowLogFailures: true, +} diff --git a/test/common/runInProcTest.ts b/test/common/runInProcTest.ts index 851c1f8b5d..5cf235f2da 100644 --- a/test/common/runInProcTest.ts +++ b/test/common/runInProcTest.ts @@ -184,11 +184,15 @@ export const runInProcTest = ( writeLogs(rendererLogs) console.log("--- " + testName + " ---") - const mainProcessLogs: any[] = await oni.client.getMainProcessLogs() console.log("---LOGS (Main): " + testName) - mainProcessLogs.forEach(l => { - console.log(l) - }) + if (!result.passed) { + const mainProcessLogs: any[] = await oni.client.getMainProcessLogs() + mainProcessLogs.forEach(l => { + console.log(l) + }) + } else { + console.log("Skipping log output since test passed.") + } console.log("--- " + testName + " ---") console.log("") diff --git a/test/demo/HeroDemo.ts b/test/demo/HeroDemo.ts index 6c7b45d4d2..67198a1b14 100644 --- a/test/demo/HeroDemo.ts +++ b/test/demo/HeroDemo.ts @@ -3,23 +3,82 @@ */ import * as assert from "assert" +import { execSync } from "child_process" +import * as fs from "fs" import * as os from "os" import * as path from "path" +import * as shell from "shelljs" + +import * as rimraf from "rimraf" import { getCompletionElement, getTemporaryFolder } from "./../ci/Common" -import { getDistPath } from "./DemoCommon" +import { getDistPath, getRootPath } from "./DemoCommon" import { remote } from "electron" +const BASEDELAY = 18 +const RANDOMDELAY = 8 + const EmptyConfigPath = path.join(getTemporaryFolder(), "config.js") -const BASEDELAY = 25 -const RANDOMDELAY = 15 +const getProjectRootPath = () => { + const root = getRoot(__dirname) + return os.platform() === "win32" ? root : os.homedir() +} + +const ReactProjectName = "oni-react-app" + +const getRoot = (dir: string): string => { + const parent = path.dirname(dir) + if (parent === dir) { + return parent + } else { + return getRoot(parent) + } +} + +const createReactAppProject = oni => { + const oniReactApp = path.join(getProjectRootPath(), ReactProjectName) + + // rimraf.sync(oniReactApp) + // const output = execSync('create-react-app "' + oniReactApp + '"') + + const oniLogoPath = path.join(getRootPath(), "images", "256x256.png") + const oniLogoDestinationPath = path.join(oniReactApp, "src", "oni.png") + + const oniLogoComponentPath = path.join(oniReactApp, "src", "OniLogo.js") + + fs.writeFileSync( + oniLogoComponentPath, + ` +import React, { Component } from 'react'; +import logo from './oni.png'; + +export class OniLogo extends Component { + render() { + return logo; + } +} + `, + "utf8", + ) + + // Delete the 'App.test.js' so it doesn't mess up fuzzy find results + rimraf.sync(path.join(oniReactApp, "src", "App.test.js")) + + shell.cp(oniLogoPath, oniLogoDestinationPath) + shell.cp(path.join(oniReactApp, "src", "Old.js"), path.join(oniReactApp, "src", "App.js")) + return oniReactApp +} export const test = async (oni: any) => { + const reactAppPath = createReactAppProject(oni) + await oni.automation.waitForEditors() + oni.workspace.changeDirectory(reactAppPath) + const isMac = process.platform === "darwin" const shortDelay = async () => oni.automation.sleep(BASEDELAY * 25) @@ -78,10 +137,221 @@ export const test = async (oni: any) => { await shortDelay() } + const splitHorizontal = async (fileName: string) => { + await shortDelay() + oni.automation.sendKeysV2("") + oni.automation.sendKeysV2("") + await shortDelay() + await simulateTyping(":tabnew VIM.md") + } + const waitForCompletion = async () => { return oni.automation.waitFor(() => !!getCompletionElement()) } + const showWelcomeAchievement = async () => { + oni.achievements.clearAchievements() + + // Create our own 'mock' achievement, because + // the welcome one won't be tracked if it has been completed + oni.achievements.registerAchievement({ + uniqueId: "oni.achievement.automation", + name: "Welcome to Oni!", + description: "Launch Oni for the first time", + goals: [ + { + name: null, + goalId: "oni.automation.goal", + count: 1, + }, + ], + }) + oni.achievements.notifyGoal("oni.automation.goal") + + await longDelay() + await longDelay() + } + + const intro = async () => { + await simulateTyping(":tabnew Hello.md") + await pressEnter() + + await simulateTyping( + "iOni is a new kind of editor: combining the best of Vim, Atom, and VSCode.", + ) + await pressEnter() + await simulateTyping( + "Built with web tech, featuring a high performance canvas renderer, with (neo)vim handling the heavy lifting.", + ) + await pressEnter() + await simulateTyping("Available for Windows, OSX, and Linux.") + + await pressEscape() + } + + const navigateToSneakWithTag = async (tag: string) => { + oni.automation.sendKeysV2("") + await shortDelay() + + const targetSneak = oni.sneak.getSneakMatchingTag(tag) + const triggerKeys = targetSneak.triggerKeys as string + await simulateTyping(triggerKeys) + await shortDelay() + } + + const showKeyboardNavigation = async () => { + await splitHorizontal("VIM.md") + await pressEnter() + + await simulateTyping("i") + await simulateTyping("Use your Vim muscle memory to be productive without a mouse...") + + await pressEscape() + + oni.automation.sendKeysV2("") + oni.automation.sendKeysV2("") + await shortDelay() + + oni.automation.sendKeysV2("G") + await longDelay() + oni.automation.sendKeysV2("gg") + await longDelay() + + oni.automation.sendKeysV2("") + oni.automation.sendKeysV2("") + await shortDelay() + + oni.automation.sendKeysV2("") + oni.automation.sendKeysV2("") + await shortDelay() + + oni.automation.sendKeysV2("") + oni.automation.sendKeysV2("") + await shortDelay() + + oni.automation.sendKeysV2("") + oni.automation.sendKeysV2("") + await shortDelay() + + await simulateTyping("o") + await simulateTyping("..but enjoy the conveniences of a modern UI editor.") + await pressEscape() + + await shortDelay() + + await navigateToSneakWithTag("oni.sidebar.search") + + await navigateToSneakWithTag("oni.sidebar.learning") + + await navigateToSneakWithTag("oni.sidebar.explorer") + + oni.automation.sendKeysV2("") + + await simulateTyping(":qa!") + oni.automation.sendKeysV2("") + + await shortDelay() + + oni.automation.sendKeysV2("") + } + + const showDevelopment = async () => { + await pressEscape() + + await openCommandPalette() + await simulateTyping("brovsp") + await pressEnter() + + await pressEscape() + await openCommandPalette() + await simulateTyping("termhzsp") + await pressEnter() + + await longDelay() + + await simulateTyping("A") + await simulateTyping("npm run start") + await pressEnter() + + await pressEscape() + + oni.automation.sendKeysV2("") + oni.automation.sendKeysV2("") + await shortDelay() + + await openQuickOpen() + await simulateTyping("Appjs") + await pressEnter() + + await navigateToSneakWithTag("browser.address") + + await shortDelay() + + await simulateTyping("http://localhost:3000") + await pressEnter() + + // The popped up browser can steal focus, causing our bindings to fail.. + require("electron") + .remote.getCurrentWindow() + .focus() + + await simulateTyping("10j") + await shortDelay() + await simulateTyping("cit") + await shortDelay() + await simulateTyping("Welcome to Oni") + await pressEscape() + await simulateTyping(":w") + await pressEnter() + await shortDelay() + + await simulateTyping("7k") + await simulateTyping("O") + await simulateTyping("impsnip") + + await waitForCompletion() + + await pressEnter() + + await shortDelay() + await simulateTyping("./Oni") + await waitForCompletion() + await pressEnter() + + await pressTab() + await simulateTyping("Oni") + await waitForCompletion() + await pressEnter() + await pressTab() + + await simulateTyping("7j") + await simulateTyping("b") + await simulateTyping("C") + await simulateTyping("OniLogo />") + + await pressEscape() + await simulateTyping(":w") + await pressEnter() + await longDelay() + await longDelay() + + oni.automation.sendKeysV2("") + oni.automation.sendKeysV2("") + await shortDelay() + + oni.automation.sendKeysV2("") + oni.automation.sendKeysV2("") + await shortDelay() + + await simulateTyping(":q") + await pressEnter() + await shortDelay() + + await simulateTyping(":q") + await pressEnter() + await shortDelay() + } + const showConfig = async () => { await pressEscape() await openCommandPalette() @@ -105,24 +375,24 @@ export const test = async (oni: any) => { await longDelay() await simulateTyping("ciw") await longDelay() - await simulateTyping("15px") + await simulateTyping("16px") await pressEscape() await simulateTyping(":w") await pressEscape() // HACK - Configuration doesn't use the same file, so we need to set this directly here - oni.configuration.setValues({ "editor.fontSize": "15px" }) + oni.configuration.setValues({ "editor.fontSize": "16px" }) await longDelay() await simulateTyping("b") await longDelay() await simulateTyping("ciw") await longDelay() - await simulateTyping("12px") + await simulateTyping("14px") await pressEscape() await simulateTyping(":w") await pressEnter() - oni.configuration.setValues({ "editor.fontSize": "12px" }) + oni.configuration.setValues({ "editor.fontSize": "14px" }) await longDelay() await pressEscape() @@ -172,6 +442,12 @@ export const test = async (oni: any) => { // item.show() await longDelay() + + await simulateTyping(":q") + await pressEnter() + + await simulateTyping(":q") + await pressEnter() } const showComingSoon = async () => { @@ -184,11 +460,15 @@ export const test = async (oni: any) => { await simulateTyping("This is just the beginning! Lots more to come:") await pressEnter() - await simulateTyping("Live Preview") + await simulateTyping("* Live Preview") + await pressEnter() + await simulateTyping("* Plugin Management") await pressEnter() - await simulateTyping("Integrated Browser") + await simulateTyping("* More tutorials ") await pressEnter() - await simulateTyping("Interactive Tutorial") + await simulateTyping("* Debuggers") + await pressEnter() + await simulateTyping("* Version Control Integration") await longDelay() await pressEnter() await pressEnter() @@ -250,11 +530,44 @@ export const test = async (oni: any) => { await pressEscape() } + const showTutorials = async () => { + await oni.editors.activeEditor.neovim.command(":tabnew TUTORIAL") + + await simulateTyping("i") + await simulateTyping( + "If you're new to modal editing, Oni comes with interactive tutorials to get you up to speed!", + ) + await pressEscape() + + await navigateToSneakWithTag("oni.sidebar.learning") + + const firstTutorialId = oni.tutorials.getNextTutorialId() + await oni.tutorials.startTutorial(firstTutorialId) + + await shortDelay() + await pressEscape() + + await simulateTyping("i") + await shortDelay() + await simulateTyping("hello") + await shortDelay() + await pressEscape() + await shortDelay() + await simulateTyping("o") + await shortDelay() + await simulateTyping("world") + await shortDelay() + await pressEscape() + + await longDelay() + await oni.editors.activeEditor.neovim.command(":q!") + } + // Prime the typescript language service prior to recording await simulateTyping(":tabnew") await pressEnter() await openQuickOpen() - await simulateTyping("NeovimInstance.ts") + await simulateTyping("App.js") await pressEnter() await simulateTyping("owindow.") @@ -264,127 +577,62 @@ export const test = async (oni: any) => { await simulateTyping(":q!") await pressEnter() - // Set window size - remote.getCurrentWindow().setSize(1280, 720) + oni.configuration.setValues({ + "ui.fontSize": "14px", + "editor.fontSize": "14px", + }) - // Disable notifications, since there is sometimes noise... (HACK) - oni.notifications.disable() + // Set window size + remote.getCurrentWindow().setSize(1920, 1080) oni.recorder.startRecording() - oni.commands.executeCommand("keyDisplayer.show") - oni.configuration.setValues({ "keyDisplayer.showInInsertMode": false }) - - await simulateTyping(":tabnew Hello.md") - await pressEnter() - - await simulateTyping("iHello and welcome to Oni!") - await pressEnter() - - await simulateTyping( - "Oni is a new kind of editor: combining the best of Vim, Atom, and VSCode.", - ) - await pressEnter() - await simulateTyping( - "Built with web tech, featuring a high performance canvas renderer, with (neo)vim handling the heavy lifting.", - ) - await pressEnter() - await simulateTyping("Available for Windows, OSX, and Linux.") - await pressEnter() - - await pressEscape() - await simulateTyping(":sp VIM.md") - await pressEnter() - - await simulateTyping("i") - await simulateTyping("Use your Vim muscle memory to be productive without a mouse...") - - await pressEscape() - - oni.automation.sendKeysV2("") - oni.automation.sendKeysV2("") - await shortDelay() - - oni.automation.sendKeysV2("G") - await longDelay() - oni.automation.sendKeysV2("gg") - await longDelay() - - oni.automation.sendKeysV2("") - oni.automation.sendKeysV2("") - await shortDelay() + await showWelcomeAchievement() - oni.automation.sendKeysV2("") - oni.automation.sendKeysV2("") - await shortDelay() + oni.tutorials.clearProgress() - oni.automation.sendKeysV2("") - oni.automation.sendKeysV2("") - await shortDelay() - - await simulateTyping("o") - await simulateTyping("..but enjoy the conveniences of a modern UI editor.") - await pressEscape() - - await shortDelay() - oni.automation.sendKeysV2("") - await shortDelay() - await simulateTyping("a") - await shortDelay() - await simulateTyping("c") + oni.commands.executeCommand("keyDisplayer.show") + oni.configuration.setValues({ + "keyDisplayer.showInInsertMode": false, + "editor.split.mode": "oni", + "browser.defaultUrl": "https://github.com/onivim/oni", + }) - oni.automation.sendKeysV2("") - await shortDelay() - await simulateTyping("a") - await shortDelay() - await simulateTyping("b") - await shortDelay() + await intro() - oni.automation.sendKeysV2("") + await showKeyboardNavigation() - await simulateTyping(":close") - oni.automation.sendKeysV2("") + await showDevelopment() // --- await showLanguageServices() // --- - await simulateTyping("gT") - - await simulateTyping("o") - await simulateTyping("Enjoy built in search with ripgrep..") - - await pressEscape() - - await openFindInFiles() - await simulateTyping("OniEditor") - await longDelay() - oni.automation.sendKeysV2("") - await longDelay() - - oni.automation.sendKeysV2("") - oni.automation.sendKeysV2("") - - await simulateTyping("o") - await simulateTyping("...or the embedded file finder.") - await shortDelay() - - await pressEscape() - await shortDelay() - await openQuickOpen() - await simulateTyping("NeovimEditor") - await shortDelay() - oni.automation.sendKeysV2("") - await longDelay() - oni.automation.sendKeysV2("") - await shortDelay() - - await simulateTyping("G") - await simulateTyping("o") - await simulateTyping("...use the built in command palette to discover functionality.") - await pressEscape() + // oni.automation.sendKeysV2("") + // oni.automation.sendKeysV2("") + + // await simulateTyping("o") + // await simulateTyping("...or the embedded file finder.") + // await shortDelay() + + // await pressEscape() + // await shortDelay() + // await openQuickOpen() + // await simulateTyping("NeovimEditor") + // await shortDelay() + // oni.automation.sendKeysV2("") + // await longDelay() + // oni.automation.sendKeysV2("") + // await shortDelay() + + // await simulateTyping("G") + // await simulateTyping("o") + // await simulateTyping("...use the built in command palette to discover functionality.") + // await pressEscape() + await showTutorials() await showConfig() + await showComingSoon() await simulateTyping(":q") diff --git a/ui-tests/ErrorInfo.test.tsx b/ui-tests/ErrorInfo.test.tsx new file mode 100644 index 0000000000..3aff74cb0a --- /dev/null +++ b/ui-tests/ErrorInfo.test.tsx @@ -0,0 +1,60 @@ +import * as React from "react" +import { shallow } from "enzyme" +import { Diagnostic } from "vscode-languageserver-types" + +import { ErrorInfo, DiagnosticMessage } from "../browser/src/UI/components/ErrorInfo" + +describe("", () => { + const errors: Diagnostic[] = [ + { + range: { + start: { + line: 0, + character: 0, + }, + end: { + line: 0, + character: 10, + }, + }, + severity: 2, + code: 5, + source: "test", + message: "an error here", + }, + ] + + it("Should Render without crashing", () => { + const wrapper = shallow() + expect(wrapper.length).toBe(1) + }) + it("Should Not Render without Errors", () => { + const wrapper = shallow() + expect(wrapper.children().length).toBe(0) + }) + it("Should render the correct diagnostic message", () => { + const wrapper = shallow() + expect(wrapper.dive().find("[data-id='diagnostic-message']").length).toEqual(1) + expect( + wrapper + .dive() + .find("[data-id='diagnostic-message']") + .dive() + .text(), + ).toBe(errors[0].message) + }) + + it("Should render the correct number of diagnostics", () => { + const wrapper = shallow( + , + ) + expect(wrapper.dive().find("[data-id='diagnostic-message']").length).toBe(3) + }) + + it("Should match the last snapshot on record unless a purposeful change was made", () => { + const wrapper = shallow( + , + ) + expect(wrapper).toMatchSnapshot() + }) +}) diff --git a/ui-tests/SidebarStore.test.ts b/ui-tests/SidebarStore.test.ts new file mode 100644 index 0000000000..989e41f0cc --- /dev/null +++ b/ui-tests/SidebarStore.test.ts @@ -0,0 +1,50 @@ +import { + decreaseWidth, + increaseWidth, + sidebarReducer, +} from "./../browser/src/Services/Sidebar/SidebarStore" + +describe("Change size function", () => { + it("Should correctly return an increased size", () => { + const newSize = increaseWidth("12em", null) + expect(newSize).toBe("13em") + }) + + it("Should correctly return an decreased size", () => { + const newSize = decreaseWidth("12em", null) + expect(newSize).toBe("11em") + }) + + it("Should return the original size if passed an invalid value", () => { + const newSize = increaseWidth("appleem", "15em") + expect(newSize).toBe("15em") + }) + it("Should return the last value if the user input is too big", () => { + const newSize = increaseWidth("50em", "15em") + expect(newSize).toBe("50em") + }) + + it("Should return the last value if the user input is too small", () => { + const newSize = decreaseWidth("1em", "15em") + expect(newSize).toBe("1em") + }) + + it("Should decrease a value if at top limit", () => { + const newSize = decreaseWidth("50em", null) + expect(newSize).toBe("49em") + }) + + it("Should increase a value if at bottom limit", () => { + const newSize = increaseWidth("1em", null) + expect(newSize).toBe("2em") + }) + + it("Should use a default unit if the user passes an invalid unit value", () => { + const newSize = decreaseWidth("15apples", "12em") + expect(newSize).toBe("14em") + }) + it("Should use default unit if the value passed in does not have any units", () => { + const newSize = increaseWidth("15", "11em") + expect(newSize).toBe("16em") + }) +}) diff --git a/ui-tests/Tabs.test.tsx b/ui-tests/Tabs.test.tsx index 06740afb43..ae55c6f6ea 100644 --- a/ui-tests/Tabs.test.tsx +++ b/ui-tests/Tabs.test.tsx @@ -2,21 +2,62 @@ import { mount, shallow } from "enzyme" import { shallowToJson } from "enzyme-to-json" import * as React from "react" +jest.mock("../browser/src/Services/FileIcon") +import { FileIcon } from "../browser/src/Services/FileIcon" +jest.mock("../browser/src/UI/components/Sneakable") +import { Sneakable } from "../browser/src/UI/components/Sneakable" + import { Tab, Tabs } from "../browser/src/UI/components/Tabs" +const MockFileIcon = FileIcon as jest.Mock<{}> +const MockSneakable = Sneakable as jest.Mock + describe(" Tests", () => { - const testTabs = [ - { - id: 2, - name: "test", - description: "a test tab", - isSelected: true, - isDirty: true, - iconFileName: "icon", - highlightColor: "#000", - }, - ] - const TestTabs = ( + MockFileIcon.mockImplementation(() => ({ render: jest.fn() })) + MockSneakable.mockImplementation(props => ({ + render: () => props.children, + })) + + const tabCloseFunction = jest.fn() + const tabSelectFunction = jest.fn() + + const tab1 = { + id: 1, + name: "test", + description: "a test tab", + isSelected: true, + isDirty: true, + iconFileName: "icon", + highlightColor: "#000", + } + + const tab2 = { + id: 2, + name: "test", + description: "a test tab", + isSelected: false, + isDirty: true, + iconFileName: "icon", + highlightColor: "#000", + } + + const TabsContainingSingleTab = ( + + ) + + const TabsContainingTwoTabs = ( Tests", () => { foregroundColor="#000" shouldWrap={false} visible={true} - tabs={testTabs} + onTabClose={tabCloseFunction} + onTabSelect={tabSelectFunction} + tabs={[tab1, tab2]} /> ) + + const TabsNotVisible = ( + + ) + + afterEach(() => { + tabCloseFunction.mockReset() + tabSelectFunction.mockReset() + }) + it("renders without crashing", () => { - const wrapper = shallow(TestTabs) + const wrapper = shallow(TabsContainingSingleTab) expect(wrapper.length).toEqual(1) }) + it("should match last known snapshot unless we make a change", () => { - const wrapper = shallow(TestTabs) + const wrapper = shallow(TabsContainingSingleTab) expect(shallowToJson(wrapper)).toMatchSnapshot() }) - it("Should render the correct number of tabs", () => { - const wrapper = shallow(TestTabs) - expect(wrapper.children.length).toEqual(1) - }) - it("Should not render if the visible prop is false", () => { - const wrapper = shallow( - , - ) + + it("should render the correct number of tabs", () => { + expect(shallow(TabsContainingSingleTab).children().length).toEqual(1) + expect(shallow(TabsContainingTwoTabs).children().length).toEqual(2) + }) + + it("should not render if the visible prop is false", () => { + const wrapper = shallow(TabsNotVisible) expect(wrapper.getElement()).toBe(null) }) + + it("should call onTabClose callback on tab close button click", () => { + const wrapper = mount(TabsContainingSingleTab) + const clickedTab = wrapper.find(Tab) + + wrapper + .find(".corner") + .last() + .simulate("click") + + expect(tabCloseFunction).toHaveBeenCalledWith(clickedTab.props().id) + + wrapper.unmount() + }) + + it("should call onTabSelect callback on tab title click", () => { + const wrapper = mount(TabsContainingTwoTabs) + const clickedTab = wrapper.find(Tab).last() + + clickedTab.find(".name").simulate("mouseDown", { button: 0 }) + expect(tabSelectFunction).toHaveBeenCalledWith(clickedTab.props().id) + + wrapper.unmount() + }) + + it("should call onTabClose callback on tab title middle click", () => { + const wrapper = mount(TabsContainingTwoTabs) + const clickedTab = wrapper.find(Tab).first() + + clickedTab.find(".name").simulate("mouseDown", { button: 1 }) + expect(tabCloseFunction).toHaveBeenCalledWith(clickedTab.props().id) + + wrapper.unmount() + }) + + it("should call onTabSelect callback on tab file icon click", () => { + const wrapper = mount(TabsContainingTwoTabs) + const clickedTab = wrapper.find(Tab).last() + + clickedTab + .find(".corner") + .first() + .simulate("mouseDown", { button: 0 }) + expect(tabSelectFunction).toHaveBeenCalledWith(clickedTab.props().id) + + wrapper.unmount() + }) + + it("should call onTabClose callback on tab file icon middle click", () => { + const wrapper = mount(TabsContainingTwoTabs) + const clickedTab = wrapper.find(Tab).first() + + clickedTab + .find(".corner") + .first() + .simulate("mouseDown", { button: 1 }) + expect(tabCloseFunction).toHaveBeenCalledWith(clickedTab.props().id) + + wrapper.unmount() + }) + + it("should pass tab name as Sneakable tag property", () => { + const wrapper = mount(TabsContainingSingleTab) + const tab = wrapper.find(Tab) + const sneakable = wrapper.find(Sneakable) + + expect(sneakable.props().tag).toEqual(tab.props().name) + + wrapper.unmount() + }) + + it("should call onTabSelect callback as Sneakable callback", () => { + MockSneakable.mockImplementationOnce(props => { + props.callback() + }) + + const wrapper = mount(TabsContainingSingleTab) + const sneakableTab = wrapper.find(Tab) + + expect(tabSelectFunction).toHaveBeenCalledWith(sneakableTab.props().id) + + wrapper.unmount() + }) + + it("should pass tab icon file name as FileIcon fileName property ", () => { + const wrapper = mount(TabsContainingSingleTab) + const tab = wrapper.find(Tab) + const fileIcon = wrapper.find(FileIcon) + + expect(fileIcon.props().fileName).toEqual(tab.props().iconFileName) + + wrapper.unmount() + }) }) diff --git a/ui-tests/Text.test.tsx b/ui-tests/Text.test.tsx new file mode 100644 index 0000000000..6bfbcbe11d --- /dev/null +++ b/ui-tests/Text.test.tsx @@ -0,0 +1,15 @@ +import * as React from "react" +import { shallow } from "enzyme" + +import { SelectedText, Text } from "./../browser/src/UI/components/Text" + +describe("", () => { + it("Text component should match the last snapshot on record", () => { + const wrapper = shallow() + expect(wrapper).toMatchSnapshot() + }) + it("SelectedText component should match the last snapshot on record", () => { + const wrapper = shallow() + expect(wrapper).toMatchSnapshot() + }) +}) diff --git a/ui-tests/WindowTitleView.test.tsx b/ui-tests/WindowTitleView.test.tsx new file mode 100644 index 0000000000..56e2f09800 --- /dev/null +++ b/ui-tests/WindowTitleView.test.tsx @@ -0,0 +1,15 @@ +import * as React from "react" +import { shallow } from "enzyme" + +import { WindowTitleView } from "./../browser/src/UI/components/WindowTitle" + +describe("", () => { + it("Text component should match the last snapshot on record", () => { + const wrapper = shallow() + expect(wrapper).toMatchSnapshot() + }) + it("should only render if visible", () => { + const wrapper = shallow() + expect(wrapper.children().length).toBe(0) + }) +}) diff --git a/ui-tests/__snapshots__/ErrorInfo.test.tsx.snap b/ui-tests/__snapshots__/ErrorInfo.test.tsx.snap new file mode 100644 index 0000000000..59ec99e0af --- /dev/null +++ b/ui-tests/__snapshots__/ErrorInfo.test.tsx.snap @@ -0,0 +1,44 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` Should match the last snapshot on record unless a purposeful change was made 1`] = ` + + + + + an error here + + + + + + an error here + + + + + + an error here + + + +`; diff --git a/ui-tests/__snapshots__/Tabs.test.tsx.snap b/ui-tests/__snapshots__/Tabs.test.tsx.snap index ae38c754de..6275ee24b6 100644 --- a/ui-tests/__snapshots__/Tabs.test.tsx.snap +++ b/ui-tests/__snapshots__/Tabs.test.tsx.snap @@ -18,14 +18,14 @@ exports[` Tests should match last known snapshot unless we make a change height="2em" highlightColor="#000" iconFileName="icon" - id={2} + id={1} isDirty={true} isSelected={true} - key="2" + key="1" maxWidth="20em" name="test" - onClickClose={[Function]} - onClickName={[Function]} + onClose={[MockFunction]} + onSelect={[MockFunction]} />
    `; diff --git a/ui-tests/__snapshots__/Text.test.tsx.snap b/ui-tests/__snapshots__/Text.test.tsx.snap new file mode 100644 index 0000000000..e312b41967 --- /dev/null +++ b/ui-tests/__snapshots__/Text.test.tsx.snap @@ -0,0 +1,13 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` SelectedText component should match the last snapshot on record 1`] = ` + + Test + +`; + +exports[` Text component should match the last snapshot on record 1`] = ` + + Test + +`; diff --git a/ui-tests/__snapshots__/WindowTitleView.test.tsx.snap b/ui-tests/__snapshots__/WindowTitleView.test.tsx.snap new file mode 100644 index 0000000000..fc1bc1038e --- /dev/null +++ b/ui-tests/__snapshots__/WindowTitleView.test.tsx.snap @@ -0,0 +1,10 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` Text component should match the last snapshot on record 1`] = ` + + Test Window + +`; diff --git a/ui-tests/mocks/Configuration.ts b/ui-tests/mocks/Configuration.ts index d8120d594a..765d2a5603 100644 --- a/ui-tests/mocks/Configuration.ts +++ b/ui-tests/mocks/Configuration.ts @@ -5,5 +5,5 @@ const Configuration = jest.fn().mockImplementation(() => { } }) -export const configuration = Configuration +export const configuration = new Configuration() export default Configuration diff --git a/ui-tests/mocks/classnames.ts b/ui-tests/mocks/classnames.ts deleted file mode 100644 index 33abc85533..0000000000 --- a/ui-tests/mocks/classnames.ts +++ /dev/null @@ -1,2 +0,0 @@ -export default (component: string, classes: { [key: string]: string }) => jest.fn() -export const classNames = jest.fn() diff --git a/ui-tests/tsconfig.react.json b/ui-tests/tsconfig.react.json index 3621a32087..1364905d4e 100644 --- a/ui-tests/tsconfig.react.json +++ b/ui-tests/tsconfig.react.json @@ -22,6 +22,12 @@ "sourceMap": true, "types": ["jest", "electron", "react", "webgl2"] }, - "include": ["**/*.tsx", "**/*.ts", "../browser/**/*.ts", "../browser/**/*.tsx"], + "include": [ + "**/*.tsx", + "**/*.ts", + "../@types/**/*.d.ts", + "../browser/**/*.ts", + "../browser/**/*.tsx" + ], "exclude": ["node_modules"] } diff --git a/vim/core/oni-plugin-buffers/index.js b/vim/core/oni-plugin-buffers/index.js index f60648296e..3b26cd5674 100644 --- a/vim/core/oni-plugin-buffers/index.js +++ b/vim/core/oni-plugin-buffers/index.js @@ -16,7 +16,9 @@ const activate = Oni => { const buffers = Oni.editors.activeEditor.getBuffers() const active = Oni.editors.activeEditor.activeBuffer.filePath - const bufferMenuItems = buffers.map(b => ({ + const validBuffers = buffers.filter(b => !b.filepath) + + const bufferMenuItems = validBuffers.map(b => ({ label: `${active === b.filePath ? b.id + " %" : b.id}`, detail: truncateFilePath(b.filePath), icon: Oni.ui.getIconClassForFile(b.filePath), diff --git a/webview_preload/src/index.ts b/webview_preload/src/index.ts index 8285f29072..5c0ef78998 100644 --- a/webview_preload/src/index.ts +++ b/webview_preload/src/index.ts @@ -5,9 +5,21 @@ * https://electronjs.org/docs/api/webview-tag#preload */ +declare var require: any ;(() => { const __oni_win: any = window + const { ipcRenderer } = require("electron") + + window.document.addEventListener("focusin", evt => { + const target = evt.target as HTMLElement + ipcRenderer.sendToHost("focusin", target ? target.tagName : null) + }) + + window.document.addEventListener("focusout", evt => { + ipcRenderer.sendToHost("focusout") + }) + interface Rectangle { x: number y: number diff --git a/yarn.lock b/yarn.lock index 99590d45cc..15d7705b8f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -55,10 +55,6 @@ "@types/events" "*" "@types/node" "*" -"@types/classnames@0.0.32": - version "0.0.32" - resolved "https://registry.yarnpkg.com/@types/classnames/-/classnames-0.0.32.tgz#449abcd9a826807811ef101e58df9f83cfc61713" - "@types/color-convert@*": version "1.9.0" resolved "https://registry.yarnpkg.com/@types/color-convert/-/color-convert-1.9.0.tgz#bfa8203e41e7c65471e9841d7e306a7cd8b5172d" @@ -126,6 +122,10 @@ "@types/tough-cookie" "*" parse5 "^3.0.2" +"@types/json5@^0.0.29": + version "0.0.29" + resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" + "@types/lodash@4.14.38": version "4.14.38" resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.38.tgz#fcbd51e740c5e260652cad975dd26c5c4151cb11" @@ -184,9 +184,9 @@ dependencies: "@types/react" "*" -"@types/react-dom@16.0.3": - version "16.0.3" - resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-16.0.3.tgz#8accad7eabdab4cca3e1a56f5ccb57de2da0ff64" +"@types/react-dom@^16.0.5": + version "16.0.5" + resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-16.0.5.tgz#a757457662e3819409229e8f86795ff37b371f96" dependencies: "@types/node" "*" "@types/react" "*" @@ -227,6 +227,12 @@ version "16.0.25" resolved "https://registry.yarnpkg.com/@types/react/-/react-16.0.25.tgz#bf696b83fe480c5e0eff4335ee39ebc95884a1ed" +"@types/react@^16.3.16": + version "16.3.16" + resolved "https://registry.yarnpkg.com/@types/react/-/react-16.3.16.tgz#78fc44a90b45701f50c8a7008f733680ba51fc86" + dependencies: + csstype "^2.2.0" + "@types/redux-batched-subscribe@^0.1.2": version "0.1.2" resolved "https://registry.yarnpkg.com/@types/redux-batched-subscribe/-/redux-batched-subscribe-0.1.2.tgz#a5f0e510b07b6e8f10fe781bd830fae4f9bcf648" @@ -2239,9 +2245,9 @@ class-utils@^0.3.5: isobject "^3.0.0" static-extend "^0.1.1" -classnames@2.2.5, classnames@^2.2.3, classnames@^2.2.5: +classnames@JedWatson/classnames, classnames@^2.2.3, classnames@^2.2.5: version "2.2.5" - resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.5.tgz#fb3801d453467649ef3603c7d61a02bd129bde6d" + resolved "https://codeload.github.com/JedWatson/classnames/tar.gz/34a05a53d31d35879ec7088949ae5a1b2224043a" clean-css@4.1.x: version "4.1.11" @@ -2269,9 +2275,9 @@ cli-spinners@^0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-0.1.2.tgz#bb764d88e185fb9e1e6a2a1f19772318f605e31c" -cli-spinners@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-1.1.0.tgz#f1847b168844d917a671eb9d147e3df497c90d06" +cli-spinners@^1.0.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-1.3.1.tgz#002c1990912d0d59580c93bd36c056de99e4259a" cli-table@^0.3.1: version "0.3.1" @@ -2938,6 +2944,10 @@ cssom@0.3.x, "cssom@>= 0.3.2 < 0.4.0": dependencies: cssom "0.3.x" +csstype@^2.2.0: + version "2.5.3" + resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.5.3.tgz#2504152e6e1cc59b32098b7f5d6a63f16294c1f7" + currently-unhandled@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/currently-unhandled/-/currently-unhandled-0.4.1.tgz#988df33feab191ef799a61369dd76c17adf957ea" @@ -3128,7 +3138,7 @@ detect-indent@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-5.0.0.tgz#3871cc0a6a002e8c3e5b3cf7f336264675f06b9d" -detect-libc@^1.0.2: +detect-libc@^1.0.2, detect-libc@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b" @@ -3472,12 +3482,13 @@ electron-publish@20.5.0: lazy-val "^1.0.3" mime "^2.2.0" -electron-rebuild@1.6.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/electron-rebuild/-/electron-rebuild-1.6.0.tgz#e8d26f4d8e9fe5388df35864b3658e5cfd4dcb7e" +electron-rebuild@^1.7.3: + version "1.7.3" + resolved "https://registry.yarnpkg.com/electron-rebuild/-/electron-rebuild-1.7.3.tgz#24ae06ad9dd61cb7e4d688961f49118c40a110eb" dependencies: colors "^1.1.2" debug "^2.6.3" + detect-libc "^1.0.3" fs-extra "^3.0.1" node-abi "^2.0.0" node-gyp "^3.6.0" @@ -3503,9 +3514,9 @@ electron-window@^0.8.0: dependencies: is-electron-renderer "^2.0.0" -electron@^1.8.4: - version "1.8.4" - resolved "https://registry.yarnpkg.com/electron/-/electron-1.8.4.tgz#cca8d0e6889f238f55b414ad224f03e03b226a38" +electron@2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/electron/-/electron-2.0.2.tgz#b77e05f83419cc5ec921a2d21f35b55e4bfc3d68" dependencies: "@types/node" "^8.0.24" electron-download "^3.0.1" @@ -4033,7 +4044,7 @@ fb-watchman@^2.0.0: dependencies: bser "^2.0.0" -fbjs@^0.8.16, fbjs@^0.8.5, fbjs@^0.8.9: +fbjs@^0.8.16, fbjs@^0.8.5: version "0.8.16" resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.8.16.tgz#5e67432f550dc41b572bf55847b8aca64e5337db" dependencies: @@ -4800,10 +4811,6 @@ hoek@4.x.x: version "4.2.0" resolved "https://registry.yarnpkg.com/hoek/-/hoek-4.2.0.tgz#72d9d0754f7fe25ca2d01ad8f8f9a9449a89526d" -hoist-non-react-statics@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-1.2.0.tgz#aa448cf0986d55cc40773b17174b7dd066cb7cfb" - hoist-non-react-statics@^2.1.0, hoist-non-react-statics@^2.2.1: version "2.3.1" resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-2.3.1.tgz#343db84c6018c650778898240135a1420ee22ce0" @@ -5286,10 +5293,6 @@ is-fullwidth-code-point@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" -is-function@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/is-function/-/is-function-1.0.1.tgz#12cfb98b65b57dd3d193a3121f5f6e2f437602b5" - is-generator-fn@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-generator-fn/-/is-generator-fn-1.0.0.tgz#969d49e1bb3329f6bb7f09089be26578b2ddd46a" @@ -6083,6 +6086,12 @@ json5@^0.5.0, json5@^0.5.1: version "0.5.1" resolved "https://registry.yarnpkg.com/json5/-/json5-0.5.1.tgz#1eade7acc012034ad84e2396767ead9fa5495821" +json5@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.1.tgz#779fb0018604fa854eacbf6252180d83543e3dbe" + dependencies: + minimist "^1.2.0" + jsonfile@^2.1.0: version "2.4.0" resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-2.4.0.tgz#3736a2b428b87bbda0cc83b53fa3d633a35c2ae8" @@ -6528,9 +6537,9 @@ map-visit@^1.0.0: dependencies: object-visit "^1.0.0" -marked@^0.3.6: - version "0.3.7" - resolved "https://registry.yarnpkg.com/marked/-/marked-0.3.7.tgz#80ef3bbf1bd00d1c9cfebe42ba1b8c85da258d0d" +marked@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/marked/-/marked-0.4.0.tgz#9ad2c2a7a1791f10a852e0112f77b571dce10c66" math-expression-evaluator@^1.2.14: version "1.2.17" @@ -6942,9 +6951,9 @@ no-case@^2.2.0: dependencies: lower-case "^1.1.1" -node-abi@^2.0.0: - version "2.1.2" - resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-2.1.2.tgz#4da6caceb6685fcd31e7dd1994ef6bb7d0a9c0b2" +node-abi@^2.0.0, node-abi@^2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-2.4.1.tgz#7628c4d4ec4e9cd3764ceb3652f36b2e7f8d4923" dependencies: semver "^5.4.1" @@ -7324,13 +7333,17 @@ onetime@^2.0.0: dependencies: mimic-fn "^1.0.0" -oni-api@^0.0.42: - version "0.0.42" - resolved "https://registry.yarnpkg.com/oni-api/-/oni-api-0.0.42.tgz#79cab4289809bda1c7b86590119f0c7aaa5a053e" +oni-api@^0.0.46: + version "0.0.46" + resolved "https://registry.yarnpkg.com/oni-api/-/oni-api-0.0.46.tgz#99511a0c5488af1762b4744ce1ca79fea45b2b8b" -oni-neovim-binaries@0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/oni-neovim-binaries/-/oni-neovim-binaries-0.1.1.tgz#7aed74c14bca2581e1447c557541192dd5e89cdd" +oni-core-logging@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/oni-core-logging/-/oni-core-logging-1.0.0.tgz#7ad6c0ad8b06c23255202f97e229c2b0947dcf0b" + +oni-neovim-binaries@0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/oni-neovim-binaries/-/oni-neovim-binaries-0.1.2.tgz#fccfab6aa71922437119a8de149582648f4e521d" oni-release-downloader@^0.0.10: version "0.0.10" @@ -7346,9 +7359,9 @@ oni-ripgrep@0.0.4: version "0.0.4" resolved "https://registry.yarnpkg.com/oni-ripgrep/-/oni-ripgrep-0.0.4.tgz#8eb52383f4a3f92b8b5d8fe29d52e9d728a46b50" -oni-types@0.0.4: - version "0.0.4" - resolved "https://registry.yarnpkg.com/oni-types/-/oni-types-0.0.4.tgz#97bc435565d5f4c3bdc50bd1700fd24dd5b6704b" +oni-types@^0.0.8: + version "0.0.8" + resolved "https://registry.yarnpkg.com/oni-types/-/oni-types-0.0.8.tgz#1dfe97eda3f2b97dfcb94609af25fc2e4172fa33" oniguruma@^6.0.1: version "6.2.1" @@ -7412,13 +7425,13 @@ ora@^0.2.3: object-assign "^4.0.1" ora@^1.2.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/ora/-/ora-1.3.0.tgz#80078dd2b92a934af66a3ad72a5b910694ede51a" + version "1.4.0" + resolved "https://registry.yarnpkg.com/ora/-/ora-1.4.0.tgz#884458215b3a5d4097592285f93321bb7a79e2e5" dependencies: - chalk "^1.1.1" + chalk "^2.1.0" cli-cursor "^2.1.0" - cli-spinners "^1.0.0" - log-symbols "^1.0.2" + cli-spinners "^1.0.1" + log-symbols "^2.1.0" original@>=0.0.5: version "1.0.0" @@ -7452,7 +7465,14 @@ os-tmpdir@^1.0.0, os-tmpdir@^1.0.1, os-tmpdir@~1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" -osenv@0, osenv@^0.1.4: +osenv@0: + version "0.1.5" + resolved "https://registry.yarnpkg.com/osenv/-/osenv-0.1.5.tgz#85cdfafaeb28e8677f416e287592b5f3f49ea410" + dependencies: + os-homedir "^1.0.0" + os-tmpdir "^1.0.0" + +osenv@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/osenv/-/osenv-0.1.4.tgz#42fe6d5953df06c8064be6f176c3d05aaaa34644" dependencies: @@ -8010,11 +8030,7 @@ preserve@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/preserve/-/preserve-0.2.0.tgz#815ed1f6ebc65926f865b310c0713bcb3315ce4b" -prettier@^1.10.2: - version "1.10.2" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.10.2.tgz#1af8356d1842276a99a5b5529c82dd9e9ad3cc93" - -prettier@^1.5.3: +prettier@^1.12.1, prettier@^1.5.3: version "1.12.1" resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.12.1.tgz#c1ad20e803e7749faf905a409d2367e06bbe7325" @@ -8036,9 +8052,9 @@ pretty-format@^22.1.0: ansi-regex "^3.0.0" ansi-styles "^3.2.0" -pretty-quick@^1.2.2: - version "1.2.2" - resolved "https://registry.yarnpkg.com/pretty-quick/-/pretty-quick-1.2.2.tgz#4ebb2bafa86aa5f67948c3d2ead6f196770f9e32" +pretty-quick@^1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/pretty-quick/-/pretty-quick-1.5.0.tgz#304853ece7f8cb56bec74ba3ccd978037e7f2117" dependencies: chalk "^2.3.0" execa "^0.8.0" @@ -8281,9 +8297,9 @@ react-dnd@^2.5.4: lodash "^4.2.0" prop-types "^15.5.10" -react-dom@16.0.0: - version "16.0.0" - resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.0.0.tgz#9cc3079c3dcd70d4c6e01b84aab2a7e34c303f58" +react-dom@^16.3.2: + version "16.3.2" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.3.2.tgz#cb90f107e09536d683d84ed5d4888e9640e0e4df" dependencies: fbjs "^0.8.16" loose-envify "^1.1.0" @@ -8301,6 +8317,10 @@ react-hot-loader@^4.0.1: react-lifecycles-compat "^2.0.0" shallowequal "^1.0.2" +react-is@^16.3.1: + version "16.3.2" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.3.2.tgz#f4d3d0e2f5fbb6ac46450641eb2e25bf05d36b22" + react-lifecycles-compat@^2.0.0: version "2.0.2" resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-2.0.2.tgz#00a23160eec17a43b94dd74f95d44a1a2c3c5ec1" @@ -8362,9 +8382,9 @@ react-virtualized@^9.18.0: loose-envify "^1.3.0" prop-types "^15.5.4" -react@16.0.0: - version "16.0.0" - resolved "https://registry.yarnpkg.com/react/-/react-16.0.0.tgz#ce7df8f1941b036f02b2cca9dbd0cb1f0e855e2d" +react@^16.3.2: + version "16.3.2" + resolved "https://registry.yarnpkg.com/react/-/react-16.3.2.tgz#fdc8420398533a1e58872f59091b272ce2f91ea9" dependencies: fbjs "^0.8.16" loose-envify "^1.1.0" @@ -8690,9 +8710,9 @@ request-promise-native@^1.0.3, request-promise-native@^1.0.5: stealthy-require "^1.1.0" tough-cookie ">=2.3.3" -request@2, request@^2.45.0, request@^2.79.0, request@^2.83.0, request@~2.83.0: - version "2.83.0" - resolved "https://registry.yarnpkg.com/request/-/request-2.83.0.tgz#ca0b65da02ed62935887808e6f510381034e3356" +request@2: + version "2.86.0" + resolved "https://registry.yarnpkg.com/request/-/request-2.86.0.tgz#2b9497f449b0a32654c081a5cf426bbfb5bf5b69" dependencies: aws-sign2 "~0.7.0" aws4 "^1.6.0" @@ -8712,7 +8732,6 @@ request@2, request@^2.45.0, request@^2.79.0, request@^2.83.0, request@~2.83.0: performance-now "^2.1.0" qs "~6.5.1" safe-buffer "^5.1.1" - stringstream "~0.0.5" tough-cookie "~2.3.3" tunnel-agent "^0.6.0" uuid "^3.1.0" @@ -8744,6 +8763,33 @@ request@2.81.0, request@~2.81.0: tunnel-agent "^0.6.0" uuid "^3.0.0" +request@^2.45.0, request@^2.79.0, request@^2.83.0, request@~2.83.0: + version "2.83.0" + resolved "https://registry.yarnpkg.com/request/-/request-2.83.0.tgz#ca0b65da02ed62935887808e6f510381034e3356" + dependencies: + aws-sign2 "~0.7.0" + aws4 "^1.6.0" + caseless "~0.12.0" + combined-stream "~1.0.5" + extend "~3.0.1" + forever-agent "~0.6.1" + form-data "~2.3.1" + har-validator "~5.0.3" + hawk "~6.0.2" + http-signature "~1.2.0" + is-typedarray "~1.0.0" + isstream "~0.1.2" + json-stringify-safe "~5.0.1" + mime-types "~2.1.17" + oauth-sign "~0.8.2" + performance-now "^2.1.0" + qs "~6.5.1" + safe-buffer "^5.1.1" + stringstream "~0.0.5" + tough-cookie "~2.3.3" + tunnel-agent "^0.6.0" + uuid "^3.1.0" + request@^2.81.0: version "2.85.0" resolved "https://registry.yarnpkg.com/request/-/request-2.85.0.tgz#5a03615a47c61420b3eb99b7dba204f83603e1fa" @@ -8910,13 +8956,7 @@ rx@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/rx/-/rx-4.1.0.tgz#a5f13ff79ef3b740fe30aa803fb09f98805d4782" -rxjs@^5.1.1: - version "5.5.2" - resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-5.5.2.tgz#28d403f0071121967f18ad665563255d54236ac3" - dependencies: - symbol-observable "^1.0.1" - -rxjs@^5.4.2, rxjs@^5.5.2: +rxjs@^5.1.1, rxjs@^5.4.2, rxjs@^5.5.2: version "5.5.10" resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-5.5.10.tgz#fde02d7a614f6c8683d0d1957827f492e09db045" dependencies: @@ -9632,23 +9672,28 @@ style-loader@0.18.2: loader-utils "^1.0.2" schema-utils "^0.3.0" -styled-components@^2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/styled-components/-/styled-components-2.3.0.tgz#d9cf4574e140fea6426e48632ed0ca4494537718" +styled-components@^3.2.6: + version "3.2.6" + resolved "https://registry.yarnpkg.com/styled-components/-/styled-components-3.2.6.tgz#99e6e75a746bdedd295a17e03dd1493055a1cc3b" dependencies: buffer "^5.0.3" css-to-react-native "^2.0.3" - fbjs "^0.8.9" - hoist-non-react-statics "^1.2.0" - is-function "^1.0.1" + fbjs "^0.8.16" + hoist-non-react-statics "^2.5.0" is-plain-object "^2.0.1" prop-types "^15.5.4" - stylis "^3.4.0" + react-is "^16.3.1" + stylis "^3.5.0" + stylis-rule-sheet "^0.0.10" supports-color "^3.2.3" -stylis@^3.4.0: - version "3.4.5" - resolved "https://registry.yarnpkg.com/stylis/-/stylis-3.4.5.tgz#d7b9595fc18e7b9c8775eca8270a9a1d3e59806e" +stylis-rule-sheet@^0.0.10: + version "0.0.10" + resolved "https://registry.yarnpkg.com/stylis-rule-sheet/-/stylis-rule-sheet-0.0.10.tgz#44e64a2b076643f4b52e5ff71efc04d8c3c4a430" + +stylis@^3.5.0: + version "3.5.0" + resolved "https://registry.yarnpkg.com/stylis/-/stylis-3.5.0.tgz#016fa239663d77f868fef5b67cf201c4b7c701e1" subarg@^1.0.0: version "1.0.0" @@ -9743,7 +9788,7 @@ symbol-observable@^0.2.2: version "0.2.4" resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-0.2.4.tgz#95a83db26186d6af7e7a18dbd9760a2f86d08f40" -symbol-observable@^1.0.1, symbol-observable@^1.0.3: +symbol-observable@^1.0.3: version "1.0.4" resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.0.4.tgz#29bf615d4aa7121bdd898b22d4b3f9bc4e2aa03d"