Feature/initial Git VCS provider (#1310)

* add git addition and deletion count

* add typing to git service

* add ideas for styling

* add error handling and checks to determine rendering

* reorder comma separator

* get gitroot utility and pass in current dit

* fix name typo for gitroot fn

* add logs for debugging

* [WIP] debug ocaml lang server

* promisify exec command
merge upstream

* add semicolon to external commandline config

* add semicolon to js file as per lint error

* write manual diffSummary function
add per file changes and option to show perfile changes

* add try catch block to prevent errors

* add further check for error points

* comment out unused fn call cast promisified type to any
add fixme comment to check back in re this typing

* add FA icons add spacer char
change rendering so elements are only created
as necessary

* change long line initialisation to explicit decls

* typos re variable names

* revert markdown changes

* revert markdown changes

* remove local changes as they make little sense
fix counting of local changes in case of future use case

* re-add git-js

* tweak use of git js

* initialise git sync then use return git obj

* remove cwd calls from git instantiation

* ongoing experimentations with git plugin

* initial re-implementation of git vcs as per #2110

* attempt promises with git and use workspace

* use simple-git to get branch

* remove console.log return empty string if no current branch

* try using require version of simple git and saving git instance

* try using chaining api rather than async/await

* add git branch menu and tidy up plugin
add get branches method and git checkout method to vcs provider

* move git into version control service and add index

* Use arrow functions in git vcs provider

* add simple git to webpack external to prevent minfication mangling

* add fetch branch commands

* invert git plugin and git provider [WIP]

* add functioning vcs manager

* separate components into separate file
remove specific  references to git

* add vcs components file

* remove errant console.log

* switch to using the registerProvider pattern

* add sidebar plugin for vcs

* tweak title styles and add modified count

* use npm/yarn-run-all for start command to avoid memory leak

* fix typing of files in vcsStore

* use oni.log to log errors or default to console

* revert changes in README.md

* add simple-git as a dependency and typescript as a dev dependency

* use run-s for unit tests

* format response from git js to match VCSProvider
return status object and render based on that

* add more events to VersionControl interface

* add enzyme tests for components, refactor additions and completions

* add enzyme test file

* tweak styles following component refactor

* tidy up deletions and insertions component

* add tests for VersionControlView component

* fix stagedfileschanged event name typo
use events to trigger getStatus in vcs pane

* encapsulate getInstance phew.. really bugged me trade accuracy for cleverness in update branch indicator

* fix use of menu api as it miss matches with the menumanager

* dont export the init function in vcsmanager

* Set error state in redux on error

* update rxjs to 5.5.10 to match oni-types package

* use fork of "oni-type" to test if ci passes

* fix overflow issues and use word-break

* wrap provider methods in try catch and explicitly return void or val

* downgrade to rxjs 5.5.8

* revert to now fixed oni-types package

* add initial functionality to activate and deactivate plugin

* [WIP] Switch to activation and deactivation exclusively through events

* remove console.log

* add check inside vcsManager to control when plugin loads WIP

* fix provider selection check

* remove extra newline

* upgrade oni types to 0.0.8

* fix manager comment typo add, ability to navigate titles
add visibility filter to view component
remove unused check in get status

* add ability to collapse and expand sections

* update tests and change from using capitalize fn :(

* Move version control components, tidy up provider activations handler
fix tests and componentize the caret

* update snapshots

* fix parsing issues in ui-test tabs

* revert changes in FileConfiguration and tsconfig react

* only create sidebar if none is existent
remove unnecessary return, revert prettier to master version

* remove useless wrapper div

* update snapshots

* allow git provider errors to bubble up so they can be handled
with notifications which inform the user of why something is not working

* remove unused var

* add vcsmanager test in jest

* fix not re-applying state error

* pull upstream

* remove uses of Log and use oni-core-logging

* merge upstream fix conflicts

* fix vcs name issue re. notifications

* remove extra variable in App.ts

* set simple git working dir constructor and handle updates there
remove explicit passing in of working dir

* remove vcs provider from duplicate interface

* update functions to remove unnecessary args

* run get diff and get branch in parallel fix missing return in diff fn

* explicitly pass in the project root to prevent race conditions
whilst changing dir

* fix removal of initial gitp assignment in constructor

* improve version control manager tests

* update jest to avoid coverage bug improve test further

* add send notification to vcs pane args

* update snapshots

* fix import ordering in view

* gst

* separate out subscription creation and error notifications
switch package json plugin commands to && chain

* remove console.logs

* rename args

* split out mocks into separate files

* update tests wip

* remove is repo check since plugin should only activate if is true
convert handle provider to async function

* await handle provider status

* remove console.log assign subscriptions directly

* upgrade ts-jest and redux mock store

* add readable names to handle provider status

* import * for vcs types fix import of redux mock store

* add vcs definition file to be deleted once oni-api adds vcs

* initialise project root to activeWorkspace

* revert changes to commont.ts use SFC typing

* fix package json

* add vcs store test

* fix new name of versioncontrolstate

* hide sidebar behind experimental feature flag

* remove collect coverage from jest config option

* update version control pane to focus on testing react not redux

* add jest to codecov command
remove unnecessary type annotations

* add jest coverage json to final istanbul report

* update jest coverage runner

* change path name for jest coverage file

* add inlineSourceMap option to jest tsconfig

* remove jest coverage merging

* add jest coverage upload command

* remove unnecessary coverage flag and type annotations

* fix chained and operator ui bug
using thing && thing && component can cause 0 to be rendered
This commit is contained in:
Akin 2018-07-18 14:02:44 +01:00 committed by GitHub
parent 388c740aae
commit 97899e1222
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
44 changed files with 2589 additions and 710 deletions

View File

@ -89,6 +89,7 @@ export const start = async (args: string[]): Promise<void> => {
const globalCommandsPromise = import("./Services/Commands/GlobalCommands")
const inputManagerPromise = import("./Services/InputManager")
const languageManagerPromise = import("./Services/Language")
const vcsManagerPromise = import("./Services/VersionControl")
const notificationsPromise = import("./Services/Notifications")
const snippetPromise = import("./Services/Snippets")
const keyDisplayerPromise = import("./Services/KeyDisplayer")
@ -235,9 +236,9 @@ export const start = async (args: string[]): Promise<void> => {
const Notifications = await notificationsPromise
Notifications.activate(configuration, overlayManager)
const notifications = Notifications.getInstance()
if (typeof developmentPluginError !== "undefined") {
const notifications = Notifications.getInstance()
const notification = notifications.createItem()
notification.setContents(developmentPluginError.title, developmentPluginError.errorText)
notification.setLevel("error")
@ -248,7 +249,6 @@ export const start = async (args: string[]): Promise<void> => {
}
configuration.onConfigurationError.subscribe(err => {
const notifications = Notifications.getInstance()
const notification = notifications.createItem()
notification.setContents("Error Loading Configuration", err.toString())
notification.setLevel("error")
@ -318,6 +318,18 @@ export const start = async (args: string[]): Promise<void> => {
Sidebar.activate(configuration, workspace)
const sidebarManager = Sidebar.getInstance()
const VCSManager = await vcsManagerPromise
VCSManager.activate(
workspace,
editorManager,
statusBar,
commandManager,
menuManager,
sidebarManager,
notifications,
configuration,
)
Explorer.activate(
commandManager,
configuration,

View File

@ -1,7 +1,7 @@
import * as Git from "../../Services/Git"
import { getInstance, VersionControlManager } from "./../../Services/VersionControl"
export class Services {
public get git(): any {
return Git
public get vcs(): VersionControlManager {
return getInstance()
}
}

View File

@ -2,6 +2,7 @@ import * as fs from "fs"
import * as path from "path"
import * as Oni from "oni-api"
import { Event, IEvent } from "oni-types"
import { Configuration, getUserConfigFolderPath } from "./../Services/Configuration"
import { IContributions } from "./Api/Capabilities"
@ -23,6 +24,11 @@ export class PluginManager implements Oni.IPluginManager {
private _anonymousPlugin: AnonymousPlugin
private _pluginsActivated: boolean = false
private _installer: IPluginInstaller = new YarnPluginInstaller()
private _pluginsLoaded = new Event<void>()
public get pluginsAllLoaded(): IEvent<void> {
return this._pluginsLoaded
}
private _developmentPluginsPath: string[] = []
@ -81,6 +87,7 @@ export class PluginManager implements Oni.IPluginManager {
})
this._pluginsActivated = true
this._pluginsLoaded.dispatch()
return this._anonymousPlugin.oni
}

View File

@ -53,6 +53,7 @@ const BaseConfiguration: IConfigurationValues = {
"wildmenu.mode": true,
"commandline.mode": true,
"commandline.icons": true,
"experimental.vcs.sidebar": false,
"experimental.particles.enabled": false,
"experimental.preview.enabled": false,
"experimental.welcome.enabled": false,

View File

@ -49,6 +49,8 @@ export interface IConfigurationValues {
// Whether or not the learning pane is available
"experimental.particles.enabled": boolean
// Whether Version control sidebar item is enabled
"experimental.vcs.sidebar": boolean
// Whether the color highlight layer is enabled
"experimental.colorHighlight.enabled": boolean
// Whitelist of extension for the color highlight layer

View File

@ -1,32 +0,0 @@
/**
* Git.ts
*
* Utilities around Git
*/
import { exec } from "child_process"
interface IExecOptions {
cwd?: string
}
export function getBranch(path?: string): Promise<string> {
return new Promise((resolve, reject) => {
const options: IExecOptions = {}
if (path) {
options.cwd = path
}
exec(
"git rev-parse --abbrev-ref HEAD",
options,
(error: any, stdout: string, stderr: string) => {
if (error && error.code) {
reject(new Error(stderr))
} else {
resolve(stdout)
}
},
)
})
}

View File

@ -95,7 +95,8 @@ export const start = (configuration: Configuration, notifications: Notifications
const errorText = val ? val.toString() : "Open the debugger for more details."
showError(
"Unhandled Exception",
errorText + "\nPlease report this error. Callstack: " + val.stack,
errorText +
`\nPlease report this error. ${val && val.stack ? `Callstack:` + val.stack : ""}`,
)
})

View File

@ -0,0 +1,309 @@
import { capitalize } from "lodash"
import * as Oni from "oni-api"
import * as Log from "oni-core-logging"
import { IDisposable } from "oni-types"
import * as React from "react"
import { store, SupportedProviders, VersionControlPane, VersionControlProvider } from "./"
import { Notifications } from "./../../Services/Notifications"
import { Branch } from "./../../UI/components/VersionControl"
import { MenuManager } from "./../Menu"
import { SidebarManager } from "./../Sidebar"
import { IWorkspace } from "./../Workspace"
interface ISendNotificationsArgs {
detail: string
level: "info" | "warn"
title: string
}
export type ISendVCSNotification = (args: ISendNotificationsArgs) => void
export class VersionControlManager {
private _vcs: SupportedProviders
private _vcsProvider: VersionControlProvider
private _menuInstance: Oni.Menu.MenuInstance
private _vcsStatusItem: Oni.StatusBarItem
private _subscriptions: IDisposable[] = []
private _providers = new Map<string, VersionControlProvider>()
constructor(
private _workspace: IWorkspace,
private _editorManager: Oni.EditorManager,
private _statusBar: Oni.StatusBar,
private _menu: MenuManager,
private _commands: Oni.Commands.Api,
private _sidebar: SidebarManager,
private _notifications: Notifications,
private _configuration: Oni.Configuration,
) {}
public get providers() {
return this._providers
}
public get activeProvider(): VersionControlProvider {
return this._vcsProvider
}
public async registerProvider(provider: VersionControlProvider): Promise<void> {
if (provider) {
this._providers.set(provider.name, provider)
const canHandleWorkspace = await provider.canHandleWorkspace()
if (canHandleWorkspace) {
await this._activateVCSProvider(provider)
}
this._workspace.onDirectoryChanged.subscribe(async dir => {
const providerToUse = await this.getCompatibleProvider(dir)
await this.handleProviderStatus(providerToUse)
})
}
}
// Use arrow function to maintain this binding of sendNotification
public sendNotification: ISendVCSNotification = ({ detail, level, title }) => {
const notification = this._notifications.createItem()
notification.setContents(title, detail)
notification.setExpiration(3_000)
notification.setLevel(level)
notification.show()
}
public deactivateProvider(): void {
this._vcsProvider.deactivate()
this._subscriptions.map(s => s.dispose())
if (this._vcsStatusItem) {
this._vcsStatusItem.hide()
}
this._vcsProvider = null
this._vcs = null
}
public async handleProviderStatus(newProvider: VersionControlProvider): Promise<void> {
const isSameProvider = this._vcsProvider && newProvider && this._vcs === newProvider.name
const noCompatibleProvider = this._vcsProvider && !newProvider
const newReplacementProvider = Boolean(this._vcsProvider && newProvider)
const compatibleProvider = Boolean(!this._vcsProvider && newProvider)
switch (true) {
case isSameProvider:
break
case noCompatibleProvider:
this.deactivateProvider()
break
case newReplacementProvider:
this.deactivateProvider()
await this._activateVCSProvider(newProvider)
break
case compatibleProvider:
await this._activateVCSProvider(newProvider)
break
default:
break
}
}
private async getCompatibleProvider(dir: string): Promise<VersionControlProvider | null> {
const allCompatibleProviders: VersionControlProvider[] = []
for (const vcs of this._providers.values()) {
const isCompatible = await vcs.canHandleWorkspace(dir)
if (isCompatible) {
allCompatibleProviders.push(vcs)
}
}
// TODO: when we have multiple providers we will need logic to determine which to
// use if more than one is compatible
const [providerToUse] = allCompatibleProviders
return providerToUse
}
private _activateVCSProvider = async (provider: VersionControlProvider) => {
this._vcs = provider.name
this._vcsProvider = provider
await this._initialize()
provider.activate()
}
private async _initialize() {
try {
await this._updateBranchIndicator()
this._setupSubscriptions()
const hasVcsSidebar = this._sidebar.entries.some(({ id }) => id.includes("vcs"))
const enabled = this._configuration.getValue("experimental.vcs.sidebar")
if (!hasVcsSidebar && enabled) {
const vcsPane = new VersionControlPane(
this._editorManager,
this._workspace,
this._vcsProvider,
this.sendNotification,
store,
)
this._sidebar.add("code-fork", vcsPane)
}
this._registerCommands()
} catch (e) {
Log.warn(`Failed to initialise provider, because, ${e.message}`)
}
}
private _setupSubscriptions() {
this._subscriptions = [
this._editorManager.activeEditor.onBufferEnter.subscribe(async () => {
await this._updateBranchIndicator()
}),
this._vcsProvider.onBranchChanged.subscribe(async newBranch => {
await this._updateBranchIndicator(newBranch)
await this._editorManager.activeEditor.neovim.command("e!")
}),
this._editorManager.activeEditor.onBufferSaved.subscribe(async () => {
await this._updateBranchIndicator()
}),
(this._workspace as any).onFocusGained.subscribe(async () => {
await this._updateBranchIndicator()
}),
]
}
private _registerCommands = () => {
this._commands.registerCommand({
command: `oni.${this._vcs}.fetch`,
name: "Fetch the selected branch",
detail: "",
execute: this._fetchBranch,
})
this._commands.registerCommand({
command: `oni.${this._vcs}.branches`,
name: `Local ${capitalize(this._vcs)} Branches`,
detail: "Open a menu with a list of all local branches",
execute: this._createBranchList,
})
}
private _updateBranchIndicator = async (branchName?: string) => {
if (!this._vcsProvider) {
return
} else if (!this._vcsStatusItem) {
const vcsId = `oni.status.${this._vcs}`
this._vcsStatusItem = this._statusBar.createItem(1, vcsId)
}
try {
const branch = await this._vcsProvider.getBranch()
const diff = await this._vcsProvider.getDiff()
if (!branch || !diff) {
return Log.warn(`The ${!branch ? "branch name" : "diff"} could not be found`)
} else if (!branch && !diff) {
return this._vcsStatusItem.hide()
}
this._vcsStatusItem.setContents(<Branch branch={branch} diff={diff} />)
this._vcsStatusItem.show()
} catch (e) {
this._notifyOfError(e)
return this._vcsStatusItem.hide()
}
}
private _createBranchList = async () => {
if (!this._vcsProvider) {
return
}
const [currentBranch, branches] = await Promise.all([
this._vcsProvider.getBranch(),
this._vcsProvider.getLocalBranches(),
])
this._menuInstance = this._menu.create()
if (!branches) {
return
}
const branchItems = branches.all.map(branch => ({
label: branch,
icon: "code-fork",
pinned: currentBranch === branch,
}))
this._menuInstance.show()
this._menuInstance.setItems(branchItems)
this._menuInstance.onItemSelected.subscribe(async menuItem => {
if (menuItem && menuItem.label) {
try {
await this._vcsProvider.changeBranch(menuItem.label)
} catch (e) {
this._notifyOfError(e)
}
}
})
}
private _notifyOfError(error: Error) {
const name = this._vcsProvider ? capitalize(this._vcs) : "VCS"
const errorMessage = error && error.message ? error.message : null
this.sendNotification({
title: `${capitalize(name)} Plugin Error:`,
detail: `${name} plugin encountered an error ${errorMessage}`,
level: "warn",
})
}
private _fetchBranch = async () => {
if (this._menuInstance.isOpen() && this._menuInstance.selectedItem) {
try {
await this._vcsProvider.fetchBranchFromRemote({
currentDir: this._workspace.activeWorkspace,
branch: this._menuInstance.selectedItem.label,
})
} catch (e) {
this._notifyOfError(e)
}
}
}
}
// Shelter the instance from the global scope -> globals are evil.
function init() {
let Provider: VersionControlManager
const Activate = (
workspace: IWorkspace,
editorManager: Oni.EditorManager,
statusBar: Oni.StatusBar,
commands: Oni.Commands.Api,
menu: MenuManager,
sidebar: SidebarManager,
notifications: Notifications,
configuration: Oni.Configuration,
): void => {
Provider = new VersionControlManager(
workspace,
editorManager,
statusBar,
menu,
commands,
sidebar,
notifications,
configuration,
)
}
const GetInstance = () => {
return Provider
}
return {
activate: Activate,
getInstance: GetInstance,
}
}
export const { activate, getInstance } = init()

View File

@ -0,0 +1,111 @@
import * as capitalize from "lodash/capitalize"
import * as Oni from "oni-api"
import * as Log from "oni-core-logging"
import * as React from "react"
import { Provider, Store } from "react-redux"
import { VersionControlProvider, VersionControlView } from "./"
import { IWorkspace } from "./../Workspace"
import { ISendVCSNotification } from "./VersionControlManager"
import { VersionControlState } from "./VersionControlStore"
export default class VersionControlPane {
public get id(): string {
return "oni.sidebar.vcs"
}
public get title(): string {
return capitalize(this._vcsProvider.name)
}
constructor(
private _editorManager: Oni.EditorManager,
private _workspace: IWorkspace,
private _vcsProvider: VersionControlProvider,
private _sendNotification: ISendVCSNotification,
private _store: Store<VersionControlState>,
) {
this._editorManager.activeEditor.onBufferSaved.subscribe(async () => {
await this.getStatus()
})
this._vcsProvider.onBranchChanged.subscribe(async () => {
await this.getStatus()
})
this._vcsProvider.onStagedFilesChanged.subscribe(async () => {
await this.getStatus()
})
this._vcsProvider.onPluginActivated.subscribe(async () => {
this._store.dispatch({ type: "ACTIVATE" })
await this.getStatus()
})
this._vcsProvider.onPluginDeactivated.subscribe(() => {
this._store.dispatch({ type: "DEACTIVATE" })
})
}
public enter(): void {
this._store.dispatch({ type: "ENTER" })
this._workspace.onDirectoryChanged.subscribe(async () => {
await this.getStatus()
})
}
public leave(): void {
this._store.dispatch({ type: "LEAVE" })
}
public getStatus = async () => {
const status = await this._vcsProvider.getStatus()
if (status) {
this._store.dispatch({ type: "STATUS", payload: { status } })
}
return status
}
public stageFile = async (file: string) => {
const { activeWorkspace } = this._workspace
try {
await this._vcsProvider.stageFile(file, activeWorkspace)
} catch (e) {
this._sendNotification({
detail: e.message,
level: "warn",
title: "Error Staging File",
})
}
}
public setError = async (e: Error) => {
Log.warn(`version control pane failed to render due to ${e.message}`)
this._store.dispatch({ type: "ERROR" })
}
public handleSelection = async (file: string): Promise<void> => {
const { status } = this._store.getState()
switch (true) {
case status.untracked.includes(file):
case status.modified.includes(file):
await this.stageFile(file)
break
case status.staged.includes(file):
default:
break
}
}
public render(): JSX.Element {
return (
<Provider store={this._store}>
<VersionControlView
setError={this.setError}
getStatus={this.getStatus}
handleSelection={this.handleSelection}
/>
</Provider>
)
}
}

View File

@ -0,0 +1,74 @@
import { IEvent } from "oni-types"
import { BranchSummary, FetchResult } from "simple-git/promise"
export type BranchChangedEvent = string
export type StagedFilesChangedEvent = string
export interface FileStatusChangedEvent {
path: string
status: "staged"
}
export interface StatusResult {
ahead: number
behind: number
currentBranch: string
modified: string[]
staged: string[]
conflicted: string[]
created: string[]
deleted: string[]
untracked: string[]
remoteTrackingBranch: string
}
export interface VersionControlProvider {
// Events
onFileStatusChanged: IEvent<FileStatusChangedEvent>
onStagedFilesChanged: IEvent<StagedFilesChangedEvent>
onBranchChanged: IEvent<BranchChangedEvent>
onPluginActivated: IEvent<void>
onPluginDeactivated: IEvent<void>
name: SupportedProviders
isActivated: boolean
deactivate(): void
activate(): void
canHandleWorkspace(dir?: string): Promise<boolean>
getStatus(): Promise<StatusResult | void>
getRoot(): Promise<string | void>
getDiff(): Promise<Diff | void>
getBranch(): Promise<string | void>
getLocalBranches(): Promise<BranchSummary | void>
changeBranch(branch: string): Promise<void>
stageFile(file: string, projectRoot?: string): Promise<void>
fetchBranchFromRemote(args: {
branch: string
origin?: string
currentDir: string
}): Promise<FetchResult>
}
export interface DiffResultTextFile {
file: string
changes: number
insertions: number
deletions: number
binary: boolean
}
export interface DiffResultBinaryFile {
file: string
before: number
after: number
binary: boolean
}
export interface Diff {
files: Array<DiffResultTextFile | DiffResultBinaryFile>
insertions: number
deletions: number
}
export type Summary = StatusResult
export type SupportedProviders = "git" | "svn"
export default VersionControlProvider

View File

@ -0,0 +1,80 @@
import { createStore as createReduxStore } from "./../../Redux"
import { StatusResult } from "./VersionControlProvider"
export interface VersionControlState {
status: StatusResult
hasFocus: boolean
hasError: boolean
activated: boolean
}
interface IGenericAction<T, P = undefined> {
type: T
payload?: P
}
export const DefaultState: VersionControlState = {
status: {
currentBranch: null,
staged: [],
conflicted: [],
created: [],
modified: [],
remoteTrackingBranch: null,
deleted: [],
untracked: [],
ahead: null,
behind: null,
},
hasFocus: null,
activated: null,
hasError: false,
}
type IActivateAction = IGenericAction<"ACTIVATE">
type IDeactivateAction = IGenericAction<"DEACTIVATE">
type IEnterAction = IGenericAction<"ENTER">
type ILeaveAction = IGenericAction<"LEAVE">
type IErrorAction = IGenericAction<"ERROR">
type IStatusAction = IGenericAction<"STATUS", { status: StatusResult }>
type IAction =
| IStatusAction
| IEnterAction
| ILeaveAction
| IErrorAction
| IDeactivateAction
| IActivateAction
export function reducer(state: VersionControlState, action: IAction) {
switch (action.type) {
case "ENTER":
return { ...state, hasFocus: true }
case "LEAVE":
return { ...state, hasFocus: false }
case "STATUS":
return {
...state,
status: action.payload.status,
}
case "DEACTIVATE":
return {
...state,
activated: false,
status: DefaultState.status,
}
case "ACTIVATE":
return {
...state,
activated: true,
}
case "ERROR":
return {
...state,
hasError: true,
}
default:
return state
}
}
export default createReduxStore("Version Control", reducer, DefaultState)

View File

@ -0,0 +1,226 @@
import * as path from "path"
import * as React from "react"
import { connect } from "react-redux"
import { Icon } from "../../UI/Icon"
import Caret from "./../../UI/components/Caret"
import { css, styled, withProps } from "./../../UI/components/common"
import { Sneakable } from "./../../UI/components/Sneakable"
import { VimNavigator } from "./../../UI/components/VimNavigator"
import { StatusResult } from "./VersionControlProvider"
import { VersionControlState } from "./VersionControlStore"
const Row = styled.div`
display: flex;
span > {
margin-right: 0.2em;
}
`
interface SelectionProps {
isSelected?: boolean
}
const selected = css`
border: ${(p: any) =>
p.isSelected && `1px solid ${p.theme["highlight.mode.normal.background"]}`};
`
const Column = withProps<SelectionProps>(styled.div)`
${selected};
display: flex;
flex-direction: column;
padding: 0.3em;
`
const Name = styled.span`
margin-left: 0.5em;
word-wrap: break-word;
`
const Title = styled.h4`
margin: 0;
`
export const SectionTitle = withProps<SelectionProps>(styled.div)`
${selected};
margin: 0.2em 0;
padding: 0.2em;
background-color: rgba(0, 0, 0, 0.2);
display: flex;
justify-content: space-between;
`
interface IModifiedFilesProps {
files?: string[]
titleId: string
selectedId: string
icon: string
onClick: (id: string) => void
toggleVisibility: () => void
visibility: boolean
}
const truncate = (str: string) =>
str
.split(path.sep)
.slice(-2)
.join(path.sep)
export const GitStatus = ({
files,
selectedId,
icon,
onClick,
toggleVisibility,
titleId,
visibility,
}: IModifiedFilesProps) =>
files && (
<div>
<SectionTitle
isSelected={selectedId === titleId}
data-test={`${titleId}-${files.length}`}
onClick={toggleVisibility}
>
<Caret active={visibility && !!files.length} />
<Title>{titleId.toUpperCase()}</Title>
<strong>{files.length}</strong>
</SectionTitle>
{visibility &&
files.map(filePath => (
<Sneakable callback={() => onClick(filePath)} key={filePath}>
<Column
onClick={() => onClick(filePath)}
isSelected={selectedId === filePath}
>
<Row>
<Icon name={icon} />
<Name>{truncate(filePath)}</Name>
</Row>
</Column>
</Sneakable>
))}
</div>
)
const StatusContainer = styled.div`
overflow-x: hidden;
overflow-y: auto;
`
interface IProps {
status: StatusResult
hasFocus: boolean
hasError: boolean
activated: boolean
setError?: (e: Error) => void
getStatus?: () => Promise<StatusResult | void>
handleSelection?: (selection: string) => void
children?: React.ReactNode
}
interface State {
modified: boolean
staged: boolean
untracked: boolean
}
export class VersionControlView extends React.Component<IProps, State> {
public state: State = {
modified: true,
staged: true,
untracked: true,
}
public async componentDidMount() {
await this.props.getStatus()
}
public async componentDidCatch(e: Error) {
this.props.setError(e)
}
public toggleVisibility = (section: keyof State) => {
this.setState(prevState => ({ ...prevState, [section]: !prevState[section] }))
}
public toggleOrAction = (id: string) => {
if (id === "modified" || id === "staged" || id === "untracked") {
this.toggleVisibility(id)
}
this.props.handleSelection(id)
}
public insertIf(condition: boolean, element: string[]) {
return condition ? element : []
}
public render() {
const error = this.props.hasError && "Something Went Wrong!"
const inactive = !this.props.activated && "Version Control Not Available"
const warning = error || inactive
const { modified, staged, untracked } = this.props.status
const ids = [
"modified",
...this.insertIf(this.state.modified, modified),
"staged",
...this.insertIf(this.state.staged, staged),
"untracked",
...this.insertIf(this.state.untracked, untracked),
]
return warning ? (
<SectionTitle>
<Title>{warning}</Title>
</SectionTitle>
) : (
<VimNavigator
ids={ids}
active={this.props.hasFocus}
onSelected={this.toggleOrAction}
render={selectedId => (
<StatusContainer>
<GitStatus
icon="minus-circle"
files={modified}
titleId="modified"
selectedId={selectedId}
visibility={this.state.modified}
onClick={this.props.handleSelection}
toggleVisibility={() => this.toggleVisibility("modified")}
/>
<GitStatus
icon="plus-circle"
titleId="staged"
files={staged}
selectedId={selectedId}
visibility={this.state.staged}
onClick={this.props.handleSelection}
toggleVisibility={() => this.toggleVisibility("staged")}
/>
<GitStatus
files={untracked}
icon="question-circle"
titleId="untracked"
selectedId={selectedId}
visibility={this.state.untracked}
onClick={this.props.handleSelection}
toggleVisibility={() => this.toggleVisibility("untracked")}
/>
</StatusContainer>
)}
/>
)
}
}
export default connect<VersionControlState>(
(state: VersionControlState): IProps => ({
status: state.status,
hasFocus: state.hasFocus,
hasError: state.hasError,
activated: state.activated,
}),
null,
)(VersionControlView)

View File

@ -0,0 +1,10 @@
export {
Diff,
Summary,
SupportedProviders,
default as VersionControlProvider,
} from "./VersionControlProvider"
export { activate, getInstance, VersionControlManager } from "./VersionControlManager"
export { default as VersionControlPane } from "./VersionControlPane"
export { default as store } from "./VersionControlStore"
export { default as VersionControlView } from "./VersionControlView"

View File

@ -0,0 +1,12 @@
import * as React from "react"
const Caret = ({ active }: { active: boolean }) => {
const caretStyle = {
transform: active ? "rotateZ(45deg)" : "rotateZ(0deg)",
transition: "transform 0.1s ease-in",
}
return <i style={caretStyle} className="fa fa-caret-right" />
}
export default Caret

View File

@ -8,6 +8,7 @@ import * as React from "react"
import { styled, withProps } from "./common"
import Caret from "./../../UI/components/Caret"
import { Sneakable } from "./../../UI/components/Sneakable"
export interface ISidebarItemViewProps {
@ -129,11 +130,6 @@ const SidebarContainer = withProps<IContainerProps>(styled.div)`
export class SidebarContainerView extends React.PureComponent<ISidebarContainerViewProps, {}> {
public render(): JSX.Element {
const caretStyle = {
transform: this.props.isExpanded ? "rotateZ(45deg)" : "rotateZ(0deg)",
transition: "transform 0.1s ease-in",
}
const icon = <i style={caretStyle} className="fa fa-caret-right" />
const indentationlevel = this.props.indentationLevel || 0
return (
@ -148,7 +144,7 @@ export class SidebarContainerView extends React.PureComponent<ISidebarContainerV
updated={this.props.updated}
didDrop={this.props.didDrop}
indentationLevel={indentationlevel}
icon={icon}
icon={<Caret active={this.props.isExpanded} />}
text={this.props.text}
isFocused={this.props.isFocused}
isContainer={this.props.isContainer}

View File

@ -0,0 +1,112 @@
import * as React from "react"
import { Diff } from "./../../Services/VersionControl"
import styled, { IThemeColors, withProps } from "./../../UI/components/common"
import { Icon } from "./../../UI/Icon"
type ChangeTypes = "change" | "addition" | "deletion"
interface ICreateIconArgs {
type: ChangeTypes
num: number
}
const BranchContainer = styled.div`
height: 100%;
width: 100%;
display: flex;
align-items: center;
`
const BranchText = styled.span`
min-width: 10px;
text-align: center;
padding: 2px 4px 0 0;
display: flex;
align-items: center;
`
export const BranchNameContainer = styled.span`
width: 100%;
margin-left: 4px;
`
const selectColorByType = (type: ChangeTypes, theme: IThemeColors) => {
switch (type) {
case "addition":
case "deletion":
case "change":
default:
return ""
}
}
const ChangeSpanContainer = withProps<{ type: ChangeTypes }>(styled.span)`
font-size: 0.7rem;
padding: 0 0.15rem;
color: ${({ type, theme }) => selectColorByType(type, theme)};
`
const ChangeSpan = styled.span`
padding-left: 0.25rem;
`
interface BranchProps {
branch: string
children?: React.ReactNode
diff: Diff
}
export const Branch: React.SFC<BranchProps> = ({ diff, branch, children }) =>
branch && (
<BranchContainer>
<BranchText>
<Icon name="code-fork" />
<BranchNameContainer>
{`${branch} `}
{diff && (
<DeletionsAndInsertions
deletions={diff.deletions}
insertions={diff.insertions}
/>
)}
{children}
</BranchNameContainer>
</BranchText>
</BranchContainer>
)
const getClassNameForType = (type: ChangeTypes) => {
switch (type) {
case "addition":
return "plus-circle"
case "deletion":
return "minus-circle"
case "change":
default:
return "question-circle"
}
}
interface ChangesProps {
deletions: number
insertions: number
}
export const DeletionsAndInsertions: React.SFC<ChangesProps> = ({ deletions, insertions }) => (
<span>
<VCSIcon type="addition" num={insertions} />
{!!(deletions && insertions) && <span key={2}>, </span>}
<VCSIcon type="deletion" num={deletions} />
</span>
)
export const VCSIcon: React.SFC<ICreateIconArgs> = ({ type, num }) =>
!!num && (
<span>
<ChangeSpanContainer type={type}>
<Icon name={getClassNameForType(type)} />
</ChangeSpanContainer>
<ChangeSpan data-test={`${type}-${num}`}>{num}</ChangeSpan>
</span>
)

View File

@ -6,7 +6,7 @@ import * as assert from "assert"
import * as path from "path"
import { Store } from "redux"
import { MockStoreCreator } from "redux-mock-store"
import configureMockStore, { MockStoreCreator } from "redux-mock-store"
import { ActionsObservable, combineEpics, createEpicMiddleware } from "redux-observable"
import * as ExplorerFileSystem from "./../../../src/Services/Explorer/ExplorerFileSystem"
@ -18,8 +18,6 @@ import * as clone from "lodash/clone"
import * as head from "lodash/head"
import * as TestHelpers from "./../../TestHelpers"
const configureMockStore = require("redux-mock-store") // tslint:disable-line
export class MockedFileSystem implements ExplorerFileSystem.IFileSystem {
public promises: Array<Promise<any>>

View File

@ -12,6 +12,7 @@ module.exports = {
"keyboard-layout": "require('keyboard-layout')",
gifshot: "require('gifshot')",
"msgpack-lite": "require('msgpack-lite')",
"simple-git/promise": "require('simple-git/promise')",
"styled-components": "require('styled-components')",
fsevents: "require('fsevents')",
},

View File

@ -43,6 +43,7 @@ if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then
ls -a lib_test/browser/src_ccov
npm run ccov:upload:jest
npm run ccov:test:browser
npm run ccov:remap:browser:lcov
npm run ccov:clean

View File

@ -2,8 +2,7 @@ module.exports = {
bail: true,
verbose: true,
collectCoverage: true,
coverageDirectory: "<rootDir>/coverage/",
collectCoverageFrom: ["**/components/*.{tsx}", "!**/node_modules/**", "!**/dist/**"],
coverageDirectory: "<rootDir>/coverage/jest/",
setupFiles: ["<rootDir>/ui-tests/jestsetup.ts"],
moduleNameMapper: {
electron: "<rootDir>/ui-tests/mocks/electronMock.ts",

View File

@ -5,14 +5,7 @@
"homepage": "https://www.onivim.io",
"version": "0.3.7",
"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": "./lib/cli/src/cli.js",
@ -50,39 +43,23 @@
"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",
@ -102,20 +79,12 @@
{
"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",
@ -130,13 +99,7 @@
{
"name": "C++ source",
"role": "Editor",
"ext": [
"cc",
"cp",
"cpp",
"cxx",
"c++"
]
"ext": ["cc", "cp", "cpp", "cxx", "c++"]
},
{
"name": "C# source",
@ -161,10 +124,7 @@
{
"name": "Clojure source",
"role": "Editor",
"ext": [
"clj",
"cljs"
]
"ext": ["clj", "cljs"]
},
{
"name": "Comma separated values",
@ -179,20 +139,12 @@
{
"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",
@ -217,10 +169,7 @@
{
"name": "Erlang source",
"role": "Editor",
"ext": [
"erl",
"hrl"
]
"ext": ["erl", "hrl"]
},
{
"name": "F-Script source",
@ -230,32 +179,17 @@
{
"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",
@ -265,28 +199,17 @@
{
"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",
@ -326,10 +249,7 @@
{
"name": "JavaScript source",
"role": "Editor",
"ext": [
"js",
"htc"
]
"ext": ["js", "htc"]
},
{
"name": "Java Server Page",
@ -354,14 +274,7 @@
{
"name": "Lisp source",
"role": "Editor",
"ext": [
"lisp",
"cl",
"l",
"lsp",
"mud",
"el"
]
"ext": ["lisp", "cl", "l", "lsp", "mud", "el"]
},
{
"name": "Log file",
@ -381,12 +294,7 @@
{
"name": "Markdown document",
"role": "Editor",
"ext": [
"markdown",
"mdown",
"markdn",
"md"
]
"ext": ["markdown", "mdown", "markdn", "md"]
},
{
"name": "Makefile source",
@ -396,29 +304,17 @@
{
"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",
@ -438,28 +334,17 @@
{
"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",
@ -469,11 +354,7 @@
{
"name": "Perl source",
"role": "Editor",
"ext": [
"pl",
"pod",
"perl"
]
"ext": ["pl", "pod", "perl"]
},
{
"name": "Perl module",
@ -483,80 +364,47 @@
{
"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",
@ -566,28 +414,17 @@
{
"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",
@ -635,10 +472,7 @@
{
"name": "SWIG source",
"role": "Editor",
"ext": [
"i",
"swg"
]
"ext": ["i", "swg"]
},
{
"name": "Tcl source",
@ -648,20 +482,12 @@
{
"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",
@ -681,32 +507,17 @@
{
"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",
@ -716,10 +527,7 @@
{
"name": "YAML document",
"role": "Editor",
"ext": [
"yaml",
"yml"
]
"ext": ["yaml", "yml"]
},
{
"name": "Text document",
@ -781,69 +589,104 @@
"scripts": {
"precommit": "pretty-quick --staged",
"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 && yarn run build:cli",
"build-debug": "yarn run build:browser-debug && yarn run build:main && yarn run build:plugins",
"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":
"yarn run build:plugin:oni-plugin-typescript && yarn run build:plugin:oni-plugin-git && yarn run build:plugin:oni-plugin-markdown-preview",
"build:cli": "cd cli && tsc -p tsconfig.json",
"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:plugin:oni-plugin-git": "cd vim/core/oni-plugin-git && 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": "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": "run-s build:test:unit:*",
"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": "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",
"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:setup": "yarn run build:test:integration && mocha -t 120000 --recursive lib_test/test/setup",
"test:integration": "yarn run build:test:integration && 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": "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",
"test:setup":
"yarn run build:test:integration && mocha -t 120000 --recursive lib_test/test/setup",
"test:integration":
"yarn run build:test:integration && mocha -t 120000 lib_test/test/CiTests.js --bail",
"test:unit:react": "jest --config ./jest.config.js ./ui-tests",
"test:unit:react:watch": "jest --config ./jest.config.js ./ui-tests --watch",
"test:unit:react:coverage": "jest --config ./jest.config.js ./ui-tests --coverage",
"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:unit: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": "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": "run-p fix-lint:*",
"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:cli": "tslint --fix --project cli/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": "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: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:cli": "tslint --project cli/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:jest": "cd ./coverage/jest && codecov",
"ccov:upload": "codecov",
"launch": "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":
"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": "yarn run watch:plugins:oni-plugin-typescript && yarn 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 watch:plugins:oni-plugin-typescript && watch:plugins:oni-plugin-markdown-preview && watch:plugins:oni-plugin-git",
"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",
"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",
"watch:plugins:oni-plugin-markdown-preview":
"cd extensions/oni-plugin-markdown-preview && tsc --watch",
"watch:plugins:oni-plugin-git": "cd vim/core/oni-plugin-git && tsc --watch",
"install:plugins": "run-s install:plugins:*",
"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",
"install:plugins:oni-plugin-git": "cd vim/core/oni-plugin-git && 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"
"profile:webpack":
"webpack --config browser/webpack.production.config.js --profile --json > stats.json && webpack-bundle-analyzer browser/stats.json"
},
"repository": {
"type": "git",
@ -874,6 +717,7 @@
"redux-batched-subscribe": "^0.1.6",
"shell-env": "^0.3.0",
"shelljs": "0.7.7",
"simple-git": "^1.92.0",
"styled-components": "^3.2.6",
"typescript": "^2.8.1",
"vscode-css-languageserver-bin": "^1.2.1",
@ -913,7 +757,7 @@
"@types/react-transition-group": "2.0.6",
"@types/react-virtualized": "^9.7.10",
"@types/redux-batched-subscribe": "^0.1.2",
"@types/redux-mock-store": "^0.0.13",
"@types/redux-mock-store": "^1.0.0",
"@types/rimraf": "^2.0.2",
"@types/shelljs": "^0.7.7",
"@types/sinon": "1.16.32",
@ -948,7 +792,7 @@
"innosetup-compiler": "5.5.9",
"istanbul-api": "^1.2.1",
"istanbul-lib-coverage": "^1.1.1",
"jest": "^22.2.2",
"jest": "^23.2.0",
"jsdom": "11.0.0",
"less": "2.7.1",
"less-loader": "^4.1.0",
@ -960,6 +804,7 @@
"mkdirp": "0.5.1",
"mocha": "3.1.2",
"node-abi": "^2.4.1",
"npm-run-all": "^4.1.3",
"nyc": "^11.4.1",
"oni-core-logging": "^1.0.0",
"oni-release-downloader": "^0.0.10",
@ -973,17 +818,17 @@
"react-transition-group": "2.2.1",
"react-virtualized": "^9.18.0",
"redux": "3.7.2",
"redux-mock-store": "^1.5.1",
"redux-mock-store": "^1.5.3",
"redux-observable": "0.17.0",
"redux-thunk": "2.2.0",
"remap-istanbul": "^0.10.1",
"reselect": "3.0.1",
"rxjs": "^5.5.8",
"rxjs": "5.5.8",
"sinon": "1.17.6",
"spectron": "^3.8.0",
"style-loader": "0.18.2",
"sudo-prompt": "7.1.1",
"ts-jest": "^22.0.4",
"ts-jest": "^23.0.0",
"ts-loader": "^4.2.0",
"tslint": "5.9.1",
"vscode-snippet-parser": "0.0.5",

View File

@ -0,0 +1,67 @@
import { mount, shallow } from "enzyme"
import { shallowToJson } from "enzyme-to-json"
import * as React from "react"
import { Branch, BranchNameContainer } from "./../browser/src/UI/components/VersionControl"
describe("<Branch />", () => {
const diff = {
insertions: 2,
deletions: 8,
files: null,
}
it("Should render without crashing", () => {
const wrapper = shallow(<Branch branch="test-branch" diff={diff} />)
expect(wrapper.length).toEqual(1)
})
it("Should render the correct branch name", () => {
const wrapper = shallow(<Branch branch="test-branch" diff={diff} />)
const name = wrapper
.find(BranchNameContainer)
.dive()
.text()
expect(name).toBe("test-branch < />")
})
it("should match last known snapshot unless we make a change", () => {
const wrapper = shallow(<Branch branch="test-branch" diff={diff} />)
expect(shallowToJson(wrapper)).toMatchSnapshot()
})
it("Should show the correct number of additions", () => {
const wrapper = mount(<Branch branch="test-branch" diff={diff} />)
const additions = wrapper.find("[data-test='addition-2']")
expect(additions.find("span").text()).toEqual("2")
})
it("Should show the correct number of deletions", () => {
const wrapper = mount(<Branch branch="test-branch" diff={diff} />)
const deletions = wrapper.find("[data-test='deletion-8']")
expect(deletions.find("span").text()).toEqual("8")
})
it("should render the correct icon for additions", () => {
const wrapper = mount(<Branch branch="test-branch" diff={diff} />)
const icon = wrapper.find("i.fa-plus-circle")
expect(icon.length).toBe(1)
})
it("should render the correct icon for deletions", () => {
const wrapper = mount(<Branch branch="test-branch" diff={diff} />)
const icon = wrapper.find("i.fa-minus-circle")
expect(icon.length).toBe(1)
})
it("Should not render an icon if there were no insertions", () => {
const wrapper = mount(<Branch branch="test-branch" diff={{ ...diff, insertions: 0 }} />)
const icon = wrapper.find("i.fa-plus-circle")
expect(icon.length).toBe(0)
})
it("Should not render an icon if there were no deletions", () => {
const wrapper = mount(<Branch branch="test-branch" diff={{ ...diff, deletions: 0 }} />)
const icon = wrapper.find("i.fa-minus-circle")
expect(icon.length).toBe(0)
})
})

View File

@ -0,0 +1,94 @@
import * as Oni from "oni-api"
import { Event } from "oni-types"
import * as React from "react"
import { Branch } from "../browser/src/UI/components/VersionControl"
import {
VersionControlManager,
VersionControlProvider,
} from "./../browser/src/Services/VersionControl"
import MockCommands from "./mocks/CommandManager"
import { configuration as MockConfiguration } from "./mocks/Configuration"
import MockEditorManager from "./mocks/EditorManager"
import MockMenu from "./mocks/MenuManager"
import MockNotifications from "./mocks/Notifications"
import MockSidebar from "./mocks/Sidebar"
import MockStatusbar, { mockStatusBarHide, mockStatusBarSetContents } from "./mocks/Statusbar"
import MockWorkspace from "./mocks/Workspace"
jest.unmock("lodash")
const makePromise = (arg?: any) => Promise.resolve(arg)
const provider: VersionControlProvider = {
name: "svn",
onFileStatusChanged: new Event(),
onBranchChanged: new Event(),
onPluginActivated: new Event(),
onPluginDeactivated: new Event(),
onStagedFilesChanged: new Event(),
isActivated: true,
fetchBranchFromRemote: () => null,
stageFile: () => null,
changeBranch: () => null,
getLocalBranches: () => makePromise(["branch1", "branch2"]),
canHandleWorkspace: () => makePromise(true),
getDiff: () => makePromise({}),
activate: () => null,
deactivate: () => null,
getStatus: () => makePromise({}),
getRoot: () => makePromise("/test/dir"),
getBranch: () => makePromise("local"),
}
describe("Version Control Manager tests", () => {
let vcsManager: VersionControlManager
beforeEach(() => {
vcsManager = new VersionControlManager(
new MockWorkspace(),
new MockEditorManager(),
new MockStatusbar(),
new MockMenu(),
new MockCommands(),
new MockSidebar(),
new MockNotifications(),
MockConfiguration,
)
vcsManager.registerProvider(provider)
})
it("Should register a vcs provider", () => {
expect(vcsManager.providers.size).toBe(1)
})
it("Should register the provider details", () => {
expect(vcsManager.activeProvider.name).toBe("svn")
})
it("should correctly deregister a provider", () => {
vcsManager.deactivateProvider()
expect(vcsManager.activeProvider).toBeFalsy()
})
it("Should correctly hide the status bar item if the dir cannot handle the workspace", () => {
provider.canHandleWorkspace = async () => makePromise(false)
vcsManager.registerProvider(provider)
expect(mockStatusBarHide.mock.calls.length).toBe(1)
})
it("should return the correct branch", async () => {
const branch = await provider.getBranch()
expect(branch).toBe("local")
})
it("Should return the correct local branches", async () => {
const localBranches = await provider.getLocalBranches()
expect(localBranches).toEqual(expect.arrayContaining(["branch1", "branch2"]))
})
it("should set the contents of the statusbar correctly", () => {
const branch = <Branch diff={{} as any} branch="local" />
expect(mockStatusBarSetContents.mock.calls[0][0]).toEqual(branch)
})
})

View File

@ -0,0 +1,69 @@
import * as Oni from "oni-api"
import { Event } from "oni-types"
import { VersionControlProvider } from "../browser/src/Services/VersionControl"
import VersionControlPane from "./../browser/src/Services/VersionControl/VersionControlPane"
import store, {
DefaultState,
VersionControlState,
} from "./../browser/src/Services/VersionControl/VersionControlStore"
import MockEditorManager from "./mocks/EditorManager"
import MockWorkspace from "./mocks/Workspace"
jest.mock("lodash/capitalize", (str: string) => str)
jest.mock("./../browser/src/Services/VersionControl/VersionControlView", () => "VersionControlView")
const makePromise = (arg?: any) => Promise.resolve(arg)
const provider: VersionControlProvider = {
name: "git",
onFileStatusChanged: new Event(),
onBranchChanged: new Event(),
onPluginActivated: new Event(),
onPluginDeactivated: new Event(),
onStagedFilesChanged: new Event(),
isActivated: true,
fetchBranchFromRemote: () => null,
stageFile: () => null,
changeBranch: () => null,
getLocalBranches: () => makePromise(["branch1", "branch2"]),
canHandleWorkspace: () => makePromise(true),
getDiff: () => makePromise({}),
activate: () => null,
deactivate: () => null,
getStatus: () =>
makePromise({
currentBranch: "master",
}),
getRoot: () => makePromise("/test/dir"),
getBranch: () => makePromise("local"),
}
describe("Version Control pane tests", () => {
const mockManager = new MockEditorManager()
const mockWorkspace = new MockWorkspace()
const vcsStore = store
const vcsPane = new VersionControlPane(
mockManager,
mockWorkspace,
provider,
args => null,
store,
)
it("Should create a new version control pane", () => {
expect(vcsPane.id).toBe("oni.sidebar.vcs")
})
it("get status should return the value expected", async () => {
const result = await vcsPane.getStatus()
if (result) {
expect(result.currentBranch).toEqual("master")
}
})
it("Correctly update the store", async () => {
await vcsPane.getStatus()
const state = store.getState()
expect(state.status.currentBranch).toBe("master")
})
})

View File

@ -0,0 +1,39 @@
import { Store } from "redux"
import store from "./../browser/src/Services/VersionControl/VersionControlStore"
describe("Version control reducer test", () => {
const vcsStore = store
it("Should correctly update the store with the vcs status", () => {
const status = {
currentBranch: "master",
staged: ["/test.txt"],
conflicted: [],
created: [],
modified: [],
remoteTrackingBranch: "origin/master",
deleted: [],
untracked: [],
ahead: null,
behind: null,
}
vcsStore.dispatch({ type: "STATUS", payload: { status } })
const state = vcsStore.getState()
expect(state.status.currentBranch).toBe("master")
expect(state.status.staged[0]).toBe("/test.txt")
})
it("should correctly update the focus state", () => {
vcsStore.dispatch({ type: "ENTER" })
const state = store.getState()
expect(state.hasFocus).toBe(true)
})
it("Should correctly register an error", () => {
vcsStore.dispatch({ type: "ERROR" })
const state = store.getState()
expect(state.hasError).toBe(true)
})
})

View File

@ -0,0 +1,104 @@
import { shallow } from "enzyme"
import { shallowToJson } from "enzyme-to-json"
import * as React from "react"
import {
DefaultState,
VersionControlState,
} from "./../browser/src/Services/VersionControl/VersionControlStore"
import {
GitStatus,
SectionTitle,
VersionControlView,
} from "./../browser/src/Services/VersionControl/VersionControlView"
const noop = () => ({})
jest.mock("./../browser/src/neovim/SharedNeovimInstance", () => ({
getInstance: () => ({
bindToMenu: () => ({
setItems: jest.fn(),
onCursorMoved: {
subscribe: jest.fn(),
},
}),
}),
}))
const makePromise = (arg?: any) => Promise.resolve(arg)
jest.mock("../browser/src/UI/components/Sneakable", () => {
const React = require("react") // tslint:disable-line
return { Sneakable: () => <div /> }
})
describe("<VersionControlView />", () => {
const state = { ...DefaultState, activated: true, hasFocus: true }
const container = shallow(<VersionControlView {...state} getStatus={() => makePromise({})} />)
it("renders without crashing", () => {
expect(container.length).toBe(1)
})
it("should render an untracked, staged and modified section", () => {
const sections = container.dive().find(GitStatus).length
expect(sections).toBe(3)
})
it("shouldn't show a section if it has no content", () => {
const wrapper = shallow(
<GitStatus
onClick={noop}
toggleVisibility={noop}
visibility={true}
titleId="modified"
selectedId="file1"
icon="M"
files={null}
/>,
)
expect(wrapper.find(SectionTitle).length).toBe(0)
})
it("should match the last recorded snapshot unless a change was made", () => {
const wrapper = shallow(
<GitStatus
titleId="modified"
visibility={true}
toggleVisibility={noop}
onClick={noop}
selectedId="file1"
icon="M"
files={["test1", "test2"]}
/>,
)
expect(shallowToJson(wrapper)).toMatchSnapshot()
})
it("should render the correct number of modified files from the store in the correct section from of the pane", () => {
const stateCopy = {
...DefaultState,
activated: true,
hasFocus: true,
status: {
currentBranch: null,
staged: [],
conflicted: [],
created: [],
modified: ["test1", "test2"],
remoteTrackingBranch: null,
deleted: [],
untracked: [],
ahead: null,
behind: null,
},
}
const statusComponent = shallow(
<VersionControlView {...stateCopy} getStatus={() => makePromise({})} />,
)
.dive()
.findWhere(component => component.prop("titleId") === "modified")
expect(statusComponent.prop("files").length).toBe(2)
})
})

View File

@ -0,0 +1,18 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<Branch /> should match last known snapshot unless we make a change 1`] = `
<styled.div>
<styled.span>
<Icon
name="code-fork"
/>
<styled.span>
test-branch
<Component
deletions={8}
insertions={2}
/>
</styled.span>
</styled.span>
</styled.div>
`;

View File

@ -0,0 +1,57 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<VersionControlView /> should match the last recorded snapshot unless a change was made 1`] = `
<div>
<styled.div
data-test="modified-2"
isSelected={false}
onClick={[Function]}
>
<Caret
active={true}
/>
<styled.h4>
MODIFIED
</styled.h4>
<strong>
2
</strong>
</styled.div>
<Sneakable
callback={[Function]}
key="test1"
>
<styled.div
isSelected={false}
onClick={[Function]}
>
<styled.div>
<Icon
name="M"
/>
<styled.span>
test1
</styled.span>
</styled.div>
</styled.div>
</Sneakable>
<Sneakable
callback={[Function]}
key="test2"
>
<styled.div
isSelected={false}
onClick={[Function]}
>
<styled.div>
<Icon
name="M"
/>
<styled.span>
test2
</styled.span>
</styled.div>
</styled.div>
</Sneakable>
</div>
`;

View File

@ -1,9 +1,9 @@
// tslint:disable
import * as Enzyme from "enzyme"
import Adapter from "enzyme-adapter-react-16"
import { configure } from "enzyme"
import * as Adapter from "enzyme-adapter-react-16"
// React 16 Enzyme adapter
Enzyme.configure({ adapter: new Adapter() })
configure({ adapter: new Adapter() })
// Make Enzyme functions available in all test files without importing
// ;(global as any).shallow = shallow

View File

@ -0,0 +1,8 @@
import { CommandManager } from "./../../browser/src/Services/CommandManager"
export const mockRegisterCommands = jest.fn()
const MockCommands = jest.fn<CommandManager>().mockImplementation(() => ({
registerCommand: mockRegisterCommands,
}))
export default MockCommands

View File

@ -1,5 +1,12 @@
const Configuration = jest.fn().mockImplementation(() => {
import * as Oni from "oni-api"
const Configuration = jest.fn<Oni.Configuration>().mockImplementation(() => {
return {
onConfigurationChanged() {
return {
subscribe: jest.fn(),
}
},
notifyListeners: jest.fn(),
updateConfig: jest.fn(),
getValue: jest.fn(),

View File

@ -0,0 +1,16 @@
import { Event } from "oni-types"
import { EditorManager } from "./../../browser/src/Services/EditorManager"
const _onBufferSaved = new Event<any>("Test:ActiveEditor-BufferSaved")
const _onBufferEnter = new Event<any>("Test:ActiveEditor-BufferEnter")
const MockEditorManager = jest.fn<EditorManager>().mockImplementation(() => ({
activeEditor: {
activeBuffer: {
filePath: "test.txt",
},
onBufferEnter: _onBufferEnter,
onBufferSaved: _onBufferSaved,
},
}))
export default MockEditorManager

View File

@ -0,0 +1,18 @@
import { MenuManager } from "./../../browser/src/Services/Menu"
export const mockMenuShow = jest.fn()
const MockMenu = jest.fn<MenuManager>().mockImplementation(() => ({
create() {
return {
show: mockMenuShow,
setItems(items: {}) {
return items
},
onItemSelected: {
subscribe: jest.fn(),
},
}
},
}))
export default MockMenu

View File

@ -0,0 +1,4 @@
import { Notifications } from "./../../browser/src/Services/Notifications"
const MockNotifications = jest.fn<Notifications>().mockImplementation(() => ({}))
export default MockNotifications

11
ui-tests/mocks/Sidebar.ts Normal file
View File

@ -0,0 +1,11 @@
import { SidebarManager } from "./../../browser/src/Services/Sidebar"
const MockSidebar = jest.fn<SidebarManager>().mockImplementation(() => ({
entries: [
{
id: "git-vcs",
},
],
}))
export default MockSidebar

View File

@ -0,0 +1,19 @@
import * as Oni from "oni-api"
export const mockStatusBarShow = jest.fn()
export const mockStatusBarHide = jest.fn()
export const mockStatusBarSetContents = jest.fn()
export const mockStatusBarDisposal = jest.fn()
const MockStatusbar = jest.fn<Oni.StatusBar>().mockImplementation(() => ({
createItem(alignment: number, vcsId: string) {
return {
show: mockStatusBarShow,
hide: mockStatusBarHide,
setContents: mockStatusBarSetContents,
dispose: mockStatusBarDisposal,
}
},
}))
export default MockStatusbar

View File

@ -0,0 +1,13 @@
import { IWorkspace, Workspace } from "./../../browser/src/Services/Workspace"
const MockWorkspace = jest.fn<IWorkspace>().mockImplementation(() => ({
activeDirectory: "test/dir",
onDirectoryChanged: {
subscribe: jest.fn(),
},
onFocusGained: {
subscribe: jest.fn(),
},
}))
export default MockWorkspace

View File

@ -20,14 +20,9 @@
"suppressImplicitAnyIndexErrors": true,
"target": "es2015",
"sourceMap": true,
"inlineSourceMap": true,
"types": ["jest", "electron", "react", "webgl2"]
},
"include": [
"**/*.tsx",
"**/*.ts",
"../@types/**/*.d.ts",
"../browser/**/*.ts",
"../browser/**/*.tsx"
],
"include": ["**/*.tsx", "**/*.ts", "../browser/**/*.ts", "../browser/**/*.tsx"],
"exclude": ["node_modules"]
}

View File

@ -1,97 +0,0 @@
const fs = require("fs")
const path = require("path")
const { promisify } = require("util")
const fsStat = promisify(fs.stat)
const activate = Oni => {
const React = Oni.dependencies.React
let isLoaded = false
try {
const updateBranchIndicator = async evt => {
if (!evt) {
return
}
const filePath = evt.filePath || evt.bufferFullPath
const gitId = "oni.status.git"
const gitBranchIndicator = Oni.statusBar.createItem(1, gitId)
isLoaded = true
let dir
try {
const isDir = await Oni.workspace.pathIsDir(filePath)
const dir = isDir ? filePath : path.dirname(filePath)
let branchName
try {
branchName = await Oni.services.git.getBranch(dir)
} catch (e) {
gitBranchIndicator.hide()
return
// return console.warn('[Oni.plugin.git]: No branch name found', e);
// branchName = 'Not a Git Repo';
}
const props = {
style: {
height: "100%",
width: "100%",
display: "flex",
alignItems: "center",
},
}
const branchContainerProps = {
style: {
minWidth: "10px",
textAlign: "center",
padding: "2px 4px 0 0",
},
}
const branchIcon = Oni.ui.createIcon({
name: "code-fork",
size: Oni.ui.iconSize.Default,
})
const branchContainer = React.createElement(
"span",
branchContainerProps,
branchIcon,
)
const branchNameContainer = React.createElement(
"div",
{ width: "100%" },
" " + branchName,
)
const gitBranch = React.createElement(
"div",
props,
branchContainer,
branchNameContainer,
)
gitBranchIndicator.setContents(gitBranch)
gitBranchIndicator.show()
} catch (e) {
console.log("[Oni.plugin.git]: ", e)
return gitBranchIndicator.hide()
}
}
if (!isLoaded) {
updateBranchIndicator(Oni.editors.activeEditor.activeBuffer)
}
Oni.editors.activeEditor.onBufferEnter.subscribe(
async evt => await updateBranchIndicator(evt),
)
Oni.workspace.onFocusGained.subscribe(async buffer => await updateBranchIndicator(buffer))
} catch (e) {
console.warn("[Oni.plugin.git] ERROR", e)
}
}
module.exports = {
activate,
}

View File

@ -1,14 +1,25 @@
{
"name": "oni-plugin-git",
"version": "1.0.0",
"main": "index.js",
"main": "lib/index.js",
"engines": {
"oni": "^0.2.6"
},
"scripts": {},
"oni": {
"supportedFileTypes": ["*"]
"scripts": {
"build": "rimraf lib && tsc",
"test": "tsc -p tsconfig.test.json && mocha --recursive ./lib_test/test"
},
"dependencies": {},
"devDependencies": {}
"oni": {
"supportedFileTypes": [
"*"
]
},
"dependencies": {
"oni-api": "^0.0.46",
"oni-types": "^0.0.8",
"simple-git": "^1.96.0"
},
"devDependencies": {
"typescript": "^2.9.2"
}
}

View File

@ -0,0 +1,220 @@
/**
* Git.ts
*
*/
import * as Oni from "oni-api"
import { Event, IEvent } from "oni-types"
import * as GitP from "simple-git/promise"
import * as VCS from "./vcs" // TODO: import from oni-api
interface FileSummary {
index: string
path: string
working_dir: string
}
export class GitVersionControlProvider implements VCS.VersionControlProvider {
private readonly _name = "git"
private _onPluginActivated = new Event<void>("Oni::VCSPluginActivated")
private _onPluginDeactivated = new Event<void>("Oni::VCSPluginDeactivated")
private _onBranchChange = new Event<VCS.BranchChangedEvent>("Oni::VCSBranchChanged")
private _onStagedFilesChanged = new Event<VCS.StagedFilesChangedEvent>(
"Oni::VCSStagedFilesChanged",
)
private _onFileStatusChanged = new Event<VCS.FileStatusChangedEvent>(
"Oni::VCSFilesStatusChanged",
)
private _isActivated = false
private _projectRoot: string
constructor(private _oni: Oni.Plugin.Api, private _git = GitP) {
this._projectRoot = this._oni.workspace.activeWorkspace
this._oni.workspace.onDirectoryChanged.subscribe(workspace => {
this._projectRoot = workspace
})
}
get onBranchChanged(): IEvent<VCS.BranchChangedEvent> {
return this._onBranchChange
}
get onFileStatusChanged(): IEvent<VCS.FileStatusChangedEvent> {
return this._onFileStatusChanged
}
get onStagedFilesChanged(): IEvent<VCS.StagedFilesChangedEvent> {
return this._onStagedFilesChanged
}
get onPluginActivated(): IEvent<void> {
return this._onPluginActivated
}
get onPluginDeactivated(): IEvent<void> {
return this._onPluginDeactivated
}
get isActivated(): boolean {
return this._isActivated
}
get name(): VCS.SupportedProviders {
return this._name
}
public activate() {
this._isActivated = true
this._onPluginActivated.dispatch()
}
public deactivate() {
this._isActivated = false
this._onPluginDeactivated.dispatch()
}
public async canHandleWorkspace(dir?: string) {
try {
return this._git(this._projectRoot)
.silent()
.checkIsRepo()
} catch (e) {
this._oni.log.warn(
`Git provider was unable to check if this directory is a repository because ${
e.message
}`,
)
return false
}
}
public async getRoot() {
try {
return this._git(this._projectRoot).revparse(["--show-toplevel"])
} catch (e) {
this._oni.log.warn(`Git provider unable to find vcs root due to ${e.message}`)
}
}
public getStatus = async (): Promise<VCS.StatusResult | void> => {
try {
const status = await this._git(this._projectRoot).status()
const { modified, staged } = this._getModifiedAndStaged(status.files)
return {
staged,
modified,
ahead: status.ahead,
behind: status.behind,
created: status.created,
deleted: status.deleted,
currentBranch: status.current,
conflicted: status.conflicted,
untracked: status.not_added,
remoteTrackingBranch: status.tracking,
}
} catch (error) {
this._oni.log.warn(
`Git provider unable to get current status because of: ${error.message}`,
)
}
}
public getDiff = async () => {
try {
return this._git(this._projectRoot).diffSummary()
} catch (e) {
const error = `Git provider unable to get current status because of: ${e.message}`
this._oni.log.warn(error)
throw new Error(error)
}
}
public stageFile = async (file: string, dir?: string) => {
try {
await this._git(this._projectRoot).add(file)
this._onStagedFilesChanged.dispatch(file)
this._onFileStatusChanged.dispatch({ path: file, status: "staged" })
} catch (e) {
const error = `Git provider unable to add ${file} because ${e.message}`
this._oni.log.warn(error)
throw new Error(error)
}
}
public fetchBranchFromRemote = async ({
branch,
currentDir,
remote = null,
}: {
branch: string
remote: string
currentDir: string
}) => {
try {
return this._git(this._projectRoot).fetch(remote, branch)
} catch (e) {
const error = `Git provider unable to fetch branch because of: ${e.message}`
this._oni.log.warn(error)
throw new Error(error)
}
}
public getLocalBranches = async () => {
try {
return this._git(this._projectRoot).branchLocal()
} catch (e) {
const error = `Git provider unable to get local branches because of: ${e.message}`
this._oni.log.warn(error)
throw new Error(error)
}
}
public getBranch = async (): Promise<string | void> => {
try {
const status = await this._git(this._projectRoot).status()
return status.current
} catch (e) {
const error = `Git Provider was unable to get current status because of: ${e.message}`
this._oni.log.warn(error)
throw new Error(error)
}
}
public async changeBranch(targetBranch: string) {
try {
await this._git(this._projectRoot).checkout(targetBranch)
this._onBranchChange.dispatch(targetBranch)
} catch (e) {
const error = `Git Provider was unable change branch because of: ${e.message}`
this._oni.log.warn(error)
throw new Error(error)
}
}
private _isStaged = (file: FileSummary) => {
const GitPIndicators = ["M", "A"]
return GitPIndicators.some(status => file.index.includes(status))
}
private _getModifiedAndStaged(files: FileSummary[]): { modified: string[]; staged: string[] } {
return files.reduce(
(acc, file) => {
if (file.working_dir === "M") {
acc.modified.push(file.path)
} else if (this._isStaged(file)) {
acc.staged.push(file.path)
}
return acc
},
{ modified: [], staged: [] },
)
}
}
export const activate = async oni => {
const provider = new GitVersionControlProvider(oni)
await oni.services.vcs.registerProvider(provider)
return provider
}

74
vim/core/oni-plugin-git/src/vcs.d.ts vendored Normal file
View File

@ -0,0 +1,74 @@
import { IEvent } from "oni-types"
import { BranchSummary, FetchResult } from "simple-git/promise"
export type BranchChangedEvent = string
export type StagedFilesChangedEvent = string
export interface FileStatusChangedEvent {
path: string
status: "staged"
}
export interface StatusResult {
ahead: number
behind: number
currentBranch: string
modified: string[]
staged: string[]
conflicted: string[]
created: string[]
deleted: string[]
untracked: string[]
remoteTrackingBranch: string
}
export interface VersionControlProvider {
// Events
onFileStatusChanged: IEvent<FileStatusChangedEvent>
onStagedFilesChanged: IEvent<StagedFilesChangedEvent>
onBranchChanged: IEvent<BranchChangedEvent>
onPluginActivated: IEvent<void>
onPluginDeactivated: IEvent<void>
name: SupportedProviders
isActivated: boolean
deactivate(): void
activate(): void
canHandleWorkspace(dir?: string): Promise<boolean>
getStatus(): Promise<StatusResult | void>
getRoot(): Promise<string | void>
getDiff(): Promise<Diff | void>
getBranch(): Promise<string | void>
getLocalBranches(): Promise<BranchSummary | void>
changeBranch(branch: string): Promise<void>
stageFile(file: string, projectRoot?: string): Promise<void>
fetchBranchFromRemote(args: {
branch: string
origin?: string
currentDir: string
}): Promise<FetchResult>
}
export interface DiffResultTextFile {
file: string
changes: number
insertions: number
deletions: number
binary: boolean
}
export interface DiffResultBinaryFile {
file: string
before: number
after: number
binary: boolean
}
export interface Diff {
files: Array<DiffResultTextFile | DiffResultBinaryFile>
insertions: number
deletions: number
}
export type Summary = StatusResult
export type SupportedProviders = "git" | "svn"
export default VersionControlProvider

View File

@ -0,0 +1,16 @@
{
"compilerOptions": {
"module": "commonjs",
"moduleResolution": "node",
"preserveConstEnums": true,
"outDir": "./lib",
"jsx": "react",
"lib": ["dom", "es2017"],
"declaration": true,
"sourceMap": true,
"target": "es2015",
"skipLibCheck": true
},
"include": ["src/**/*.ts", "src/**/*.tsx"],
"exclude": ["node_modules"]
}

879
yarn.lock

File diff suppressed because it is too large Load Diff