mirror of https://github.com/onivim/oni.git
Feature/oni sessions (#2479)
* add session manager, store and sidebar pane * hook up session services to app * move side effects into epics * add creation and cancel creation actions * add restore session epic and remove console.log * await sessions in case of error * add error handling to session functions * Revert "Refocus previously open menu on reactivating Oni (#2472)" This reverts commit97f0c61345
. * Revert "Refocus previously open menu on reactivating Oni (#2472)" This reverts commit97f0c61345
. * remove console.log * remove unused props passed to session pane * add persist session command [WIP] * Add current session action and update store with i t * use get user config add icons and some error handling * add unclick handlers for sessions move section title to general location add delete session functionality * add title and toggling of sessions add on vim leave handler * fix lint errors * refactor epics in preparation for 1.0 and rxjs 6 * update snapshot * add bufdo bd command prior to restoring session fix delete session bug causing reappearances * create separate close all buffers method * remove update session method * Add audit time to update current session * add close all buffers method and use persist session action in update session * add restore session error action and complete action * add console log for debugging return metadata directly * add error handling to git blame function * reduce persist audit time make ids in session.tsx readonly * comment out console.log * check neovim for current session when updating this ensures the session is valid added getCurrentSession method which checks vims v:this_session var * fix lint errors * add tests for sessions component and mock for neovim instance * switch generic input check to specific text input view check * add update timestamp functionality so these are more accurate * switch to adding updated at by checking mtime of f ile * switch to storing sessions in persistent store * add delete functionality to persistent store and mock * fix lint error * rename sessionName var to name for simplicity * add session manager initial test * create path using path.join * add experimental config flag * update Oni mock and increase sessionmanager tests * add session store tests - [WIP] * return simple session manager mock remove need for instantiation, use done callback * remove session store tests as redux observable api has changed a large refactor of all observables is going to be required to upgrade redux observable so seems counter productive to write tests that will need to be re-written entirely once that is done * add user configurable session directory * tweak sidebar item styles * update vim navigator to pass function to update selected item on click render session sidebar item second * fix lint error * fix broken tests
This commit is contained in:
parent
2fd82f36cf
commit
e3f65e6948
|
@ -75,6 +75,7 @@ export const start = async (args: string[]): Promise<void> => {
|
|||
const themesPromise = import("./Services/Themes")
|
||||
const iconThemesPromise = import("./Services/IconThemes")
|
||||
|
||||
const sessionManagerPromise = import("./Services/Sessions")
|
||||
const sidebarPromise = import("./Services/Sidebar")
|
||||
const overlayPromise = import("./Services/Overlay")
|
||||
const statusBarPromise = import("./Services/StatusBar")
|
||||
|
@ -327,6 +328,10 @@ export const start = async (args: string[]): Promise<void> => {
|
|||
Sidebar.getInstance(),
|
||||
WindowManager.windowManager,
|
||||
)
|
||||
|
||||
const Sessions = await sessionManagerPromise
|
||||
Sessions.activate(oniApi, sidebarManager)
|
||||
|
||||
Performance.endMeasure("Oni.Start.Sidebar")
|
||||
|
||||
const createLanguageClientsFromConfiguration =
|
||||
|
|
|
@ -44,6 +44,7 @@ import { Completion, CompletionProviders } from "./../../Services/Completion"
|
|||
import { Configuration, IConfigurationValues } from "./../../Services/Configuration"
|
||||
import { IDiagnosticsDataSource } from "./../../Services/Diagnostics"
|
||||
import { Overlay, OverlayManager } from "./../../Services/Overlay"
|
||||
import { ISession } from "./../../Services/Sessions"
|
||||
import { SnippetManager } from "./../../Services/Snippets"
|
||||
import { TokenColors } from "./../../Services/TokenColors"
|
||||
|
||||
|
@ -99,6 +100,8 @@ import { CanvasRenderer } from "../../Renderer/CanvasRenderer"
|
|||
import { WebGLRenderer } from "../../Renderer/WebGL/WebGLRenderer"
|
||||
import { getInstance as getNotificationsInstance } from "./../../Services/Notifications"
|
||||
|
||||
type NeovimError = [number, string]
|
||||
|
||||
export class NeovimEditor extends Editor implements Oni.Editor {
|
||||
private _bufferManager: BufferManager
|
||||
private _neovimInstance: NeovimInstance
|
||||
|
@ -889,6 +892,31 @@ export class NeovimEditor extends Editor implements Oni.Editor {
|
|||
)
|
||||
}
|
||||
|
||||
// "v:this_session" |this_session-variable| - is a variable nvim sets to the path of
|
||||
// the current session file when one is loaded we use it here to check the current session
|
||||
// if it in oni's session dir then this is updated
|
||||
public async getCurrentSession(): Promise<string | void> {
|
||||
const result = await this._neovimInstance.request<string | NeovimError>("nvim_get_vvar", [
|
||||
"this_session",
|
||||
])
|
||||
|
||||
if (Array.isArray(result)) {
|
||||
return this._handleNeovimError(result)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
public async persistSession(session: ISession) {
|
||||
const result = await this._neovimInstance.command(`mksession! ${session.file}`)
|
||||
return this._handleNeovimError(result)
|
||||
}
|
||||
|
||||
public async restoreSession(session: ISession) {
|
||||
await this._neovimInstance.closeAllBuffers()
|
||||
const result = await this._neovimInstance.command(`source ${session.file}`)
|
||||
return this._handleNeovimError(result)
|
||||
}
|
||||
|
||||
public async openFile(
|
||||
file: string,
|
||||
openOptions: Oni.FileOpenOptions = Oni.DefaultFileOpenOptions,
|
||||
|
@ -1295,4 +1323,16 @@ export class NeovimEditor extends Editor implements Oni.Editor {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _handleNeovimError(result: NeovimError | void): void {
|
||||
if (!result) {
|
||||
return null
|
||||
}
|
||||
// the first value of the error response is a 0
|
||||
if (Array.isArray(result) && !result[0]) {
|
||||
const [, error] = result
|
||||
Log.warn(error)
|
||||
throw new Error(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -49,6 +49,7 @@ import { NeovimEditor } from "./../NeovimEditor"
|
|||
|
||||
import { SplitDirection, windowManager } from "./../../Services/WindowManager"
|
||||
|
||||
import { ISession } from "../../Services/Sessions"
|
||||
import { IBuffer } from "../BufferManager"
|
||||
import ColorHighlightLayer from "./ColorHighlightLayer"
|
||||
import { ImageBufferLayer } from "./ImageBufferLayer"
|
||||
|
@ -101,6 +102,10 @@ export class OniEditor extends Utility.Disposable implements Oni.Editor {
|
|||
return this._neovimEditor.activeBuffer
|
||||
}
|
||||
|
||||
public get onQuit(): IEvent<void> {
|
||||
return this._neovimEditor.onNeovimQuit
|
||||
}
|
||||
|
||||
// Capabilities
|
||||
public get neovim(): Oni.NeovimEditorCapability {
|
||||
return this._neovimEditor.neovim
|
||||
|
@ -288,6 +293,18 @@ export class OniEditor extends Utility.Disposable implements Oni.Editor {
|
|||
this._neovimEditor.executeCommand(command)
|
||||
}
|
||||
|
||||
public restoreSession(sessionDetails: ISession) {
|
||||
return this._neovimEditor.restoreSession(sessionDetails)
|
||||
}
|
||||
|
||||
public getCurrentSession() {
|
||||
return this._neovimEditor.getCurrentSession()
|
||||
}
|
||||
|
||||
public persistSession(sessionDetails: ISession) {
|
||||
return this._neovimEditor.persistSession(sessionDetails)
|
||||
}
|
||||
|
||||
public getBuffers(): Array<Oni.Buffer | Oni.InactiveBuffer> {
|
||||
return this._neovimEditor.getBuffers()
|
||||
}
|
||||
|
|
|
@ -35,6 +35,7 @@ export const applyDefaultKeyBindings = (oni: Oni.Plugin.Api, config: Configurati
|
|||
!isMenuOpen()
|
||||
|
||||
const isExplorerActive = () => isSidebarPaneOpen("oni.sidebar.explorer")
|
||||
const areSessionsActive = () => isSidebarPaneOpen("oni.sidebar.sessions")
|
||||
const isVCSActive = () => isSidebarPaneOpen("oni.sidebar.vcs")
|
||||
|
||||
const isMenuOpen = () => menu.isMenuOpen()
|
||||
|
@ -168,4 +169,7 @@ export const applyDefaultKeyBindings = (oni: Oni.Plugin.Api, config: Configurati
|
|||
input.bind("u", "vcs.unstage", isVCSActive)
|
||||
input.bind("<c-r>", "vcs.refresh", isVCSActive)
|
||||
input.bind("?", "vcs.showHelp", isVCSActive)
|
||||
|
||||
// Sessions
|
||||
input.bind("<c-d>", "oni.sessions.delete", areSessionsActive)
|
||||
}
|
||||
|
|
|
@ -15,6 +15,8 @@ const PersistentSettings = remote.require("electron-settings")
|
|||
export interface IPersistentStore<T> {
|
||||
get(): Promise<T>
|
||||
set(value: T): Promise<void>
|
||||
delete(key: string): Promise<T>
|
||||
has(key: string): boolean
|
||||
}
|
||||
|
||||
export const getPersistentStore = <T>(
|
||||
|
@ -70,4 +72,12 @@ export class PersistentStore<T> implements IPersistentStore<T> {
|
|||
|
||||
PersistentSettings.set(this._storeKey, JSON.stringify(this._currentValue))
|
||||
}
|
||||
|
||||
public has(key: string) {
|
||||
return PersistentSettings.has(key)
|
||||
}
|
||||
|
||||
public async delete(key: string) {
|
||||
return PersistentSettings.delete(`${this._storeKey}.${key}`)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -53,10 +53,12 @@ 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,
|
||||
"experimental.particles.enabled": false,
|
||||
"experimental.sessions.enabled": false,
|
||||
"experimental.sessions.directory": null,
|
||||
"experimental.vcs.sidebar": false,
|
||||
"experimental.vcs.blame.enabled": false,
|
||||
"experimental.vcs.blame.mode": "auto",
|
||||
"experimental.vcs.blame.timeout": 800,
|
||||
|
|
|
@ -48,7 +48,10 @@ export interface IConfigurationValues {
|
|||
|
||||
// Whether or not the learning pane is available
|
||||
"experimental.particles.enabled": boolean
|
||||
|
||||
// Whether or not the sessions sidebar pane is enabled
|
||||
"experimental.sessions.enabled": boolean
|
||||
// A User specified directory for where Oni session files should be saved
|
||||
"experimental.sessions.directory": string
|
||||
// Whether Version control sidebar item is enabled
|
||||
"experimental.vcs.sidebar": boolean
|
||||
// Whether the color highlight layer is enabled
|
||||
|
|
|
@ -0,0 +1,179 @@
|
|||
import * as fs from "fs-extra"
|
||||
import { Editor, EditorManager, Plugin } from "oni-api"
|
||||
import { IEvent } from "oni-types"
|
||||
import * as path from "path"
|
||||
|
||||
import { SidebarManager } from "../Sidebar"
|
||||
import { SessionActions, SessionsPane, store } from "./"
|
||||
import { getPersistentStore, IPersistentStore } from "./../../PersistentStore"
|
||||
import { getUserConfigFolderPath } from "./../../Services/Configuration/UserConfiguration"
|
||||
|
||||
export interface ISession {
|
||||
name: string
|
||||
id: string
|
||||
file: string
|
||||
directory: string
|
||||
updatedAt?: string
|
||||
workspace: string
|
||||
// can be use to save other metadata for restoration like statusbar info or sidebar info etc
|
||||
metadata?: { [key: string]: any }
|
||||
}
|
||||
|
||||
export interface ISessionService {
|
||||
sessionsDir: string
|
||||
sessions: ISession[]
|
||||
persistSession(sessionName: string): Promise<ISession>
|
||||
restoreSession(sessionName: string): Promise<ISession>
|
||||
}
|
||||
|
||||
export interface UpdatedOni extends Plugin.Api {
|
||||
editors: UpdatedEditorManager
|
||||
}
|
||||
|
||||
interface UpdatedEditorManager extends EditorManager {
|
||||
activeEditor: UpdatedEditor
|
||||
}
|
||||
|
||||
interface UpdatedEditor extends Editor {
|
||||
onQuit: IEvent<void>
|
||||
persistSession(sessionDetails: ISession): Promise<ISession>
|
||||
restoreSession(sessionDetails: ISession): Promise<ISession>
|
||||
getCurrentSession(): Promise<string | void>
|
||||
}
|
||||
|
||||
/**
|
||||
* Class SessionManager
|
||||
*
|
||||
* Provides a service to manage oni session i.e. buffers, screen layout etc.
|
||||
*
|
||||
*/
|
||||
export class SessionManager implements ISessionService {
|
||||
private _store = store({ sessionManager: this, fs })
|
||||
private get _sessionsDir() {
|
||||
const defaultDirectory = path.join(getUserConfigFolderPath(), "sessions")
|
||||
const userDirectory = this._oni.configuration.getValue<string>(
|
||||
"experimental.sessions.directory",
|
||||
)
|
||||
const directory = userDirectory || defaultDirectory
|
||||
return directory
|
||||
}
|
||||
|
||||
constructor(
|
||||
private _oni: UpdatedOni,
|
||||
private _sidebarManager: SidebarManager,
|
||||
private _persistentStore: IPersistentStore<{ [sessionName: string]: ISession }>,
|
||||
) {
|
||||
fs.ensureDirSync(this.sessionsDir)
|
||||
const enabled = this._oni.configuration.getValue<boolean>("experimental.sessions.enabled")
|
||||
if (enabled) {
|
||||
this._sidebarManager.add(
|
||||
"save",
|
||||
new SessionsPane({ store: this._store, commands: this._oni.commands }),
|
||||
)
|
||||
}
|
||||
this._setupSubscriptions()
|
||||
}
|
||||
|
||||
public get sessions() {
|
||||
return this._store.getState().sessions
|
||||
}
|
||||
|
||||
public get sessionsDir() {
|
||||
return this._sessionsDir
|
||||
}
|
||||
|
||||
public async updateOniSession(name: string, value: Partial<ISession>) {
|
||||
const persistedSessions = await this._persistentStore.get()
|
||||
if (name in persistedSessions) {
|
||||
this._persistentStore.set({
|
||||
...persistedSessions,
|
||||
[name]: { ...persistedSessions[name], ...value },
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
public async createOniSession(sessionName: string) {
|
||||
const persistedSessions = await this._persistentStore.get()
|
||||
const file = this._getSessionFilename(sessionName)
|
||||
|
||||
const session: ISession = {
|
||||
file,
|
||||
id: sessionName,
|
||||
name: sessionName,
|
||||
directory: this.sessionsDir,
|
||||
workspace: this._oni.workspace.activeWorkspace,
|
||||
metadata: null,
|
||||
}
|
||||
|
||||
this._persistentStore.set({ ...persistedSessions, [sessionName]: session })
|
||||
|
||||
return session
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve or Create a persistent Oni Session
|
||||
*
|
||||
* @name getSessionFromStore
|
||||
* @function
|
||||
* @param {string} sessionName The name of the session
|
||||
* @returns {ISession} The session metadata object
|
||||
*/
|
||||
public async getSessionFromStore(name: string) {
|
||||
const sessions = await this._persistentStore.get()
|
||||
if (name in sessions) {
|
||||
return sessions[name]
|
||||
}
|
||||
return this.createOniSession(name)
|
||||
}
|
||||
|
||||
public persistSession = async (sessionName: string) => {
|
||||
const sessionDetails = await this.getSessionFromStore(sessionName)
|
||||
await this._oni.editors.activeEditor.persistSession(sessionDetails)
|
||||
return sessionDetails
|
||||
}
|
||||
|
||||
public deleteSession = async (sessionName: string) => {
|
||||
await this._persistentStore.delete(sessionName)
|
||||
}
|
||||
|
||||
public getCurrentSession = async () => {
|
||||
const filepath = await this._oni.editors.activeEditor.getCurrentSession()
|
||||
if (!filepath) {
|
||||
return null
|
||||
}
|
||||
const [name] = path.basename(filepath).split(".")
|
||||
return filepath.includes(this._sessionsDir) ? this.getSessionFromStore(name) : null
|
||||
}
|
||||
|
||||
public restoreSession = async (name: string) => {
|
||||
const sessionDetails = await this.getSessionFromStore(name)
|
||||
await this._oni.editors.activeEditor.restoreSession(sessionDetails)
|
||||
const session = await this.getCurrentSession()
|
||||
return session
|
||||
}
|
||||
|
||||
private _getSessionFilename(name: string) {
|
||||
return path.join(this.sessionsDir, `${name}.vim`)
|
||||
}
|
||||
|
||||
private _setupSubscriptions() {
|
||||
this._oni.editors.activeEditor.onBufferEnter.subscribe(() => {
|
||||
this._store.dispatch(SessionActions.updateCurrentSession())
|
||||
})
|
||||
this._oni.editors.activeEditor.onQuit.subscribe(() => {
|
||||
this._store.dispatch(SessionActions.updateCurrentSession())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function init() {
|
||||
let instance: SessionManager
|
||||
return {
|
||||
getInstance: () => instance,
|
||||
activate: (oni: Plugin.Api, sidebarManager: SidebarManager) => {
|
||||
const persistentStore = getPersistentStore("sessions", {}, 1)
|
||||
instance = new SessionManager(oni as UpdatedOni, sidebarManager, persistentStore)
|
||||
},
|
||||
}
|
||||
}
|
||||
export const { activate, getInstance } = init()
|
|
@ -0,0 +1,228 @@
|
|||
import * as path from "path"
|
||||
import * as React from "react"
|
||||
import { connect } from "react-redux"
|
||||
|
||||
import SectionTitle from "../../UI/components/SectionTitle"
|
||||
import { Icon } from "../../UI/Icon"
|
||||
|
||||
import styled, { css, sidebarItemSelected, withProps } from "../../UI/components/common"
|
||||
import TextInputView from "../../UI/components/LightweightText"
|
||||
import { VimNavigator } from "../../UI/components/VimNavigator"
|
||||
import { getTimeSince } from "../../Utility"
|
||||
import { ISession, ISessionState, SessionActions } from "./"
|
||||
|
||||
interface IStateProps {
|
||||
sessions: ISession[]
|
||||
active: boolean
|
||||
creating: boolean
|
||||
selected: ISession
|
||||
}
|
||||
|
||||
interface ISessionActions {
|
||||
populateSessions: () => void
|
||||
updateSelection: (selected: string) => void
|
||||
getAllSessions: (sessions: ISession[]) => void
|
||||
updateSession: (session: ISession) => void
|
||||
restoreSession: (session: string) => void
|
||||
persistSession: (session: string) => void
|
||||
createSession: () => void
|
||||
cancelCreating: () => void
|
||||
}
|
||||
|
||||
interface IConnectedProps extends IStateProps, ISessionActions {}
|
||||
|
||||
interface ISessionItem {
|
||||
session: ISession
|
||||
isSelected: boolean
|
||||
onClick: () => void
|
||||
}
|
||||
|
||||
export const Container = styled.div`
|
||||
padding: 0 1em;
|
||||
`
|
||||
|
||||
const SessionItem: React.SFC<ISessionItem> = ({ session, isSelected, onClick }) => {
|
||||
const truncatedWorkspace = session.workspace
|
||||
.split(path.sep)
|
||||
.slice(-2)
|
||||
.join(path.sep)
|
||||
|
||||
return (
|
||||
<ListItem isSelected={isSelected} onClick={onClick}>
|
||||
<div>
|
||||
<strong>
|
||||
<Icon name="file" /> Name: {session.name}
|
||||
</strong>
|
||||
</div>
|
||||
<div>Workspace: {truncatedWorkspace}</div>
|
||||
{<div>Last updated: {getTimeSince(new Date(session.updatedAt))} ago</div>}
|
||||
</ListItem>
|
||||
)
|
||||
}
|
||||
|
||||
const inputStyles = css`
|
||||
background-color: transparent;
|
||||
width: 100%;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
color: ${p => p.theme["sidebar.foreground"]};
|
||||
`
|
||||
|
||||
const ListItem = withProps<Partial<ISessionItem>>(styled.li)`
|
||||
box-sizing: border-box;
|
||||
padding: 0.5em 1em;
|
||||
${sidebarItemSelected};
|
||||
`
|
||||
|
||||
const List = styled.ul`
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
`
|
||||
|
||||
interface IState {
|
||||
sessionName: string
|
||||
showAll: boolean
|
||||
}
|
||||
|
||||
interface IIDs {
|
||||
input: string
|
||||
title: string
|
||||
}
|
||||
|
||||
export class Sessions extends React.PureComponent<IConnectedProps, IState> {
|
||||
public readonly _ID: Readonly<IIDs> = {
|
||||
input: "new_session",
|
||||
title: "title",
|
||||
}
|
||||
|
||||
public state = {
|
||||
sessionName: "",
|
||||
showAll: true,
|
||||
}
|
||||
|
||||
public async componentDidMount() {
|
||||
this.props.populateSessions()
|
||||
}
|
||||
|
||||
public updateSelection = (selected: string) => {
|
||||
this.props.updateSelection(selected)
|
||||
}
|
||||
|
||||
public handleSelection = async (id: string) => {
|
||||
const { sessionName } = this.state
|
||||
const inputSelected = id === this._ID.input
|
||||
const isTitle = id === this._ID.title
|
||||
const isReadonlyField = id in this._ID
|
||||
switch (true) {
|
||||
case inputSelected && this.props.creating:
|
||||
await this.props.persistSession(sessionName)
|
||||
break
|
||||
case inputSelected && !this.props.creating:
|
||||
this.props.createSession()
|
||||
break
|
||||
case isTitle:
|
||||
this.setState({ showAll: !this.state.showAll })
|
||||
break
|
||||
case isReadonlyField:
|
||||
break
|
||||
default:
|
||||
await this.props.restoreSession(id)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
public restoreSession = async (selected: string) => {
|
||||
if (selected) {
|
||||
await this.props.restoreSession(selected)
|
||||
}
|
||||
}
|
||||
|
||||
public handleChange: React.ChangeEventHandler<HTMLInputElement> = evt => {
|
||||
const { value } = evt.currentTarget
|
||||
this.setState({ sessionName: value })
|
||||
}
|
||||
|
||||
public persistSession = async () => {
|
||||
const { sessionName } = this.state
|
||||
if (sessionName) {
|
||||
await this.props.persistSession(sessionName)
|
||||
}
|
||||
}
|
||||
|
||||
public handleCancel = () => {
|
||||
if (this.props.creating) {
|
||||
this.props.cancelCreating()
|
||||
}
|
||||
this.setState({ sessionName: "" })
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { showAll } = this.state
|
||||
const { sessions, active, creating } = this.props
|
||||
const ids = [this._ID.title, this._ID.input, ...sessions.map(({ id }) => id)]
|
||||
return (
|
||||
<VimNavigator
|
||||
ids={ids}
|
||||
active={active}
|
||||
onSelected={this.handleSelection}
|
||||
onSelectionChanged={this.updateSelection}
|
||||
render={(selectedId, updateSelection) => (
|
||||
<List>
|
||||
<SectionTitle
|
||||
active
|
||||
count={sessions.length}
|
||||
title="All Sessions"
|
||||
testId="sessions-title"
|
||||
isSelected={selectedId === this._ID.title}
|
||||
onClick={() => this.handleSelection(selectedId)}
|
||||
/>
|
||||
{showAll && (
|
||||
<>
|
||||
<ListItem isSelected={selectedId === this._ID.input}>
|
||||
{creating ? (
|
||||
<TextInputView
|
||||
styles={inputStyles}
|
||||
onChange={this.handleChange}
|
||||
onCancel={this.handleCancel}
|
||||
onComplete={this.persistSession}
|
||||
defaultValue="Enter a new Session Name"
|
||||
/>
|
||||
) : (
|
||||
<div onClick={() => this.handleSelection(selectedId)}>
|
||||
<Icon name="pencil" /> Create a new session
|
||||
</div>
|
||||
)}
|
||||
</ListItem>
|
||||
{sessions.length ? (
|
||||
sessions.map((session, idx) => (
|
||||
<SessionItem
|
||||
key={session.id}
|
||||
session={session}
|
||||
isSelected={selectedId === session.id}
|
||||
onClick={() => {
|
||||
updateSelection(session.id)
|
||||
this.handleSelection(session.id)
|
||||
}}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<Container>No Sessions Saved</Container>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</List>
|
||||
)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = ({ sessions, selected, active, creating }: ISessionState): IStateProps => ({
|
||||
sessions,
|
||||
active,
|
||||
creating,
|
||||
selected,
|
||||
})
|
||||
|
||||
export default connect<IStateProps, ISessionActions>(mapStateToProps, SessionActions)(Sessions)
|
|
@ -0,0 +1,71 @@
|
|||
import { Commands } from "oni-api"
|
||||
import * as React from "react"
|
||||
import { Provider } from "react-redux"
|
||||
|
||||
import { ISessionStore, Sessions } from "./"
|
||||
|
||||
interface SessionPaneProps {
|
||||
commands: Commands.Api
|
||||
store: ISessionStore
|
||||
}
|
||||
|
||||
/**
|
||||
* Class SessionsPane
|
||||
*
|
||||
* A Side bar pane for Oni's Session Management
|
||||
*
|
||||
*/
|
||||
export default class SessionsPane {
|
||||
private _store: ISessionStore
|
||||
private _commands: Commands.Api
|
||||
|
||||
constructor({ store, commands }: SessionPaneProps) {
|
||||
this._commands = commands
|
||||
this._store = store
|
||||
|
||||
this._setupCommands()
|
||||
}
|
||||
|
||||
get id() {
|
||||
return "oni.sidebar.sessions"
|
||||
}
|
||||
|
||||
public get title() {
|
||||
return "Sessions"
|
||||
}
|
||||
|
||||
public enter() {
|
||||
this._store.dispatch({ type: "ENTER" })
|
||||
}
|
||||
|
||||
public leave() {
|
||||
this._store.dispatch({ type: "LEAVE" })
|
||||
}
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<Provider store={this._store}>
|
||||
<Sessions />
|
||||
</Provider>
|
||||
)
|
||||
}
|
||||
|
||||
private _isActive = () => {
|
||||
const state = this._store.getState()
|
||||
return state.active && !state.creating
|
||||
}
|
||||
|
||||
private _deleteSession = () => {
|
||||
this._store.dispatch({ type: "DELETE_SESSION" })
|
||||
}
|
||||
|
||||
private _setupCommands() {
|
||||
this._commands.registerCommand({
|
||||
command: "oni.sessions.delete",
|
||||
name: "Sessions: Delete the current session",
|
||||
detail: "Delete the current or selected session",
|
||||
enabled: this._isActive,
|
||||
execute: this._deleteSession,
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,303 @@
|
|||
import "rxjs"
|
||||
|
||||
import * as fsExtra from "fs-extra"
|
||||
import * as path from "path"
|
||||
import { Store } from "redux"
|
||||
import { combineEpics, createEpicMiddleware, Epic, ofType } from "redux-observable"
|
||||
import { from } from "rxjs/observable/from"
|
||||
import { auditTime, catchError, filter, flatMap } from "rxjs/operators"
|
||||
|
||||
import { ISession, SessionManager } from "./"
|
||||
import { createStore as createReduxStore } from "./../../Redux"
|
||||
|
||||
export interface ISessionState {
|
||||
sessions: ISession[]
|
||||
selected: ISession
|
||||
currentSession: ISession
|
||||
active: boolean
|
||||
creating: boolean
|
||||
}
|
||||
|
||||
const DefaultState: ISessionState = {
|
||||
sessions: [],
|
||||
selected: null,
|
||||
active: false,
|
||||
creating: false,
|
||||
currentSession: null,
|
||||
}
|
||||
|
||||
interface IGenericAction<N, T = undefined> {
|
||||
type: N
|
||||
payload?: T
|
||||
}
|
||||
|
||||
export type ISessionStore = Store<ISessionState>
|
||||
|
||||
export type IUpdateMultipleSessions = IGenericAction<"GET_ALL_SESSIONS", { sessions: ISession[] }>
|
||||
export type IUpdateSelection = IGenericAction<"UPDATE_SELECTION", { selected: string }>
|
||||
export type IUpdateSession = IGenericAction<"UPDATE_SESSION", { session: ISession }>
|
||||
export type IRestoreSession = IGenericAction<"RESTORE_SESSION", { sessionName: string }>
|
||||
export type IPersistSession = IGenericAction<"PERSIST_SESSION", { sessionName: string }>
|
||||
export type IPersistSessionSuccess = IGenericAction<"PERSIST_SESSION_SUCCESS">
|
||||
export type IPersistSessionFailed = IGenericAction<"PERSIST_SESSION_FAILED", { error: Error }>
|
||||
export type IRestoreSessionError = IGenericAction<"RESTORE_SESSION_ERROR", { error: Error }>
|
||||
export type IDeleteSession = IGenericAction<"DELETE_SESSION">
|
||||
export type IDeleteSessionSuccess = IGenericAction<"DELETE_SESSION_SUCCESS">
|
||||
export type IDeleteSessionFailed = IGenericAction<"DELETE_SESSION_FAILED">
|
||||
export type IUpdateCurrentSession = IGenericAction<"UPDATE_CURRENT_SESSION">
|
||||
export type ISetCurrentSession = IGenericAction<"SET_CURRENT_SESSION", { session: ISession }>
|
||||
export type IPopulateSessions = IGenericAction<"POPULATE_SESSIONS">
|
||||
export type ICreateSession = IGenericAction<"CREATE_SESSION">
|
||||
export type ICancelCreateSession = IGenericAction<"CANCEL_NEW_SESSION">
|
||||
export type IEnter = IGenericAction<"ENTER">
|
||||
export type ILeave = IGenericAction<"LEAVE">
|
||||
|
||||
export type ISessionActions =
|
||||
| IUpdateMultipleSessions
|
||||
| ICancelCreateSession
|
||||
| IRestoreSessionError
|
||||
| IUpdateCurrentSession
|
||||
| IPopulateSessions
|
||||
| IUpdateSelection
|
||||
| IUpdateSession
|
||||
| IPersistSession
|
||||
| IPersistSessionSuccess
|
||||
| IPersistSessionFailed
|
||||
| IDeleteSession
|
||||
| IDeleteSessionSuccess
|
||||
| IDeleteSessionFailed
|
||||
| IRestoreSession
|
||||
| ISetCurrentSession
|
||||
| ICreateSession
|
||||
| IEnter
|
||||
| ILeave
|
||||
|
||||
export const SessionActions = {
|
||||
persistSessionSuccess: () => ({ type: "PERSIST_SESSION_SUCCESS" } as IPersistSessionSuccess),
|
||||
populateSessions: () => ({ type: "POPULATE_SESSIONS" } as IPopulateSessions),
|
||||
deleteSession: () => ({ type: "DELETE_SESSION" } as IDeleteSession),
|
||||
cancelCreating: () => ({ type: "CANCEL_NEW_SESSION" } as ICancelCreateSession),
|
||||
createSession: () => ({ type: "CREATE_SESSION" } as ICreateSession),
|
||||
updateCurrentSession: () => ({ type: "UPDATE_CURRENT_SESSION" } as IUpdateCurrentSession),
|
||||
deleteSessionSuccess: () => ({ type: "DELETE_SESSION_SUCCESS" } as IDeleteSessionSuccess),
|
||||
|
||||
updateSession: (session: ISession) => ({ type: "UPDATE_SESSION", session } as IUpdateSession),
|
||||
setCurrentSession: (session: ISession) =>
|
||||
({ type: "SET_CURRENT_SESSION", payload: { session } } as ISetCurrentSession),
|
||||
|
||||
deleteSessionFailed: (error: Error) =>
|
||||
({ type: "DELETE_SESSION_FAILED", error } as IDeleteSessionFailed),
|
||||
|
||||
persistSessionFailed: (error: Error) =>
|
||||
({ type: "PERSIST_SESSION_FAILED", error } as IPersistSessionFailed),
|
||||
|
||||
updateSelection: (selected: string) =>
|
||||
({ type: "UPDATE_SELECTION", payload: { selected } } as IUpdateSelection),
|
||||
|
||||
getAllSessions: (sessions: ISession[]) =>
|
||||
({
|
||||
type: "GET_ALL_SESSIONS",
|
||||
payload: { sessions },
|
||||
} as IUpdateMultipleSessions),
|
||||
|
||||
persistSession: (sessionName: string) =>
|
||||
({
|
||||
type: "PERSIST_SESSION",
|
||||
payload: { sessionName },
|
||||
} as IPersistSession),
|
||||
|
||||
restoreSessionError: (error: Error) =>
|
||||
({
|
||||
type: "RESTORE_SESSION_ERROR",
|
||||
payload: { error },
|
||||
} as IRestoreSessionError),
|
||||
|
||||
restoreSession: (sessionName: string) =>
|
||||
({
|
||||
type: "RESTORE_SESSION",
|
||||
payload: { sessionName },
|
||||
} as IRestoreSession),
|
||||
}
|
||||
|
||||
type SessionEpic = Epic<ISessionActions, ISessionState, Dependencies>
|
||||
|
||||
export const persistSessionEpic: SessionEpic = (action$, store, { sessionManager }) =>
|
||||
action$.pipe(
|
||||
ofType("PERSIST_SESSION"),
|
||||
auditTime(200),
|
||||
flatMap((action: IPersistSession) => {
|
||||
return from(sessionManager.persistSession(action.payload.sessionName)).pipe(
|
||||
flatMap(session => {
|
||||
return [
|
||||
SessionActions.cancelCreating(),
|
||||
SessionActions.persistSessionSuccess(),
|
||||
SessionActions.setCurrentSession(session),
|
||||
SessionActions.populateSessions(),
|
||||
]
|
||||
}),
|
||||
catchError(error => [SessionActions.persistSessionFailed(error)]),
|
||||
)
|
||||
}),
|
||||
)
|
||||
|
||||
const updateCurrentSessionEpic: SessionEpic = (action$, store, { fs, sessionManager }) => {
|
||||
return action$.pipe(
|
||||
ofType("UPDATE_CURRENT_SESSION"),
|
||||
auditTime(200),
|
||||
flatMap(() =>
|
||||
from(sessionManager.getCurrentSession()).pipe(
|
||||
filter(session => !!session),
|
||||
flatMap(currentSession => [SessionActions.persistSession(currentSession.name)]),
|
||||
catchError(error => [SessionActions.persistSessionFailed(error)]),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
const deleteSessionEpic: SessionEpic = (action$, store, { fs, sessionManager }) =>
|
||||
action$.pipe(
|
||||
ofType("DELETE_SESSION"),
|
||||
flatMap(() => {
|
||||
const { selected, currentSession } = store.getState()
|
||||
const sessionToDelete = selected || currentSession
|
||||
return from(
|
||||
fs
|
||||
.remove(sessionToDelete.file)
|
||||
.then(() => sessionManager.deleteSession(sessionToDelete.name)),
|
||||
).pipe(
|
||||
flatMap(() => [
|
||||
SessionActions.deleteSessionSuccess(),
|
||||
SessionActions.populateSessions(),
|
||||
]),
|
||||
catchError(error => {
|
||||
return [SessionActions.deleteSessionFailed(error)]
|
||||
}),
|
||||
)
|
||||
}),
|
||||
)
|
||||
|
||||
const restoreSessionEpic: SessionEpic = (action$, store, { sessionManager }) =>
|
||||
action$.pipe(
|
||||
ofType("RESTORE_SESSION"),
|
||||
flatMap((action: IRestoreSession) =>
|
||||
from(sessionManager.restoreSession(action.payload.sessionName)).pipe(
|
||||
flatMap(session => [
|
||||
SessionActions.setCurrentSession(session),
|
||||
SessionActions.populateSessions(),
|
||||
]),
|
||||
),
|
||||
),
|
||||
catchError(error => [SessionActions.restoreSessionError(error)]),
|
||||
)
|
||||
|
||||
export const fetchSessionsEpic: SessionEpic = (action$, store, { fs, sessionManager }) =>
|
||||
action$.pipe(
|
||||
ofType("POPULATE_SESSIONS"),
|
||||
flatMap((action: IPopulateSessions) => {
|
||||
return from(
|
||||
fs.readdir(sessionManager.sessionsDir).then(async dir => {
|
||||
const metadata = await Promise.all(
|
||||
dir.map(async file => {
|
||||
const filepath = path.join(sessionManager.sessionsDir, file)
|
||||
// use fs.stat mtime to figure when last a file was modified
|
||||
const { mtime } = await fs.stat(filepath)
|
||||
const [name] = file.split(".")
|
||||
return {
|
||||
name,
|
||||
file: filepath,
|
||||
updatedAt: mtime.toUTCString(),
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
const sessions = Promise.all(
|
||||
metadata.map(async ({ file, name, updatedAt }) => {
|
||||
const savedSession = await sessionManager.getSessionFromStore(name)
|
||||
await sessionManager.updateOniSession(name, { updatedAt })
|
||||
return { ...savedSession, updatedAt }
|
||||
}),
|
||||
)
|
||||
return sessions
|
||||
}),
|
||||
).flatMap(sessions => [SessionActions.getAllSessions(sessions)])
|
||||
}),
|
||||
)
|
||||
|
||||
const findSelectedSession = (sessions: ISession[], selected: string) =>
|
||||
sessions.find(session => session.id === selected)
|
||||
|
||||
const updateSessions = (sessions: ISession[], newSession: ISession) =>
|
||||
sessions.map(session => (session.id === newSession.id ? newSession : session))
|
||||
|
||||
function reducer(state: ISessionState, action: ISessionActions) {
|
||||
switch (action.type) {
|
||||
case "UPDATE_SESSION":
|
||||
return {
|
||||
...state,
|
||||
sessions: updateSessions(state.sessions, action.payload.session),
|
||||
}
|
||||
case "GET_ALL_SESSIONS":
|
||||
return {
|
||||
...state,
|
||||
sessions: action.payload.sessions,
|
||||
}
|
||||
case "CREATE_SESSION":
|
||||
return {
|
||||
...state,
|
||||
creating: true,
|
||||
}
|
||||
case "DELETE_SESSION_SUCCESS":
|
||||
return {
|
||||
...state,
|
||||
currentSession: null,
|
||||
}
|
||||
case "SET_CURRENT_SESSION":
|
||||
return {
|
||||
...state,
|
||||
currentSession: action.payload.session,
|
||||
}
|
||||
case "CANCEL_NEW_SESSION":
|
||||
return {
|
||||
...state,
|
||||
creating: false,
|
||||
}
|
||||
case "ENTER":
|
||||
return {
|
||||
...state,
|
||||
active: true,
|
||||
}
|
||||
case "LEAVE":
|
||||
return {
|
||||
...state,
|
||||
active: false,
|
||||
}
|
||||
case "UPDATE_SELECTION":
|
||||
return {
|
||||
...state,
|
||||
selected: findSelectedSession(state.sessions, action.payload.selected),
|
||||
}
|
||||
default:
|
||||
return state
|
||||
}
|
||||
}
|
||||
|
||||
interface Dependencies {
|
||||
fs: typeof fsExtra
|
||||
sessionManager: SessionManager
|
||||
}
|
||||
|
||||
const createStore = (dependencies: Dependencies) =>
|
||||
createReduxStore("sessions", reducer, DefaultState, [
|
||||
createEpicMiddleware<ISessionActions, ISessionState, Dependencies>(
|
||||
combineEpics(
|
||||
fetchSessionsEpic,
|
||||
persistSessionEpic,
|
||||
restoreSessionEpic,
|
||||
updateCurrentSessionEpic,
|
||||
deleteSessionEpic,
|
||||
),
|
||||
{ dependencies },
|
||||
),
|
||||
])
|
||||
|
||||
export default createStore
|
|
@ -0,0 +1,5 @@
|
|||
export * from "./SessionManager"
|
||||
export * from "./SessionsStore"
|
||||
export { default as SessionsPane } from "./SessionsPane"
|
||||
export { default as Sessions } from "./Sessions"
|
||||
export { default as store } from "./SessionsStore"
|
|
@ -2,9 +2,9 @@ import * as React from "react"
|
|||
import { connect } from "react-redux"
|
||||
|
||||
import { styled } from "./../../UI/components/common"
|
||||
import { SectionTitle, Title } from "./../../UI/components/SectionTitle"
|
||||
import CommitsSection from "./../../UI/components/VersionControl/Commits"
|
||||
import Help from "./../../UI/components/VersionControl/Help"
|
||||
import { SectionTitle, Title } from "./../../UI/components/VersionControl/SectionTitle"
|
||||
import StagedSection from "./../../UI/components/VersionControl/Staged"
|
||||
import VersionControlStatus from "./../../UI/components/VersionControl/Status"
|
||||
import { VimNavigator } from "./../../UI/components/VimNavigator"
|
||||
|
|
|
@ -32,6 +32,10 @@ const WordRegex = /[$_a-zA-Z0-9]/i
|
|||
* common functionality (like focus management, key handling)
|
||||
*/
|
||||
export class TextInputView extends React.PureComponent<ITextInputViewProps, {}> {
|
||||
public static defaultProps = {
|
||||
InputComponent: Input,
|
||||
}
|
||||
|
||||
private _element: HTMLInputElement
|
||||
|
||||
public componentDidMount(): void {
|
||||
|
@ -48,7 +52,7 @@ export class TextInputView extends React.PureComponent<ITextInputViewProps, {}>
|
|||
}
|
||||
|
||||
const defaultValue = this.props.defaultValue || ""
|
||||
const { InputComponent = Input } = this.props
|
||||
const { InputComponent } = this.props
|
||||
|
||||
return (
|
||||
<div className="input-container enable-mouse">
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import * as React from "react"
|
||||
|
||||
import Caret from "./../Caret"
|
||||
import { sidebarItemSelected, styled, withProps } from "./../common"
|
||||
import Caret from "./Caret"
|
||||
import { sidebarItemSelected, styled, withProps } from "./common"
|
||||
|
||||
export const Title = styled.h4`
|
||||
margin: 0;
|
||||
|
@ -25,7 +25,7 @@ interface IProps {
|
|||
testId: string
|
||||
}
|
||||
|
||||
const VCSSectionTitle: React.SFC<IProps> = props => (
|
||||
const SidebarSectionTitle: React.SFC<IProps> = props => (
|
||||
<SectionTitle isSelected={props.isSelected} data-test={props.testId} onClick={props.onClick}>
|
||||
<Caret active={props.active} />
|
||||
<Title>{props.title.toUpperCase()}</Title>
|
||||
|
@ -33,4 +33,4 @@ const VCSSectionTitle: React.SFC<IProps> = props => (
|
|||
</SectionTitle>
|
||||
)
|
||||
|
||||
export default VCSSectionTitle
|
||||
export default SidebarSectionTitle
|
|
@ -3,7 +3,7 @@ import * as React from "react"
|
|||
import { Logs } from "../../../Services/VersionControl/VersionControlProvider"
|
||||
import { sidebarItemSelected, styled, withProps } from "./../../../UI/components/common"
|
||||
import { formatDate } from "./../../../Utility"
|
||||
import VCSSectionTitle from "./SectionTitle"
|
||||
import VCSSectionTitle from "./../SectionTitle"
|
||||
|
||||
interface ICommitsSection {
|
||||
commits: Logs["all"]
|
||||
|
|
|
@ -2,7 +2,7 @@ import * as React from "react"
|
|||
|
||||
import { inputManager } from "../../../Services/InputManager"
|
||||
import styled from "../common"
|
||||
import { SectionTitle, Title } from "./SectionTitle"
|
||||
import { SectionTitle, Title } from "./../SectionTitle"
|
||||
|
||||
const sidebarCommands = [
|
||||
{ command: "vcs.openFile", description: "Open the currently selected file" },
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import * as React from "react"
|
||||
|
||||
import styled, { Center, sidebarItemSelected, withProps } from "../common"
|
||||
import SectionTitle from "../SectionTitle"
|
||||
import { LoadingSpinner } from "./../../../UI/components/LoadingSpinner"
|
||||
import CommitMessage from "./CommitMessage"
|
||||
import File from "./File"
|
||||
import SectionTitle from "./SectionTitle"
|
||||
|
||||
const Explainer = styled.div`
|
||||
width: 100%;
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import * as React from "react"
|
||||
|
||||
import VCSSectionTitle from "../SectionTitle"
|
||||
import File from "./File"
|
||||
import VCSSectionTitle from "./SectionTitle"
|
||||
|
||||
interface IModifiedFilesProps {
|
||||
files?: string[]
|
||||
|
|
|
@ -32,7 +32,7 @@ export interface IVimNavigatorProps {
|
|||
onSelectionChanged?: (selectedId: string) => void
|
||||
onSelected?: (selectedId: string) => void
|
||||
|
||||
render: (selectedId: string) => JSX.Element
|
||||
render: (selectedId: string, updateSelection: (id: string) => void) => JSX.Element
|
||||
|
||||
style?: React.CSSProperties
|
||||
idToSelect?: string
|
||||
|
@ -66,6 +66,10 @@ export class VimNavigator extends React.PureComponent<IVimNavigatorProps, IVimNa
|
|||
this._releaseBinding()
|
||||
}
|
||||
|
||||
public updateSelection = (id: string) => {
|
||||
this.setState({ selectedId: id })
|
||||
}
|
||||
|
||||
public render() {
|
||||
const inputElement = (
|
||||
<div className="input">
|
||||
|
@ -85,7 +89,9 @@ export class VimNavigator extends React.PureComponent<IVimNavigatorProps, IVimNa
|
|||
|
||||
return (
|
||||
<div style={this.props.style}>
|
||||
<div className="items">{this.props.render(this.state.selectedId)}</div>
|
||||
<div className="items">
|
||||
{this.props.render(this.state.selectedId, this.updateSelection)}
|
||||
</div>
|
||||
{this.props.active ? inputElement : null}
|
||||
</div>
|
||||
)
|
||||
|
|
|
@ -78,8 +78,10 @@ export const StackLayer = styled<{ zIndex?: number | string }, "div">("div")`
|
|||
`
|
||||
|
||||
export const sidebarItemSelected = css`
|
||||
border: ${(p: any) =>
|
||||
p.isSelected && `1px solid ${p.theme["highlight.mode.normal.background"]}`};
|
||||
border: ${(p: { isSelected?: boolean; theme?: styledComponents.ThemeProps<IThemeColors> }) =>
|
||||
p.isSelected
|
||||
? `1px solid ${p.theme["highlight.mode.normal.background"]}`
|
||||
: `1px solid transparent`};
|
||||
`
|
||||
|
||||
export type StyledFunction<T> = styledComponents.ThemedStyledFunction<T, IThemeColors>
|
||||
|
|
|
@ -560,6 +560,15 @@ export class NeovimInstance extends EventEmitter implements INeovimInstance {
|
|||
return this.command(`e! ${fileName}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* closeAllBuffers
|
||||
*
|
||||
* silently close all open buffers
|
||||
*/
|
||||
public async closeAllBuffers() {
|
||||
await this.command(`silent! %bdelete`)
|
||||
}
|
||||
|
||||
/**
|
||||
* getInitVimPath
|
||||
* return the init vim path with no check to ensure existence
|
||||
|
|
|
@ -18,4 +18,13 @@ export class MockPersistentStore<T> implements IPersistentStore<T> {
|
|||
public async get(): Promise<T> {
|
||||
return this._state
|
||||
}
|
||||
|
||||
public async delete(key: string): Promise<T> {
|
||||
this._state[key] = undefined
|
||||
return this._state
|
||||
}
|
||||
|
||||
public has(key: string) {
|
||||
return !!this._state[key]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,7 +9,9 @@ module.exports = {
|
|||
PersistentSettings: "<rootDir>/ui-tests/mocks/PersistentSettings.ts",
|
||||
Utility: "<rootDir>/ui-tests/mocks/Utility.ts",
|
||||
Configuration: "<rootDir>/ui-tests/mocks/Configuration.ts",
|
||||
UserConfiguration: "<rootDir>/ui-tests/mocks/UserConfiguration.ts",
|
||||
KeyboardLayout: "<rootDir>/ui-tests/mocks/keyboardLayout.ts",
|
||||
SharedNeovimInstance: "<rootDir>/ui-tests/mocks/SharedNeovimInstance.ts",
|
||||
},
|
||||
snapshotSerializers: ["enzyme-to-json/serializer"],
|
||||
transform: {
|
||||
|
|
|
@ -672,7 +672,7 @@
|
|||
"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": "run-p watch:plugins:*",
|
||||
"watch:plugins": "run-p --race watch:plugins:*",
|
||||
"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",
|
||||
|
|
|
@ -0,0 +1,75 @@
|
|||
import * as path from "path"
|
||||
|
||||
import {
|
||||
ISession,
|
||||
SessionManager,
|
||||
UpdatedOni,
|
||||
} from "./../browser/src/Services/Sessions/SessionManager"
|
||||
|
||||
import Oni from "./mocks/Oni"
|
||||
import Sidebar from "./mocks/Sidebar"
|
||||
|
||||
jest.mock("./../browser/src/Services/Configuration/UserConfiguration", () => ({
|
||||
getUserConfigFolderPath: jest.fn().mockReturnValue("~/.config/oni"),
|
||||
}))
|
||||
|
||||
interface IStore {
|
||||
[key: string]: ISession
|
||||
}
|
||||
|
||||
const mockPersistentStore = {
|
||||
_store: {} as IStore,
|
||||
get(): Promise<{ [key: string]: ISession }> {
|
||||
return new Promise((resolve, reject) => {
|
||||
resolve(this._store || {})
|
||||
})
|
||||
},
|
||||
set(obj: { [key: string]: any }) {
|
||||
return new Promise(resolve => {
|
||||
this._store = { ...this._store, ...obj }
|
||||
resolve(null)
|
||||
})
|
||||
},
|
||||
delete(key: string) {
|
||||
delete this._store[key]
|
||||
return new Promise(resolve => resolve(this._store))
|
||||
},
|
||||
has(key) {
|
||||
return !!this._store[key]
|
||||
},
|
||||
}
|
||||
|
||||
describe("Session Manager Tests", () => {
|
||||
const persistentStore = mockPersistentStore
|
||||
const oni = new Oni({})
|
||||
const manager = new SessionManager(oni as UpdatedOni, new Sidebar(), persistentStore)
|
||||
|
||||
beforeEach(() => {
|
||||
mockPersistentStore._store = {}
|
||||
})
|
||||
|
||||
it("Should return the correct session directory", () => {
|
||||
expect(manager.sessionsDir).toMatch(path.join(".config", "oni", "session"))
|
||||
})
|
||||
|
||||
it("should save a session in the persistentStore", async () => {
|
||||
await manager.persistSession("test-session")
|
||||
const session = await persistentStore.get()
|
||||
expect(session).toBeTruthy()
|
||||
})
|
||||
|
||||
it("should correctly delete a session", async () => {
|
||||
await manager.persistSession("test-session")
|
||||
const session = await persistentStore.get()
|
||||
expect(session).toBeTruthy()
|
||||
await manager.deleteSession("test-session")
|
||||
expect(session["test-session"]).toBeFalsy()
|
||||
})
|
||||
|
||||
it("should correctly update a session", async () => {
|
||||
await manager.persistSession("test-session")
|
||||
await manager.updateOniSession("test-session", { newValue: 2 })
|
||||
const session = await manager.getSessionFromStore("test-session")
|
||||
expect(session.newValue).toBe(2)
|
||||
})
|
||||
})
|
|
@ -0,0 +1,168 @@
|
|||
import { shallow, mount } from "enzyme"
|
||||
import * as React from "react"
|
||||
|
||||
import { Sessions, Container } from "./../browser/src/Services/Sessions/Sessions"
|
||||
import TextInputView from "../browser/src/UI/components/LightweightText"
|
||||
|
||||
const noop = () => ({})
|
||||
|
||||
jest.mock("./../browser/src/neovim/SharedNeovimInstance", () => ({
|
||||
getInstance: () => ({
|
||||
bindToMenu: () => ({
|
||||
setItems: jest.fn(),
|
||||
onCursorMoved: {
|
||||
subscribe: jest.fn(),
|
||||
},
|
||||
}),
|
||||
}),
|
||||
}))
|
||||
|
||||
describe("<Sessions />", () => {
|
||||
const sessions = [
|
||||
{
|
||||
name: "test",
|
||||
id: "test-1",
|
||||
file: "/sessions/test.vim",
|
||||
directory: "/sessions",
|
||||
updatedAt: null,
|
||||
workspace: "/workspace",
|
||||
},
|
||||
{
|
||||
name: "testing",
|
||||
id: "testing-2",
|
||||
file: "/sessions/testing.vim",
|
||||
directory: "/sessions",
|
||||
updatedAt: null,
|
||||
workspace: "/workspace",
|
||||
},
|
||||
]
|
||||
it("should render without crashing", () => {
|
||||
const wrapper = shallow(
|
||||
<Sessions
|
||||
active
|
||||
cancelCreating={noop}
|
||||
createSession={noop}
|
||||
persistSession={noop}
|
||||
restoreSession={noop}
|
||||
updateSession={noop}
|
||||
getAllSessions={noop}
|
||||
updateSelection={noop}
|
||||
sessions={sessions}
|
||||
creating={false}
|
||||
selected={sessions[0]}
|
||||
populateSessions={noop}
|
||||
/>,
|
||||
)
|
||||
})
|
||||
it("should render no children if showAll is false", () => {
|
||||
const wrapper = shallow(
|
||||
<Sessions
|
||||
active
|
||||
cancelCreating={noop}
|
||||
createSession={noop}
|
||||
persistSession={noop}
|
||||
restoreSession={noop}
|
||||
updateSession={noop}
|
||||
getAllSessions={noop}
|
||||
updateSelection={noop}
|
||||
sessions={sessions}
|
||||
creating={false}
|
||||
selected={sessions[0]}
|
||||
populateSessions={noop}
|
||||
/>,
|
||||
)
|
||||
wrapper.setState({ showAll: false })
|
||||
const items = wrapper
|
||||
.dive()
|
||||
.find("ul")
|
||||
.children()
|
||||
expect(items.length).toBe(0)
|
||||
})
|
||||
|
||||
it("should render correct number of children if showAll is true", () => {
|
||||
const wrapper = mount(
|
||||
<Sessions
|
||||
active
|
||||
cancelCreating={noop}
|
||||
createSession={noop}
|
||||
persistSession={noop}
|
||||
restoreSession={noop}
|
||||
updateSession={noop}
|
||||
getAllSessions={noop}
|
||||
updateSelection={noop}
|
||||
sessions={sessions.slice(1)} // remove one session
|
||||
creating={false}
|
||||
selected={sessions[0]}
|
||||
populateSessions={noop}
|
||||
/>,
|
||||
)
|
||||
wrapper.setState({ showAll: true })
|
||||
const items = wrapper.find("ul").children()
|
||||
expect(items.length).toBe(3)
|
||||
})
|
||||
|
||||
it("should render an input if creating is true", () => {
|
||||
const wrapper = mount(
|
||||
<Sessions
|
||||
active
|
||||
cancelCreating={noop}
|
||||
createSession={noop}
|
||||
persistSession={noop}
|
||||
restoreSession={noop}
|
||||
updateSession={noop}
|
||||
getAllSessions={noop}
|
||||
updateSelection={noop}
|
||||
sessions={sessions.slice(1)} // remove one session
|
||||
creating={true}
|
||||
selected={sessions[0]}
|
||||
populateSessions={noop}
|
||||
/>,
|
||||
)
|
||||
const hasInput = wrapper.find(TextInputView).length
|
||||
|
||||
expect(hasInput).toBeTruthy()
|
||||
})
|
||||
|
||||
it("should render no input if creating is false", () => {
|
||||
const wrapper = mount(
|
||||
<Sessions
|
||||
active
|
||||
cancelCreating={noop}
|
||||
createSession={noop}
|
||||
persistSession={noop}
|
||||
restoreSession={noop}
|
||||
updateSession={noop}
|
||||
getAllSessions={noop}
|
||||
updateSelection={noop}
|
||||
sessions={sessions.slice(1)}
|
||||
creating={false}
|
||||
selected={null}
|
||||
populateSessions={noop}
|
||||
/>,
|
||||
)
|
||||
const hasInput = wrapper.find(TextInputView).length
|
||||
|
||||
expect(hasInput).toBeFalsy()
|
||||
})
|
||||
|
||||
it("should empty message if there are no sessions", () => {
|
||||
const wrapper = mount(
|
||||
<Sessions
|
||||
active
|
||||
cancelCreating={noop}
|
||||
createSession={noop}
|
||||
persistSession={noop}
|
||||
restoreSession={noop}
|
||||
updateSession={noop}
|
||||
getAllSessions={noop}
|
||||
updateSelection={noop}
|
||||
sessions={[]}
|
||||
creating={true}
|
||||
selected={null}
|
||||
populateSessions={noop}
|
||||
/>,
|
||||
)
|
||||
expect(wrapper.find(Container).length).toBe(1)
|
||||
expect(wrapper.find(Container).text()).toBe("No Sessions Saved")
|
||||
})
|
||||
})
|
|
@ -2,9 +2,7 @@ import * as React from "react"
|
|||
import { shallow } from "enzyme"
|
||||
import { shallowToJson } from "enzyme-to-json"
|
||||
|
||||
import VersionControlTitle, {
|
||||
Title,
|
||||
} from "./../../browser/src/UI/components/VersionControl/SectionTitle"
|
||||
import VersionControlTitle, { Title } from "./../../browser/src/UI/components/SectionTitle"
|
||||
|
||||
describe("<VersionControlTitle />", () => {
|
||||
it("correctly renders without crashing", () => {
|
||||
|
|
|
@ -2,6 +2,8 @@ import { shallow, mount } from "enzyme"
|
|||
import { shallowToJson } from "enzyme-to-json"
|
||||
import * as React from "react"
|
||||
|
||||
import { SectionTitle, Title } from "./../../browser/src/UI/components/SectionTitle"
|
||||
|
||||
import {
|
||||
DefaultState,
|
||||
VersionControlState,
|
||||
|
@ -12,7 +14,6 @@ import CommitMessage, {
|
|||
} from "./../../browser/src/UI/components/VersionControl/CommitMessage"
|
||||
import Commits from "./../../browser/src/UI/components/VersionControl/Commits"
|
||||
import Help from "./../../browser/src/UI/components/VersionControl/Help"
|
||||
import { SectionTitle, Title } from "./../../browser/src/UI/components/VersionControl/SectionTitle"
|
||||
import Staged, { LoadingHandler } from "./../../browser/src/UI/components/VersionControl/Staged"
|
||||
import VersionControlStatus from "./../../browser/src/UI/components/VersionControl/Status"
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
exports[`<VersionControlView /> should match the last recorded snapshot unless a change was made 1`] = `
|
||||
<div>
|
||||
<VCSSectionTitle
|
||||
<SidebarSectionTitle
|
||||
active={true}
|
||||
count={2}
|
||||
isSelected={false}
|
||||
|
|
|
@ -1,17 +1,16 @@
|
|||
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(),
|
||||
}
|
||||
})
|
||||
const Configuration = jest.fn<Oni.Configuration>().mockImplementation(() => ({
|
||||
onConfigurationChanged() {
|
||||
return {
|
||||
subscribe: jest.fn(),
|
||||
}
|
||||
},
|
||||
notifyListeners: jest.fn(),
|
||||
updateConfig: jest.fn(),
|
||||
getValue: jest.fn(),
|
||||
}))
|
||||
|
||||
export const configuration = new Configuration()
|
||||
|
||||
export default Configuration
|
||||
|
|
|
@ -3,13 +3,19 @@ import { EditorManager } from "./../../browser/src/Services/EditorManager"
|
|||
|
||||
const _onBufferSaved = new Event<any>("Test:ActiveEditor-BufferSaved")
|
||||
const _onBufferEnter = new Event<any>("Test:ActiveEditor-BufferEnter")
|
||||
const _onQuit = new Event<void>()
|
||||
|
||||
const MockEditorManager = jest.fn<EditorManager>().mockImplementation(() => ({
|
||||
activeEditor: {
|
||||
onQuit: _onQuit,
|
||||
activeBuffer: {
|
||||
filePath: "test.txt",
|
||||
},
|
||||
onBufferEnter: _onBufferEnter,
|
||||
onBufferSaved: _onBufferSaved,
|
||||
restoreSession: jest.fn(),
|
||||
persistSession: jest.fn(),
|
||||
getCurrentSession: jest.fn().mockReturnValue("test-session"),
|
||||
},
|
||||
}))
|
||||
|
||||
|
|
|
@ -1,120 +1,53 @@
|
|||
import * as Oni from "oni-api"
|
||||
|
||||
import MockCommands from "./CommandManager"
|
||||
import { configuration } from "./Configuration"
|
||||
import MockEditorManager from "./EditorManager"
|
||||
import MockMenu from "./MenuManager"
|
||||
import MockSidebar from "./../mocks/Sidebar"
|
||||
import MockStatusbar from "./Statusbar"
|
||||
import MockWorkspace from "./Workspace"
|
||||
|
||||
class MockOni implements Oni.Plugin.Api {
|
||||
private _commands = new MockCommands()
|
||||
private _configuration = configuration
|
||||
private _editorManager = new MockEditorManager()
|
||||
private _sidebar = new MockSidebar()
|
||||
private _statusBar = new MockStatusbar()
|
||||
private _workspace = new MockWorkspace()
|
||||
const MockOni = jest
|
||||
.fn<Oni.Plugin.Api>()
|
||||
.mockImplementation((values: { apiMock: { [k: string]: any } }) => {
|
||||
const commands = new MockCommands()
|
||||
const editors = new MockEditorManager()
|
||||
const sidebar = new MockSidebar()
|
||||
const statusBar = new MockStatusbar()
|
||||
const workspace = new MockWorkspace()
|
||||
|
||||
get automation(): Oni.Automation.Api {
|
||||
throw Error("Not yet implemented")
|
||||
}
|
||||
|
||||
get colors(): Oni.IColors {
|
||||
throw Error("Not yet implemented")
|
||||
}
|
||||
|
||||
get commands(): Oni.Commands.Api {
|
||||
return this._commands
|
||||
}
|
||||
|
||||
get configuration(): Oni.Configuration {
|
||||
return this._configuration
|
||||
}
|
||||
|
||||
get contextMenu(): any /* TODO */ {
|
||||
throw Error("Not yet implemented")
|
||||
}
|
||||
|
||||
get diagnostics(): Oni.Plugin.Diagnostics.Api {
|
||||
throw Error("Not yet implemented")
|
||||
}
|
||||
|
||||
get editors(): Oni.EditorManager {
|
||||
return this._editorManager
|
||||
}
|
||||
|
||||
get filter(): Oni.Menu.IMenuFilters {
|
||||
throw Error("Not yet implemented")
|
||||
}
|
||||
|
||||
get input(): Oni.Input.InputManager {
|
||||
throw Error("Not yet implemented")
|
||||
}
|
||||
|
||||
get language(): any /* TODO */ {
|
||||
throw Error("Not yet implemented")
|
||||
}
|
||||
|
||||
get log(): any /* TODO */ {
|
||||
throw Error("Not yet implemented")
|
||||
}
|
||||
|
||||
get notifications(): Oni.Notifications.Api {
|
||||
throw Error("Not yet implemented")
|
||||
}
|
||||
|
||||
get overlays(): Oni.Overlays.Api {
|
||||
throw Error("Not yet implemented")
|
||||
}
|
||||
|
||||
get plugins(): Oni.IPluginManager {
|
||||
throw Error("Not yet implemented")
|
||||
}
|
||||
|
||||
get search(): Oni.Search.ISearch {
|
||||
throw Error("Not yet implemented")
|
||||
}
|
||||
|
||||
get sidebar(): Oni.Sidebar.Api {
|
||||
return this._sidebar
|
||||
}
|
||||
|
||||
get ui(): Oni.Ui.IUi {
|
||||
throw Error("Not yet implemented")
|
||||
}
|
||||
|
||||
get menu(): Oni.Menu.Api {
|
||||
throw Error("Not yet implemented")
|
||||
}
|
||||
|
||||
get process(): Oni.Process {
|
||||
throw Error("Not yet implemented")
|
||||
}
|
||||
|
||||
get recorder(): Oni.Recorder {
|
||||
throw Error("Not yet implemented")
|
||||
}
|
||||
|
||||
get snippets(): Oni.Snippets.SnippetManager {
|
||||
throw Error("Not yet implemented")
|
||||
}
|
||||
|
||||
get statusBar(): Oni.StatusBar {
|
||||
return this._statusBar
|
||||
}
|
||||
|
||||
get windows(): Oni.IWindowManager {
|
||||
throw Error("Not yet implemented")
|
||||
}
|
||||
|
||||
get workspace(): Oni.Workspace.Api {
|
||||
return this._workspace
|
||||
}
|
||||
|
||||
public populateQuickFix(entries: Oni.QuickFixEntry[]): void {
|
||||
throw Error("Not yet implemented")
|
||||
}
|
||||
}
|
||||
return {
|
||||
commands,
|
||||
configuration: {
|
||||
getValue: jest.fn(),
|
||||
},
|
||||
editors,
|
||||
sidebar,
|
||||
statusBar,
|
||||
workspace,
|
||||
filter: null,
|
||||
input: null,
|
||||
language: null,
|
||||
log: null,
|
||||
notifications: null,
|
||||
overlays: null,
|
||||
plugins: null,
|
||||
search: null,
|
||||
ui: null,
|
||||
menu: null,
|
||||
process: null,
|
||||
recorder: null,
|
||||
snippets: null,
|
||||
windows: null,
|
||||
automation: null,
|
||||
colors: null,
|
||||
contextMenu: null,
|
||||
diagnostics: null,
|
||||
populateQuickFix(entries: Oni.QuickFixEntry[]): void {
|
||||
throw Error("Not yet implemented")
|
||||
},
|
||||
...values,
|
||||
}
|
||||
})
|
||||
|
||||
export default MockOni
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
const SharedNeovimInstance = jest.fn().mockImplementation(() => ({
|
||||
bindToMenu: () => ({
|
||||
setItems: jest.fn(),
|
||||
onCursorMoved: {
|
||||
subscribe: jest.fn(),
|
||||
},
|
||||
}),
|
||||
}))
|
||||
|
||||
export const getInstance = new SharedNeovimInstance()
|
|
@ -1,6 +1,7 @@
|
|||
import { SidebarManager } from "./../../browser/src/Services/Sidebar"
|
||||
|
||||
const MockSidebar = jest.fn<SidebarManager>().mockImplementation(() => ({
|
||||
add: jest.fn(),
|
||||
entries: [
|
||||
{
|
||||
id: "git-vcs",
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
export const getUserConfigFolderPath = jest.fn().mockReturnValue("~/.config/oni")
|
|
@ -249,6 +249,9 @@ export class GitVersionControlProvider implements VCS.VersionControlProvider {
|
|||
}
|
||||
|
||||
private _formatRawBlame(rawOutput: string): VCS.Blame {
|
||||
if (!rawOutput) {
|
||||
return null
|
||||
}
|
||||
const firstSpace = (str: string) => str.indexOf(" ")
|
||||
const blameArray = rawOutput.split("\n")
|
||||
const formatted = blameArray
|
||||
|
@ -258,11 +261,13 @@ export class GitVersionControlProvider implements VCS.VersionControlProvider {
|
|||
const formattedKey = key.replace("-", "_")
|
||||
if (!index && value) {
|
||||
acc.hash = formattedKey
|
||||
const [originalLine, finalLine, numberOfLines] = value.split(" ")
|
||||
acc.line = {
|
||||
originalLine,
|
||||
finalLine,
|
||||
numberOfLines,
|
||||
if (value) {
|
||||
const [originalLine, finalLine, numberOfLines] = value.split(" ")
|
||||
acc.line = {
|
||||
originalLine,
|
||||
finalLine,
|
||||
numberOfLines,
|
||||
}
|
||||
}
|
||||
return acc
|
||||
} else if (!key) {
|
||||
|
|
Loading…
Reference in New Issue