mirror of https://github.com/onivim/oni.git
1079 lines
35 KiB
TypeScript
1079 lines
35 KiB
TypeScript
import { EventEmitter } from "events"
|
|
import { pathExistsSync } from "fs-extra"
|
|
import * as path from "path"
|
|
|
|
import * as mkdirp from "mkdirp"
|
|
import * as Oni from "oni-api"
|
|
import * as Log from "oni-core-logging"
|
|
import { Event, IDisposable, IEvent } from "oni-types"
|
|
|
|
import * as Performance from "./../Performance"
|
|
import { CommandContext } from "./CommandContext"
|
|
import { EventContext } from "./EventContext"
|
|
|
|
import { addDefaultUnitIfNeeded, measureFont } from "./../Font"
|
|
import * as Platform from "./../Platform"
|
|
import { Configuration } from "./../Services/Configuration"
|
|
|
|
import * as Actions from "./actions"
|
|
import { NeovimBufferReference } from "./MsgPack"
|
|
import { INeovimAutoCommands, NeovimAutoCommands } from "./NeovimAutoCommands"
|
|
import { INeovimMarks, NeovimMarks } from "./NeovimMarks"
|
|
import { INeovimStartOptions, startNeovim } from "./NeovimProcessSpawner"
|
|
import { IQuickFixList, QuickFixList } from "./QuickFix"
|
|
import { IPixelPosition, IPosition } from "./Screen"
|
|
import { Session } from "./Session"
|
|
|
|
import { PromiseQueue } from "./../Services/Language/PromiseQueue"
|
|
import { TokenColor } from "./../Services/TokenColors"
|
|
import { INeovimBufferUpdate, NeovimBufferUpdateManager } from "./NeovimBufferUpdateManager"
|
|
import { NeovimTokenColorSynchronizer } from "./NeovimTokenColorSynchronizer"
|
|
|
|
import {
|
|
IVimHighlight,
|
|
VimHighlightToDefaultScope,
|
|
vimHighlightToTokenColorStyle,
|
|
} from "./VimHighlights"
|
|
|
|
export interface INeovimYankInfo {
|
|
operator: string
|
|
regcontents: string[]
|
|
regname: string
|
|
regtype: string
|
|
}
|
|
|
|
export interface INeovimApiVersion {
|
|
major: number
|
|
minor: number
|
|
patch: number
|
|
}
|
|
|
|
export interface IFullBufferUpdateEvent {
|
|
context: EventContext
|
|
bufferLines: string[]
|
|
}
|
|
|
|
export interface IIncrementalBufferUpdateEvent {
|
|
context: EventContext
|
|
lineNumber: number
|
|
lineContents: string
|
|
}
|
|
|
|
export interface INeovimCompletionItem {
|
|
word: string
|
|
kind: string
|
|
menu: string
|
|
info: string
|
|
}
|
|
|
|
export interface INeovimCompletionInfo {
|
|
items: INeovimCompletionItem[]
|
|
selectedIndex: number
|
|
row: number
|
|
col: number
|
|
}
|
|
|
|
export type CommandLineContent = [any, string]
|
|
|
|
export type MapcheckModes = "n" | "v" | "i"
|
|
|
|
interface IMapping {
|
|
key: string
|
|
mode: MapcheckModes
|
|
}
|
|
|
|
export interface INeovimCommandLineShowEvent {
|
|
content: CommandLineContent[]
|
|
pos: number
|
|
firstc: string
|
|
prompt: string
|
|
indent: number
|
|
level: number
|
|
}
|
|
|
|
export interface IWildMenuShowEvent {
|
|
options: string[]
|
|
}
|
|
|
|
export interface IWildMenuSelectEvent {
|
|
selected: any
|
|
}
|
|
|
|
export interface INeovimCommandLineSetCursorPosition {
|
|
pos: number
|
|
level: number
|
|
}
|
|
|
|
export interface IMessageInfo {
|
|
severity: "warn" | "error" | "info"
|
|
title: string
|
|
details: string
|
|
}
|
|
|
|
// Limit for the number of lines to handle buffer updates
|
|
// If the file is too large, it ends up being too much traffic
|
|
// between Neovim <-> Oni <-> Language Servers - so
|
|
// set a hard limit. In the future, if need be, this could be
|
|
// moved to a configuration setting.
|
|
export const MAX_LINES_FOR_BUFFER_UPDATE = 5000
|
|
|
|
export type NeovimEventHandler = (...args: any[]) => void
|
|
|
|
export interface INeovimEvent {
|
|
eventName: string
|
|
eventContext: EventContext
|
|
}
|
|
|
|
export interface INeovimInstance {
|
|
cursorPosition: IPosition
|
|
quickFix: IQuickFixList
|
|
|
|
// Events
|
|
onYank: IEvent<INeovimYankInfo>
|
|
|
|
onBufferUpdate: IEvent<INeovimBufferUpdate>
|
|
|
|
onRedrawComplete: IEvent<void>
|
|
|
|
onScroll: IEvent<EventContext>
|
|
|
|
onTitleChanged: IEvent<string>
|
|
|
|
// When an OniCommand is requested, ie :OniCommand("quickOpen.show")
|
|
onOniCommand: IEvent<CommandContext>
|
|
|
|
onHidePopupMenu: IEvent<void>
|
|
onShowPopupMenu: IEvent<INeovimCompletionInfo>
|
|
|
|
onColorsChanged: IEvent<void>
|
|
|
|
onCommandLineShow: IEvent<INeovimCommandLineShowEvent>
|
|
onCommandLineHide: IEvent<void>
|
|
onCommandLineSetCursorPosition: IEvent<INeovimCommandLineSetCursorPosition>
|
|
|
|
onMessage: IEvent<IMessageInfo>
|
|
|
|
onVimEvent: IEvent<INeovimEvent>
|
|
|
|
autoCommands: INeovimAutoCommands
|
|
marks: INeovimMarks
|
|
|
|
screenToPixels(row: number, col: number): IPixelPosition
|
|
|
|
/**
|
|
* Supply input (keyboard/mouse) to Neovim
|
|
*/
|
|
input(inputString: string): Promise<void>
|
|
|
|
/**
|
|
* Call a VimL function
|
|
*/
|
|
callFunction(functionName: string, args: any[]): Promise<any>
|
|
|
|
/**
|
|
* Change the working directory of Neovim
|
|
*/
|
|
chdir(directoryPath: string): Promise<any>
|
|
|
|
/**
|
|
* Execute a VimL command
|
|
*/
|
|
command(command: string): Promise<any>
|
|
|
|
/**
|
|
* Evaluate a VimL block
|
|
*/
|
|
eval(expression: string): Promise<any>
|
|
|
|
// TODO:
|
|
// - Refactor remaining events into strongly typed events, as part of the interface
|
|
on(event: string, handler: NeovimEventHandler): void
|
|
|
|
setFont(fontFamily: string, fontSize: string, fontWeight: string, linePadding: number): void
|
|
|
|
getBufferIds(): Promise<number[]>
|
|
|
|
getApiVersion(): Promise<INeovimApiVersion>
|
|
|
|
open(fileName: string): Promise<void>
|
|
openInitVim(): Promise<void>
|
|
}
|
|
|
|
/**
|
|
* Integration with NeoVim API
|
|
*/
|
|
export class NeovimInstance extends EventEmitter implements INeovimInstance {
|
|
private _neovim: Session
|
|
private _isDisposed: boolean = false
|
|
private _initPromise: Promise<void>
|
|
private _isLeaving: boolean
|
|
private _currentVimDirectory: string
|
|
|
|
private _inputQueue: PromiseQueue = new PromiseQueue()
|
|
|
|
private _configuration: Configuration
|
|
private _autoCommands: NeovimAutoCommands
|
|
|
|
private _fontFamily: string
|
|
private _fontSize: string
|
|
private _fontWeight: string
|
|
private _fontWidthInPixels: number
|
|
private _fontHeightInPixels: number
|
|
|
|
private _lastHeightInPixels: number
|
|
private _lastWidthInPixels: number
|
|
|
|
private _rows: number
|
|
private _cols: number
|
|
|
|
private _quickFix: QuickFixList
|
|
private _marks: NeovimMarks
|
|
private _initComplete: boolean
|
|
|
|
private _onDirectoryChanged = new Event<string>()
|
|
private _onErrorEvent = new Event<Error | string>()
|
|
private _onYank = new Event<INeovimYankInfo>()
|
|
private _onOniCommand = new Event<CommandContext>()
|
|
private _onRedrawComplete = new Event<void>()
|
|
private _onScroll = new Event<EventContext>()
|
|
private _onTitleChanged = new Event<string>()
|
|
private _onModeChanged = new Event<Oni.Vim.Mode>()
|
|
private _onHidePopupMenu = new Event<void>()
|
|
private _onShowPopupMenu = new Event<INeovimCompletionInfo>()
|
|
private _onSelectPopupMenu = new Event<number>()
|
|
private _onLeave = new Event<void>()
|
|
private _onMessage = new Event<IMessageInfo>()
|
|
|
|
private _onColorsChanged = new Event<void>()
|
|
|
|
private _onCommandLineShowEvent = new Event<INeovimCommandLineShowEvent>()
|
|
private _onCommandLineHideEvent = new Event<void>()
|
|
private _onCommandLineSetCursorPositionEvent = new Event<INeovimCommandLineSetCursorPosition>()
|
|
private _onVimEvent = new Event<INeovimEvent>()
|
|
private _onWildMenuHideEvent = new Event<void>()
|
|
private _onWildMenuSelectEvent = new Event<IWildMenuSelectEvent>()
|
|
private _onWildMenuShowEvent = new Event<IWildMenuShowEvent>()
|
|
private _bufferUpdateManager: NeovimBufferUpdateManager
|
|
private _tokenColorSynchronizer: NeovimTokenColorSynchronizer
|
|
|
|
private _pendingScrollTimeout: number | null = null
|
|
|
|
private _disposables: IDisposable[] = []
|
|
|
|
public get isInitialized(): boolean {
|
|
return this._initComplete
|
|
}
|
|
|
|
public get quickFix(): IQuickFixList {
|
|
return this._quickFix
|
|
}
|
|
|
|
public get onBufferUpdate(): IEvent<INeovimBufferUpdate> {
|
|
return this._bufferUpdateManager.onBufferUpdate
|
|
}
|
|
|
|
public get onColorsChanged(): IEvent<void> {
|
|
return this._onColorsChanged
|
|
}
|
|
|
|
public get onDirectoryChanged(): IEvent<string> {
|
|
return this._onDirectoryChanged
|
|
}
|
|
|
|
public get onError(): IEvent<Error | string> {
|
|
return this._onErrorEvent
|
|
}
|
|
|
|
public get onLeave(): IEvent<void> {
|
|
return this._onLeave
|
|
}
|
|
|
|
public get onMessage(): IEvent<IMessageInfo> {
|
|
return this._onMessage
|
|
}
|
|
|
|
public get onModeChanged(): IEvent<Oni.Vim.Mode> {
|
|
return this._onModeChanged
|
|
}
|
|
|
|
public get onOniCommand(): IEvent<CommandContext> {
|
|
return this._onOniCommand
|
|
}
|
|
|
|
public get onRedrawComplete(): IEvent<void> {
|
|
return this._onRedrawComplete
|
|
}
|
|
|
|
public get onScroll(): IEvent<EventContext> {
|
|
return this._onScroll
|
|
}
|
|
|
|
public get onTitleChanged(): IEvent<string> {
|
|
return this._onTitleChanged
|
|
}
|
|
|
|
public get onHidePopupMenu(): IEvent<void> {
|
|
return this._onHidePopupMenu
|
|
}
|
|
|
|
public get onSelectPopupMenu(): IEvent<number> {
|
|
return this._onSelectPopupMenu
|
|
}
|
|
|
|
public get onShowPopupMenu(): IEvent<INeovimCompletionInfo> {
|
|
return this._onShowPopupMenu
|
|
}
|
|
|
|
public get onCommandLineShow(): IEvent<INeovimCommandLineShowEvent> {
|
|
return this._onCommandLineShowEvent
|
|
}
|
|
|
|
public get onCommandLineHide(): IEvent<void> {
|
|
return this._onCommandLineHideEvent
|
|
}
|
|
|
|
public get onCommandLineSetCursorPosition(): IEvent<INeovimCommandLineSetCursorPosition> {
|
|
return this._onCommandLineSetCursorPositionEvent
|
|
}
|
|
|
|
public get onVimEvent(): IEvent<INeovimEvent> {
|
|
return this._onVimEvent
|
|
}
|
|
|
|
public get onWildMenuShow(): IEvent<IWildMenuShowEvent> {
|
|
return this._onWildMenuShowEvent
|
|
}
|
|
|
|
public get onWildMenuSelect(): IEvent<IWildMenuSelectEvent> {
|
|
return this._onWildMenuSelectEvent
|
|
}
|
|
|
|
public get onWildMenuHide(): IEvent<void> {
|
|
return this._onWildMenuHideEvent
|
|
}
|
|
|
|
public get onYank(): IEvent<INeovimYankInfo> {
|
|
return this._onYank
|
|
}
|
|
|
|
public get autoCommands(): INeovimAutoCommands {
|
|
return this._autoCommands
|
|
}
|
|
|
|
public get marks(): INeovimMarks {
|
|
return this._marks
|
|
}
|
|
|
|
public get tokenColorSynchronizer(): NeovimTokenColorSynchronizer {
|
|
return this._tokenColorSynchronizer
|
|
}
|
|
|
|
public get currentVimDirectory(): string {
|
|
return this._currentVimDirectory
|
|
}
|
|
|
|
constructor(widthInPixels: number, heightInPixels: number, configuration: Configuration) {
|
|
super()
|
|
this._configuration = configuration
|
|
this._fontFamily = this._configuration.getValue("editor.fontFamily")
|
|
this._fontSize = addDefaultUnitIfNeeded(this._configuration.getValue("editor.fontSize"))
|
|
this._fontWeight = this._configuration.getValue("editor.fontWeight")
|
|
|
|
this._lastWidthInPixels = widthInPixels
|
|
this._lastHeightInPixels = heightInPixels
|
|
|
|
this._quickFix = new QuickFixList(this)
|
|
this._autoCommands = new NeovimAutoCommands(this)
|
|
this._marks = new NeovimMarks(this)
|
|
this._tokenColorSynchronizer = new NeovimTokenColorSynchronizer(this)
|
|
|
|
this._bufferUpdateManager = new NeovimBufferUpdateManager(this._configuration, this)
|
|
|
|
const s1 = this._onModeChanged.subscribe(newMode => {
|
|
this._bufferUpdateManager.notifyModeChanged(newMode)
|
|
})
|
|
|
|
const dispatchScroll = () => this._dispatchScrollEvent()
|
|
|
|
const s2 = this._autoCommands.onCursorMoved.subscribe(dispatchScroll)
|
|
const s3 = this._autoCommands.onCursorMovedI.subscribe(dispatchScroll)
|
|
|
|
this._disposables = [s1, s2, s3]
|
|
}
|
|
|
|
public dispose(): void {
|
|
if (this._neovim) {
|
|
this._neovim.dispose()
|
|
this._neovim = null
|
|
}
|
|
|
|
if (this._disposables) {
|
|
this._disposables.forEach(d => d.dispose())
|
|
}
|
|
|
|
this._configuration = null
|
|
}
|
|
|
|
public async chdir(directoryPath: string): Promise<void> {
|
|
await this.command(`cd! ${directoryPath}`)
|
|
}
|
|
|
|
public async checkUserMapping({ key, mode = "n" }: IMapping) {
|
|
const mapping: string = await this.callFunction("mapcheck", [key, mode])
|
|
return { key, mapping }
|
|
}
|
|
|
|
public async checkUserMappings(keys: IMapping[]) {
|
|
const mappings = await Promise.all(keys.map(this.checkUserMapping))
|
|
return mappings
|
|
}
|
|
|
|
// Make a direct request against the msgpack API
|
|
public async request<T>(request: string, args: any[]): Promise<T> {
|
|
if (!this._neovim || this._neovim.isDisposed) {
|
|
Log.warn("[NeovimInstance::request] Attempted to request on a disposed neovim instance")
|
|
return null
|
|
}
|
|
|
|
return this._neovim.request<T>(request, args)
|
|
}
|
|
|
|
public async getContext(): Promise<EventContext> {
|
|
return this.callFunction("OniGetContext", [])
|
|
}
|
|
|
|
public async start(startOptions?: INeovimStartOptions): Promise<void> {
|
|
Performance.startMeasure("NeovimInstance.Start")
|
|
this._initPromise = startNeovim(startOptions).then(async nv => {
|
|
Log.info("NeovimInstance: Neovim started")
|
|
|
|
// Workaround for issue where UI
|
|
// can fail to attach if there is a UI-blocking error
|
|
// nv.input("<ESC>")
|
|
|
|
this._neovim = nv
|
|
|
|
this._neovim.on("error", (err: Error) => {
|
|
this._onError(err)
|
|
})
|
|
|
|
this._neovim.on("notification", (method: any, args: any) =>
|
|
this._onNotification(method, args),
|
|
)
|
|
|
|
this._neovim.on("request", (method: any, _args: any, _resp: any) => {
|
|
Log.warn("Unhandled request: " + method)
|
|
})
|
|
|
|
this._neovim.on("disconnect", () => {
|
|
if (!this._isLeaving) {
|
|
this._onError(
|
|
"Neovim disconnected. This likely means that the Neovim process crashed.",
|
|
)
|
|
}
|
|
})
|
|
|
|
await this._checkAndFixIfBlocked()
|
|
|
|
const size = this._getSize()
|
|
this._rows = size.rows
|
|
this._cols = size.cols
|
|
|
|
// Workaround for bug in neovim/node-client
|
|
// The 'uiAttach' method overrides the new 'nvim_ui_attach' method
|
|
Performance.startMeasure("NeovimInstance.Start.Attach")
|
|
return this._attachUI(size.cols, size.rows).then(
|
|
async () => {
|
|
Log.info("Attach success")
|
|
Performance.endMeasure("NeovimInstance.Start.Attach")
|
|
|
|
// TODO: #702 - Batch these calls via `nvim_call_atomic`
|
|
// Override completeopt so Oni works correctly with external popupmenu
|
|
// await this.command("set completeopt=longest,menu")
|
|
|
|
// set title after attaching listeners so we can get the initial title
|
|
await this.command("set title")
|
|
|
|
Performance.endMeasure("NeovimInstance.Start")
|
|
|
|
this._initComplete = true
|
|
},
|
|
(err: any) => {
|
|
this._onError(err)
|
|
this._initComplete = true
|
|
},
|
|
)
|
|
})
|
|
|
|
return this._initPromise
|
|
}
|
|
|
|
public async getTokenColors(): Promise<TokenColor[]> {
|
|
const vimHighlights = Object.keys(VimHighlightToDefaultScope)
|
|
const atomicCalls = vimHighlights.map(highlight => {
|
|
return ["nvim_get_hl_by_name", [highlight, 1]]
|
|
})
|
|
|
|
const [highlightInfo] = await this.request<[IVimHighlight[]]>("nvim_call_atomic", [
|
|
atomicCalls,
|
|
])
|
|
|
|
const ret = highlightInfo.reduce(
|
|
(prev: TokenColor[], currentValue: IVimHighlight, currentIndex: number) => {
|
|
const highlightGroupName = vimHighlights[currentIndex]
|
|
const settings = vimHighlightToTokenColorStyle(currentValue)
|
|
const newScopeNames: string[] = VimHighlightToDefaultScope[highlightGroupName] || []
|
|
|
|
const newScopes = newScopeNames.map((scope): TokenColor => ({
|
|
scope,
|
|
settings,
|
|
}))
|
|
|
|
return [...prev, ...newScopes]
|
|
},
|
|
[] as TokenColor[],
|
|
)
|
|
|
|
return ret
|
|
}
|
|
|
|
public setFont(
|
|
fontFamily: string,
|
|
fontSize: string,
|
|
fontWeight: string,
|
|
linePadding: number,
|
|
): void {
|
|
this._fontFamily = fontFamily
|
|
this._fontSize = fontSize
|
|
this._fontWeight = fontWeight
|
|
|
|
const { width, height, isBoldAvailable, isItalicAvailable } = measureFont(
|
|
this._fontFamily,
|
|
this._fontSize,
|
|
this._fontWeight,
|
|
)
|
|
|
|
this._fontWidthInPixels = width
|
|
this._fontHeightInPixels = height + linePadding
|
|
|
|
this.emit(
|
|
"action",
|
|
Actions.setFont({
|
|
fontFamily,
|
|
fontSize,
|
|
fontWeight,
|
|
fontWidthInPixels: width,
|
|
fontHeightInPixels: height + linePadding,
|
|
linePaddingInPixels: linePadding,
|
|
isItalicAvailable,
|
|
isBoldAvailable,
|
|
}),
|
|
)
|
|
|
|
this.resize(this._lastWidthInPixels, this._lastHeightInPixels)
|
|
}
|
|
|
|
public open(fileName: string): Promise<void> {
|
|
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
|
|
*/
|
|
public getInitVimPath(): string {
|
|
// tslint:disable no-string-literal
|
|
const MYVIMRC = process.env["MYVIMRC"]
|
|
const rootFolder = Platform.isWindows()
|
|
? // Use path from: https://github.com/neovim/neovim/wiki/FAQ
|
|
path.join(process.env["LOCALAPPDATA"], "nvim")
|
|
: path.join(Platform.getUserHome(), ".config", "nvim")
|
|
const initVimPath = MYVIMRC || path.join(rootFolder, "init.vim")
|
|
return initVimPath
|
|
// tslint:enable no-string-literal
|
|
}
|
|
|
|
/**
|
|
* doesInitVimExist
|
|
* Returns the init.vim path after checking the file exists
|
|
*/
|
|
public doesInitVimExist(): string {
|
|
const initVimPath = this.getInitVimPath()
|
|
try {
|
|
return pathExistsSync(initVimPath) ? initVimPath : null
|
|
} catch (e) {
|
|
return null
|
|
}
|
|
}
|
|
|
|
public openInitVim(): Promise<void> {
|
|
const loadInitVim = this._configuration.getValue("oni.loadInitVim")
|
|
|
|
if (typeof loadInitVim === "string") {
|
|
return this.open(loadInitVim)
|
|
} else {
|
|
const initVimPath = this.getInitVimPath()
|
|
const rootFolder = path.dirname(initVimPath)
|
|
mkdirp.sync(rootFolder)
|
|
|
|
return this.open(initVimPath)
|
|
}
|
|
}
|
|
|
|
public eval<T>(expression: string): Promise<T> {
|
|
return this.request("nvim_eval", [expression])
|
|
}
|
|
|
|
public command(command: string): Promise<any> {
|
|
Log.verbose("[NeovimInstance] Executing command: " + command)
|
|
return this.request<any>("nvim_command", [command])
|
|
}
|
|
|
|
public callFunction(functionName: string, args: any[]): Promise<any> {
|
|
return this.request<void>("nvim_call_function", [functionName, args])
|
|
}
|
|
|
|
public async getBufferIds(): Promise<number[]> {
|
|
const buffers = await this.request<NeovimBufferReference[]>("nvim_list_bufs", [])
|
|
|
|
return buffers.map(b => b.id as any)
|
|
}
|
|
|
|
public async getCurrentWorkingDirectory(): Promise<string> {
|
|
const currentWorkingDirectory = await this.eval<string>("getcwd()")
|
|
return path.normalize(currentWorkingDirectory)
|
|
}
|
|
|
|
public get cursorPosition(): IPosition {
|
|
return {
|
|
row: 0,
|
|
column: 0,
|
|
}
|
|
}
|
|
|
|
public screenToPixels(_row: number, _col: number): IPixelPosition {
|
|
return {
|
|
x: 0,
|
|
y: 0,
|
|
}
|
|
}
|
|
|
|
public input(inputString: string): Promise<void> {
|
|
return this._inputQueue.enqueuePromise(async () => {
|
|
await this._neovim.request("nvim_input", [inputString])
|
|
})
|
|
}
|
|
|
|
public blockInput(func: (opt?: any) => Promise<void>): void {
|
|
const forceInput = (inputString: string) => {
|
|
return this._neovim.request("nvim_input", [inputString])
|
|
}
|
|
|
|
this._inputQueue.enqueuePromise(async () => {
|
|
await func(forceInput)
|
|
})
|
|
}
|
|
|
|
public resize(widthInPixels: number, heightInPixels: number): void {
|
|
this._lastWidthInPixels = widthInPixels
|
|
this._lastHeightInPixels = heightInPixels
|
|
|
|
const size = this._getSize()
|
|
|
|
this._resizeInternal(size.rows, size.cols)
|
|
}
|
|
|
|
public async getApiVersion(): Promise<INeovimApiVersion> {
|
|
const versionInfo = await this._neovim.request("nvim_get_api_info", [])
|
|
return versionInfo[1].version as any
|
|
}
|
|
|
|
public async quit(): Promise<void> {
|
|
// This command won't resolve the promise (since it's quitting),
|
|
// so we're not awaiting..
|
|
// TODO: Is there a way we can deterministically resolve the promise? Like use the `VimLeave` event?
|
|
this.command(":qa!")
|
|
}
|
|
|
|
private async _checkAndFixIfBlocked(): Promise<void> {
|
|
Log.info("[NeovimInstance::_checkAndFixIfBlocked] checking mode...")
|
|
const mode: any = await this._neovim.request("nvim_get_mode", [])
|
|
|
|
if (mode && mode.blocking) {
|
|
Log.info("[NeovimInstance::_checkAndFixIfBlocked] mode is blocking, attempt to cancel.")
|
|
// The UI is blocked on some error message.
|
|
// Let's grab the message and show it, and unblock the UI
|
|
await this.input("<esc>")
|
|
const output = await this._neovim.request<string>("nvim_command_output", [":messages"])
|
|
Log.info("[NeovimInstance::_checkAndFixIfBlocked] sent esc, getting command")
|
|
|
|
this._onMessage.dispatch({
|
|
severity: "error",
|
|
title: "Problem loading `init.vim`:",
|
|
details: output,
|
|
})
|
|
} else {
|
|
Log.info("[NeovimInstance::_checkAndFixIfBlocked] Not blocking mode.")
|
|
}
|
|
}
|
|
|
|
private _dispatchScrollEvent(): void {
|
|
if (this._pendingScrollTimeout || this._isDisposed) {
|
|
return
|
|
}
|
|
|
|
this._pendingScrollTimeout = window.setTimeout(async () => {
|
|
if (this._isDisposed) {
|
|
return
|
|
}
|
|
|
|
const evt = await this.getContext()
|
|
this._onScroll.dispatch(evt)
|
|
this._pendingScrollTimeout = null
|
|
})
|
|
}
|
|
|
|
private _resizeInternal(rows: number, columns: number): void {
|
|
if (this._configuration.hasValue("debug.fixedSize")) {
|
|
const fixedSize = this._configuration.getValue("debug.fixedSize")
|
|
rows = fixedSize.rows
|
|
columns = fixedSize.columns
|
|
Log.warn("Overriding screen size based on debug.fixedSize")
|
|
}
|
|
|
|
if (rows === this._rows && columns === this._cols) {
|
|
return
|
|
}
|
|
|
|
this._rows = rows
|
|
this._cols = columns
|
|
|
|
// If _initPromise isn't initialized, it means the UI hasn't attached to NeoVim
|
|
// yet. In that case, we don't need to call uiTryResize
|
|
if (!this._initPromise) {
|
|
return
|
|
}
|
|
|
|
this._initPromise.then(() => {
|
|
return this._neovim.request("nvim_ui_try_resize", [columns, rows])
|
|
})
|
|
}
|
|
|
|
private _getSize() {
|
|
const rows = Math.floor(this._lastHeightInPixels / this._fontHeightInPixels)
|
|
const cols = Math.floor(this._lastWidthInPixels / this._fontWidthInPixels)
|
|
return { rows, cols }
|
|
}
|
|
|
|
private _handleNotification(_method: any, args: any): void {
|
|
if (this._isDisposed) {
|
|
Log.warn(`[NeovimInstance] - ignoring ${_method} because disposed`)
|
|
return
|
|
}
|
|
|
|
args.forEach((a: any[]) => {
|
|
const command = a[0]
|
|
a = a.slice(1)
|
|
|
|
switch (command) {
|
|
case "cursor_goto":
|
|
this.emit("action", Actions.createCursorGotoAction(a[0][0], a[0][1]))
|
|
break
|
|
case "put":
|
|
const charactersToPut = a.map(v => v[0])
|
|
this.emit("action", Actions.put(charactersToPut))
|
|
break
|
|
case "set_scroll_region":
|
|
const param = a[0]
|
|
this.emit(
|
|
"action",
|
|
Actions.setScrollRegion(param[0], param[1], param[2], param[3]),
|
|
)
|
|
break
|
|
case "scroll":
|
|
this.emit("action", Actions.scroll(a[0][0]))
|
|
this._dispatchScrollEvent()
|
|
break
|
|
case "highlight_set":
|
|
const highlightInfo = a[a.length - 1][0]
|
|
|
|
this.emit(
|
|
"action",
|
|
Actions.setHighlight(
|
|
!!highlightInfo.bold,
|
|
!!highlightInfo.italic,
|
|
!!highlightInfo.reverse,
|
|
!!highlightInfo.underline,
|
|
!!highlightInfo.undercurl,
|
|
highlightInfo.foreground,
|
|
highlightInfo.background,
|
|
),
|
|
)
|
|
break
|
|
case "resize":
|
|
this.emit("action", Actions.resize(a[0][0], a[0][1]))
|
|
break
|
|
case "set_title":
|
|
this._onTitleChanged.dispatch(a[0][0])
|
|
break
|
|
case "set_icon":
|
|
// window title when minimized, no-op
|
|
break
|
|
case "eol_clear":
|
|
this.emit("action", Actions.clearToEndOfLine())
|
|
break
|
|
case "clear":
|
|
this.emit("action", Actions.clear())
|
|
break
|
|
case "mouse_on":
|
|
// TODO
|
|
break
|
|
case "update_bg":
|
|
this.emit("action", Actions.updateBackground(a[0][0]))
|
|
break
|
|
case "update_fg":
|
|
this.emit("action", Actions.updateForeground(a[0][0]))
|
|
break
|
|
case "mode_change":
|
|
const newMode = a[a.length - 1][0]
|
|
this.emit("action", Actions.changeMode(newMode))
|
|
this._onModeChanged.dispatch(newMode as Oni.Vim.Mode)
|
|
break
|
|
case "popupmenu_select":
|
|
this._onSelectPopupMenu.dispatch(a[0][0])
|
|
break
|
|
case "popupmenu_hide":
|
|
this._onHidePopupMenu.dispatch()
|
|
break
|
|
case "popupmenu_show":
|
|
const [items, selected, row, col] = a[0]
|
|
|
|
const mappedItems = items.map((item: string[]) => {
|
|
const [word, kind, menu, info] = item
|
|
return {
|
|
word,
|
|
kind,
|
|
menu,
|
|
info,
|
|
}
|
|
})
|
|
|
|
const completionInfo: INeovimCompletionInfo = {
|
|
items: mappedItems,
|
|
selectedIndex: selected,
|
|
row,
|
|
col,
|
|
}
|
|
|
|
this._onShowPopupMenu.dispatch(completionInfo)
|
|
break
|
|
case "tabline_update":
|
|
const [currentTab, tabs] = a[0]
|
|
const mappedTabs: any = tabs.map((t: any) => ({
|
|
id: t.tab.id,
|
|
name: t.name,
|
|
}))
|
|
this.emit("tabline-update", currentTab.id, mappedTabs)
|
|
break
|
|
case "bell":
|
|
const bellUrl = this._configuration.getValue("oni.audio.bellUrl")
|
|
if (bellUrl) {
|
|
const audio = new Audio(bellUrl)
|
|
audio.play()
|
|
}
|
|
break
|
|
case "cmdline_show":
|
|
{
|
|
const [content, pos, firstc, prompt, indent, level] = a[0]
|
|
const commandLineShowInfo: INeovimCommandLineShowEvent = {
|
|
content,
|
|
pos,
|
|
firstc,
|
|
prompt,
|
|
indent,
|
|
level,
|
|
}
|
|
this._onCommandLineShowEvent.dispatch(commandLineShowInfo)
|
|
}
|
|
break
|
|
case "cmdline_pos":
|
|
{
|
|
const [pos, level] = a[0]
|
|
const commandLinePositionInfo: INeovimCommandLineSetCursorPosition = {
|
|
pos,
|
|
level,
|
|
}
|
|
this._onCommandLineSetCursorPositionEvent.dispatch(commandLinePositionInfo)
|
|
}
|
|
break
|
|
case "cmdline_hide":
|
|
this._onCommandLineHideEvent.dispatch()
|
|
break
|
|
case "wildmenu_show":
|
|
const [[options]] = a
|
|
this._onWildMenuShowEvent.dispatch({ options })
|
|
break
|
|
case "wildmenu_select":
|
|
const [[selection]] = a
|
|
this._onWildMenuSelectEvent.dispatch({ selected: selection })
|
|
break
|
|
case "wildmenu_hide":
|
|
this._onWildMenuHideEvent.dispatch()
|
|
break
|
|
case "update_sp":
|
|
case "mode_info_set":
|
|
case "busy_start":
|
|
case "busy_stop":
|
|
Log.verbose("Ignore command: " + command)
|
|
break
|
|
default:
|
|
Log.warn("Unhandled command: " + command)
|
|
}
|
|
})
|
|
}
|
|
|
|
private _onError(error: Error | string): void {
|
|
Log.error(error)
|
|
this._onErrorEvent.dispatch(error)
|
|
}
|
|
|
|
private _onNotification(method: string, args: any): void {
|
|
if (method === "redraw") {
|
|
this._handleNotification(method, args)
|
|
this._onRedrawComplete.dispatch()
|
|
} else if (method === "oni_plugin_notify") {
|
|
const pluginArgs = args[0]
|
|
const pluginMethod = pluginArgs.shift()
|
|
|
|
// TODO: Update pluginManager to subscribe from event here, instead of dupliating this
|
|
|
|
if (pluginMethod === "buffer_update") {
|
|
const eventContext: EventContext = args[0][0]
|
|
|
|
this._bufferUpdateManager.notifyFullBufferUpdate(eventContext)
|
|
} else if (pluginMethod === "oni_yank") {
|
|
this._onYank.dispatch(args[0][0])
|
|
} else if (pluginMethod === "oni_command") {
|
|
this._onOniCommand.dispatch(args[0][0])
|
|
} else if (pluginMethod === "event") {
|
|
const eventName = args[0][0]
|
|
const eventContext = args[0][1]
|
|
|
|
if (eventName === "DirChanged") {
|
|
this._updateProcessDirectory()
|
|
} else if (eventName === "VimLeave") {
|
|
this._isLeaving = true
|
|
this._onLeave.dispatch()
|
|
} else if (eventName === "ColorScheme") {
|
|
this._onColorsChanged.dispatch()
|
|
}
|
|
|
|
this._autoCommands.notifyAutocommand(eventName, eventContext)
|
|
|
|
this._dispatchEvent(eventName, eventContext)
|
|
} else if (pluginMethod === "incremental_buffer_update") {
|
|
const eventContext = args[0][0]
|
|
const lineContents = args[0][1]
|
|
const lineNumber = args[0][2]
|
|
|
|
this._bufferUpdateManager.notifyIncrementalBufferUpdate(
|
|
eventContext,
|
|
lineNumber,
|
|
lineContents,
|
|
)
|
|
} else {
|
|
Log.warn("Unknown event from oni_plugin_notify: " + pluginMethod)
|
|
}
|
|
} else {
|
|
Log.warn("Unknown notification: " + method)
|
|
}
|
|
}
|
|
|
|
private _dispatchEvent(eventName: string, context: any): void {
|
|
const eventContext: EventContext = context.current || context
|
|
this._onVimEvent.dispatch({
|
|
eventName,
|
|
eventContext,
|
|
})
|
|
|
|
// TODO: Remove this
|
|
this.emit("event", eventName, eventContext)
|
|
}
|
|
|
|
private async _updateProcessDirectory(): Promise<void> {
|
|
this._currentVimDirectory = await this.getCurrentWorkingDirectory()
|
|
this._onDirectoryChanged.dispatch(this._currentVimDirectory)
|
|
}
|
|
|
|
private async _attachUI(columns: number, rows: number): Promise<void> {
|
|
const version = await this.getApiVersion()
|
|
|
|
const useNativeTabs = this._configuration.getValue("tabs.mode") === "native"
|
|
const useNativePopupWindows =
|
|
this._configuration.getValue("editor.completions.mode") === "native"
|
|
|
|
const externaliseTabline = !useNativeTabs
|
|
const externalisePopupWindows = !useNativePopupWindows
|
|
|
|
Log.info(
|
|
`[NeovimInstance::_attachUI] Neovim version reported as ${version.major}.${
|
|
version.minor
|
|
}.${version.patch}`,
|
|
) // tslint:disable-line no-console
|
|
|
|
const startupOptions = this._getStartupOptionsForVersion(
|
|
version.major,
|
|
version.minor,
|
|
version.patch,
|
|
externaliseTabline,
|
|
externalisePopupWindows,
|
|
)
|
|
|
|
Log.info(
|
|
`[NeovimInstance::_attachUI] Using startup options: ${JSON.stringify(
|
|
startupOptions,
|
|
)} and size: ${columns}, ${rows}`,
|
|
)
|
|
|
|
await this._neovim.request("nvim_ui_attach", [columns, rows, startupOptions])
|
|
}
|
|
|
|
private _getStartupOptionsForVersion(
|
|
major: number,
|
|
minor: number,
|
|
patch: number,
|
|
shouldExtTabs: boolean,
|
|
shouldExtPopups: boolean,
|
|
) {
|
|
if (major > 0 || minor > 2 || (minor === 2 && patch >= 1)) {
|
|
const useExtCmdLine = this._configuration.getValue("commandline.mode")
|
|
const useExtWildMenu = this._configuration.getValue("wildmenu.mode")
|
|
return {
|
|
rgb: true,
|
|
popupmenu_external: shouldExtPopups,
|
|
ext_tabline: shouldExtTabs,
|
|
ext_cmdline: useExtCmdLine,
|
|
ext_wildmenu: useExtWildMenu,
|
|
}
|
|
} else if (major === 0 && minor === 2) {
|
|
// 0.1 and below does not support external tabline
|
|
// See #579 for more info on the manifestation.
|
|
return {
|
|
rgb: true,
|
|
popupmenu_external: shouldExtPopups,
|
|
}
|
|
} else {
|
|
throw new Error("Unsupported version of Neovim.")
|
|
}
|
|
}
|
|
}
|