Skip to content
This repository has been archived by the owner on Apr 1, 2020. It is now read-only.

API - Completion Providers #1329

Merged
merged 12 commits into from
Jan 24, 2018
5 changes: 3 additions & 2 deletions browser/src/Editor/NeovimEditor/NeovimEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ import { PluginManager } from "./../../Plugins/PluginManager"

import { IColors } from "./../../Services/Colors"
import { commandManager } from "./../../Services/CommandManager"
import { Completion } from "./../../Services/Completion"
import { Completion, CompletionProviders } from "./../../Services/Completion"
import { Configuration, IConfigurationValues } from "./../../Services/Configuration"
import { IDiagnosticsDataSource } from "./../../Services/Diagnostics"
import { Errors } from "./../../Services/Errors"
Expand Down Expand Up @@ -135,6 +135,7 @@ export class NeovimEditor extends Editor implements IEditor {

constructor(
private _colors: IColors,
private _completionProviders: CompletionProviders,
private _configuration: Configuration,
private _diagnostics: IDiagnosticsDataSource,
private _languageManager: LanguageManager,
Expand Down Expand Up @@ -403,7 +404,7 @@ export class NeovimEditor extends Editor implements IEditor {
const textMateHighlightingEnabled = this._configuration.getValue("experimental.editor.textMateHighlighting.enabled")
this._syntaxHighlighter = textMateHighlightingEnabled ? new SyntaxHighlighter(this._configuration, this) : new NullSyntaxHighlighter()

this._completion = new Completion(this, this._languageManager, this._configuration)
this._completion = new Completion(this, this._configuration, this._completionProviders, this._languageManager)
this._completionMenu = new CompletionMenu(this._contextMenuManager.create())

this._completion.onShowCompletionItems.subscribe((completions) => {
Expand Down
10 changes: 8 additions & 2 deletions browser/src/Plugins/Api/Oni.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { Ui } from "./Ui"
import { automation } from "./../../Services/Automation"
import { Colors, getInstance as getColors } from "./../../Services/Colors"
import { commandManager } from "./../../Services/CommandManager"
import { getInstance as getCompletionProvidersInstance } from "./../../Services/Completion/CompletionProviders"
import { configuration } from "./../../Services/Configuration"
import { getInstance as getDiagnosticsInstance } from "./../../Services/Diagnostics"
import { editorManager } from "./../../Services/EditorManager"
Expand Down Expand Up @@ -80,8 +81,9 @@ export class Oni extends EventEmitter implements OniApi.Plugin.Api {
return recorder
}

public get snippets(): any {
return getSnippetsInstance()
public get completions(): any {
return getCompletionProvidersInstance()

}

public get configuration(): OniApi.Configuration {
Expand Down Expand Up @@ -124,6 +126,10 @@ export class Oni extends EventEmitter implements OniApi.Plugin.Api {
return getSidebarInstance()
}

public get snippets(): any {
return getSnippetsInstance()
}

public get statusBar(): OniApi.StatusBar {
return getStatusBarInstance()
}
Expand Down
26 changes: 20 additions & 6 deletions browser/src/Services/Completion/Completion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,22 +7,36 @@ import { Event, IDisposable, IEvent } from "oni-types"
import { Store, Unsubscribe } from "redux"
import * as types from "vscode-languageserver-types"

import { LanguageManager } from "./../Language"

import { getFilteredCompletions } from "./CompletionSelectors"
import { ICompletionsRequestor, LanguageServiceCompletionsRequestor } from "./CompletionsRequestor"
import { ICompletionsRequestor } from "./CompletionsRequestor"

import { ICompletionState } from "./CompletionState"

import { createStore } from "./CompletionStore"

import { Configuration } from "./../Configuration"
import { LanguageManager } from "./../Language"
import * as CompletionUtility from "./CompletionUtility"

export interface ICompletionShowEventArgs {
filteredCompletions: types.CompletionItem[]
base: string
}

export class TestRequestor implements ICompletionsRequestor {
public async getCompletions(language: string, filePath: string, line: number, column: number): Promise<types.CompletionItem[]> {
return [
types.CompletionItem.create("test1"),
types.CompletionItem.create("test2"),
]
}

public async getCompletionDetails(language: string, filePath: string, completionItem: types.CompletionItem): Promise<types.CompletionItem> {
return completionItem
}
}

export class Completion implements IDisposable {

private _lastCursorPosition: Oni.Cursor
Expand All @@ -43,12 +57,12 @@ export class Completion implements IDisposable {

constructor(
private _editor: Oni.Editor,
private _languageManager: LanguageManager,
private _configuration: Configuration,
private _completionsRequestor?: ICompletionsRequestor,
private _completionsRequestor: ICompletionsRequestor,
private _languageManager: LanguageManager,
) {
this._completionsRequestor = this._completionsRequestor || new LanguageServiceCompletionsRequestor(this._languageManager)
this._store = createStore(this._languageManager, this._configuration, this._completionsRequestor)
this._completionsRequestor = this._completionsRequestor
this._store = createStore(this._editor, this._languageManager, this._configuration, this._completionsRequestor)

const sub1 = this._editor.onBufferEnter.subscribe((buf: Oni.Buffer) => {
this._onBufferEnter(buf)
Expand Down
25 changes: 0 additions & 25 deletions browser/src/Services/Completion/CompletionProvider.ts

This file was deleted.

85 changes: 85 additions & 0 deletions browser/src/Services/Completion/CompletionProviders.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/**
* CompletionProviders.ts
*/

import * as types from "vscode-languageserver-types"

import { LanguageManager } from "./../Language"

import { ICompletionsRequestor, LanguageServiceCompletionsRequestor } from "./CompletionsRequestor"

export interface ICompletionProviderInfo {
id: string
provider: ICompletionsRequestor
}

export interface ICompletionInfoWithProvider extends types.CompletionItem {
__provider: string
}

export class CompletionProviders implements ICompletionsRequestor {
private _completionProviders: ICompletionProviderInfo[] = []

public registerCompletionProvider(id: string, provider: ICompletionsRequestor): void {
this._completionProviders.push({
id,
provider,
})
}

public async getCompletions(language: string, filePath: string, line: number, column: number): Promise<types.CompletionItem[]> {
const completionItemsPromise = this._completionProviders.map(async (prov) => {
const items = await prov.provider.getCompletions(language, filePath, line, column)

// Tag the items with the provider id, so we know who to ask for details
const augmentedItems = items.map((item) => {
return {
...item,
__provider: prov.id,
}
})

return augmentedItems
})

const allItems = await Promise.all(completionItemsPromise)

const flattenedItems = allItems.reduce((prev: ICompletionInfoWithProvider[], current: ICompletionInfoWithProvider[]) => {
return [...prev, ...current]
}, [] as ICompletionInfoWithProvider[])

return flattenedItems
}

public async getCompletionDetails(language: string, filePath: string, completionItem: ICompletionInfoWithProvider): Promise<types.CompletionItem> {
if (completionItem.__provider) {
const prov = this._getProviderById(completionItem.__provider)

if (prov && prov.getCompletionDetails) {
return prov.getCompletionDetails(language, filePath, completionItem)
}
}

return completionItem
}

private _getProviderById(id: string): ICompletionsRequestor {
const providersMatchingId = this._completionProviders.filter((prov) => prov.id === id)

return providersMatchingId.length > 0 ? providersMatchingId[0].provider : null
}
}

let _completionProviders: CompletionProviders

export const activate = (languageManager: LanguageManager) => {
_completionProviders = new CompletionProviders()

const languageServiceCompletion = new LanguageServiceCompletionsRequestor(languageManager)

_completionProviders.registerCompletionProvider("oni.completions.language-server", languageServiceCompletion)
}

export const getInstance = (): CompletionProviders => {
return _completionProviders
}
13 changes: 7 additions & 6 deletions browser/src/Services/Completion/CompletionStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

import * as types from "vscode-languageserver-types"

import * as Oni from "oni-api"

import "rxjs/add/operator/mergeMap"
import { Observable } from "rxjs/Observable"

Expand All @@ -15,7 +17,6 @@ import { createStore as oniCreateStore } from "./../../Redux"
import { Configuration } from "./../Configuration"
import { LanguageManager } from "./../Language"

import { commitCompletion } from "./CompletionProvider"
import * as CompletionSelects from "./CompletionSelectors"
import { ICompletionsRequestor } from "./CompletionsRequestor"
import * as CompletionUtility from "./CompletionUtility"
Expand Down Expand Up @@ -209,14 +210,14 @@ const createGetCompletionMeetEpic = (languageManager: LanguageManager, configura
} as CompletionAction
})

const commitCompletionEpic: Epic<CompletionAction, ICompletionState> = (action$, store) =>
const commitCompletionEpic = (editor: Oni.Editor): Epic<CompletionAction, ICompletionState> => (action$, store) =>
action$.ofType("COMMIT_COMPLETION")
.do(async (action) => {
.do(async (action: CompletionAction) => {
if (action.type !== "COMMIT_COMPLETION") {
return
}

await commitCompletion(action.meetLine, action.meetPosition, action.completionText)
await CompletionUtility.commitCompletion(editor.activeBuffer, action.meetLine, action.meetPosition, action.completionText)
}).map(_ => nullAction)

const createGetCompletionsEpic = (completionsRequestor: ICompletionsRequestor): Epic<CompletionAction, ICompletionState> => (action$, store) =>
Expand Down Expand Up @@ -337,7 +338,7 @@ const selectFirstItemEpic: Epic<CompletionAction, ICompletionState> = (action$,

})

export const createStore = (languageManager: LanguageManager, configuration: Configuration, completionsRequestor: ICompletionsRequestor): Store<ICompletionState> => {
export const createStore = (editor: Oni.Editor, languageManager: LanguageManager, configuration: Configuration, completionsRequestor: ICompletionsRequestor): Store<ICompletionState> => {
return oniCreateStore("COMPLETION_STORE",
combineReducers<ICompletionState>({
enabled: enabledReducer,
Expand All @@ -349,7 +350,7 @@ export const createStore = (languageManager: LanguageManager, configuration: Con
}),
DefaultCompletionState,
[createEpicMiddleware(combineEpics(
commitCompletionEpic,
commitCompletionEpic(editor),
createGetCompletionMeetEpic(languageManager, configuration),
createGetCompletionsEpic(completionsRequestor),
createGetCompletionDetailsEpic(completionsRequestor),
Expand Down
18 changes: 18 additions & 0 deletions browser/src/Services/Completion/CompletionUtility.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,26 @@
* Helper functions for auto completion
*/

import * as Oni from "oni-api"
import * as types from "vscode-languageserver-types"

export const commitCompletion = async (buffer: Oni.Buffer, line: number, base: number, completion: string) => {
const currentLines = await buffer.getLines(line, line + 1)

const column = buffer.cursor.column

if (!currentLines || !currentLines.length) {
return
}

const originalLine = currentLines[0]

const newLine = replacePrefixWithCompletion(originalLine, base, column, completion)
await buffer.setLines(line, line + 1, [newLine])
const cursorOffset = newLine.length - originalLine.length
await buffer.setCursorPosition(line, column + cursorOffset)
}

export function getCompletionStart(bufferLine: string, cursorColumn: number, completion: string): number {

cursorColumn = Math.min(cursorColumn, bufferLine.length)
Expand Down
1 change: 1 addition & 0 deletions browser/src/Services/Completion/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from "./Completion"
export * from "./CompletionProviders"
export * from "./CompletionsRequestor"
export * from "./CompletionUtility"
6 changes: 5 additions & 1 deletion browser/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ const start = async (args: string[]): Promise<void> => {

const themePickerPromise = import("./Services/Themes/ThemePicker")
const cssPromise = import("./CSS")
const completionProvidersPromise = import("./Services/Completion/CompletionProviders")

// Helper for debugging:
Performance.startMeasure("Oni.Start.Config")
Expand Down Expand Up @@ -126,10 +127,13 @@ const start = async (args: string[]): Promise<void> => {
const Diagnostics = await diagnosticsPromise
const diagnostics = Diagnostics.getInstance()

const CompletionProviders = await completionProvidersPromise
CompletionProviders.activate(languageManager)

await Promise.race([Utility.delay(5000),
Promise.all([
SharedNeovimInstance.activate(configuration, pluginManager),
startEditors(parsedArgs._, Colors.getInstance(), configuration, diagnostics, languageManager, pluginManager, Themes.getThemeManagerInstance(), workspace)
startEditors(parsedArgs._, Colors.getInstance(), CompletionProviders.getInstance(), configuration, diagnostics, languageManager, pluginManager, Themes.getThemeManagerInstance(), workspace)
])
])
Performance.endMeasure("Oni.Start.Editors")
Expand Down
5 changes: 3 additions & 2 deletions browser/src/startEditors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { NeovimEditor } from "./Editor/NeovimEditor"
import { PluginManager } from "./Plugins/PluginManager"

import { Colors } from "./Services/Colors"
import { CompletionProviders } from "./Services/Completion"
import { Configuration } from "./Services/Configuration"
import { IDiagnosticsDataSource } from "./Services/Diagnostics"
import { editorManager } from "./Services/EditorManager"
Expand All @@ -17,9 +18,9 @@ import { ThemeManager } from "./Services/Themes"
import { windowManager } from "./Services/WindowManager"
import { Workspace } from "./Services/Workspace"

export const startEditors = async (args: any, colors: Colors, configuration: Configuration, diagnostics: IDiagnosticsDataSource, languageManager: LanguageManager, pluginManager: PluginManager, themeManager: ThemeManager, workspace: Workspace): Promise<void> => {
export const startEditors = async (args: any, colors: Colors, completionProviders: CompletionProviders, configuration: Configuration, diagnostics: IDiagnosticsDataSource, languageManager: LanguageManager, pluginManager: PluginManager, themeManager: ThemeManager, workspace: Workspace): Promise<void> => {

const editor = new NeovimEditor(colors, configuration, diagnostics, languageManager, pluginManager, themeManager, workspace)
const editor = new NeovimEditor(colors, completionProviders, configuration, diagnostics, languageManager, pluginManager, themeManager, workspace)
editorManager.setActiveEditor(editor)
windowManager.split(0, editor)

Expand Down
2 changes: 1 addition & 1 deletion browser/test/Services/Completion/CompletionTests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ describe("Completion", () => {
mockEditor = new Mocks.MockEditor()
mockLanguageManager = new Mocks.MockLanguageManager()
mockCompletionRequestor = new MockCompletionRequestor()
completion = new Completion.Completion(mockEditor, mockLanguageManager as any, mockConfiguration as any, mockCompletionRequestor)
completion = new Completion.Completion(mockEditor, mockConfiguration as any, mockCompletionRequestor, mockLanguageManager as any)
})

afterEach(() => {
Expand Down