feat(lsp): Add lsp wasm and connect to monaco (#16842)

* feat(lsp): Add flux-lsp-browswer dependency

* feat(lsp): Change webpack settings to compile lsp wasm

* feat(lsp): Add monaco lsp client dep

* feat:lsp instantiate lsp server in monaco editor file

* feat(lsp): Update monaco loaders and load in webpack.common

* feat(lsp): Update flux-lsp-browser dependency

* feat(lsp): Connect monaco to lsp server

* feat(lsp): Add trigger characters

* feat(lsp): Dispose of completion item provider

* feat(lsp): Remove javascript and go as monaco languages

* feat(lsp): Fix type errors in tests

* feat(lsp): Define constants file for fluxlangid

* feat(lsp): Remove console

* feat(lsp): Fix variable and function insertion

* feat(lsp): Fix script entering in DE

* feat(lsp): FIx task tests

* feat(lsp): Add monaco as global

* feat(lsp): Add monaco to window type
pull/16893/head
Deniz Kusefoglu 2020-02-14 09:59:05 -08:00 committed by GitHub
parent f218dfaf67
commit 38e014ee90
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 537 additions and 453 deletions

View File

@ -498,9 +498,9 @@ describe('DataExplorer', () => {
.click()
.focused()
.type(
`from(bucket: "defbuck")
|> range(start: -10s)
|> filter(fn: (r) => r._measurement == "no exist")`,
`from(bucket: "defbuck"{rightarrow}
|> range(start: -10s{rightarrow}
|> filter(fn: (r{rightarrow} => r._measurement == "no exist"{rightarrow}`,
{force: true, delay: TYPE_DELAY}
)
cy.getByTestID('time-machine-submit-button').click()
@ -518,9 +518,9 @@ describe('DataExplorer', () => {
.click()
.focused()
.type(
`from(bucket: "defbuck")
|> range(start: -15m, stop: now())
|> filter(fn: (r) => r._measurement == `,
`from(bucket: "defbuck"{rightarrow}
|> range(start: -15m, stop: now({rightarrow}{rightarrow}
|> filter(fn: (r{rightarrow} => r._measurement ==`,
{force: true, delay: TYPE_DELAY}
)
})
@ -528,14 +528,6 @@ describe('DataExplorer', () => {
cy.getByTestID('toolbar-tab').click()
//insert variable name by clicking on variable
cy.get('.variables-toolbar--label').click()
// finish flux
cy.getByTestID('flux-editor').within(() => {
cy.get('.react-monaco-editor-container')
.should('exist')
.click()
.focused()
.type(`)`, {force: true, delay: TYPE_DELAY})
})
cy.getByTestID('save-query-as').click()
cy.getByTestID('task--radio-button').click()

View File

@ -28,11 +28,11 @@ describe('Tasks', () => {
const taskName = 'Bad Task'
createFirstTask(taskName, ({name}) => {
return `import "influxdata/influxdb/v1"
v1.tagValues(bucket: "${name}", tag: "_field")
from(bucket: "${name}")
|> range(start: -2m)
|> to(org: "${name}")`
return `import "influxdata/influxdb/v1{rightarrow}
v1.tagValues(bucket: "${name}", tag: "_field"{rightarrow}
from(bucket: "${name}"{rightarrow}
|> range(start: -2m{rightarrow}
|> to(org: "${name}"{rightarrow}`
})
cy.getByTestID('task-save-btn').click()
@ -46,10 +46,10 @@ from(bucket: "${name}")
it('can create a task', () => {
const taskName = 'Task'
createFirstTask(taskName, ({name}) => {
return `import "influxdata/influxdb/v1"
v1.tagValues(bucket: "${name}", tag: "_field")
from(bucket: "${name}")
|> range(start: -2m)`
return `import "influxdata/influxdb/v1{rightarrow}
v1.tagValues(bucket: "${name}", tag: "_field"{rightarrow}
from(bucket: "${name}"{rightarrow}
|> range(start: -2m{rightarrow}`
})
cy.getByTestID('task-save-btn').click()
@ -62,11 +62,11 @@ from(bucket: "${name}")
it.skip('can create a task using http.post', () => {
const taskName = 'Task'
createFirstTask(taskName, () => {
return `import "http"
return `import "http{rightarrow}
http.post(
url: "https://foo.bar/baz",
data: bytes(v: "body")
)`
data: bytes(v: "body"{rightarrow}
{rightarrow}`
})
cy.getByTestID('task-save-btn').click()
@ -229,10 +229,10 @@ http.post(
createFirstTask(
taskName,
({name}) => {
return `import "influxdata/influxdb/v1"
v1.tagValues(bucket: "${name}", tag: "_field")
from(bucket: "${name}")
|> range(start: -2m)`
return `import "influxdata/influxdb/v1{rightarrow}
v1.tagValues(bucket: "${name}", tag: "_field"{rightarrow}
from(bucket: "${name}"{rightarrow}
|> range(start: -2m{rightarrow}`
},
interval,
offset
@ -304,55 +304,22 @@ http.post(
// https://github.com/influxdata/influxdb/issues/15552
const firstTask = 'First_Task'
const secondTask = 'Second_Task'
const interval = '12h'
const offset = '30m'
const flux = name => `import "influxdata/influxdb/v1"
v1.tagValues(bucket: "${name}", tag: "_field")
from(bucket: "${name}")
|> range(start: -2m)`
beforeEach(() => {
createFirstTask(
firstTask,
({name}) => {
return flux(name)
},
interval,
offset
)
cy.getByTestID('task-save-btn').click()
cy.getByTestID('task-card')
.should('have.length', 1)
.and('contain', firstTask)
cy.getByTestID('add-resource-dropdown--button').click()
cy.getByTestID('add-resource-dropdown--new').click()
cy.getByInputName('name').type(secondTask)
cy.getByTestID('task-form-schedule-input').type(interval)
cy.getByTestID('task-form-offset-input').type(offset)
cy.get<Bucket>('@bucket').then(bucket => {
cy.getByTestID('flux-editor').within(() => {
cy.get('.react-monaco-editor-container')
.should('be.visible')
.click()
.focused()
.type(flux(bucket), {force: true, delay: 2})
cy.get('@org').then(({id}: Organization) => {
cy.get<string>('@token').then(token => {
cy.createTask(token, id, firstTask)
cy.createTask(token, id, secondTask)
})
})
cy.fixture('routes').then(({orgs}) => {
cy.get('@org').then(({id}: Organization) => {
cy.visit(`${orgs}/${id}/tasks`)
})
})
cy.getByTestID('task-save-btn').click()
cy.getByTestID('task-card')
.should('have.length', 2)
.and('contain', firstTask)
.and('contain', secondTask)
cy.getByTestID('task-card--name')
.contains(firstTask)
.click()
})
it('when navigating using the navbar', () => {
// verify that the previously input data exists
cy.getByInputValue(firstTask)
// navigate home
cy.get('div.cf-nav--item.active').click()
// click on the second task
cy.getByTestID('task-card--name')
.contains(secondTask)
@ -368,10 +335,6 @@ http.post(
})
it('when navigating using the cancel button', () => {
// verify that the previously input data exists
cy.getByInputValue(firstTask)
// navigate home
cy.getByTestID('task-cancel-btn').click()
// click on the second task
cy.getByTestID('task-card--name')
.contains(secondTask)
@ -388,10 +351,6 @@ http.post(
})
it('when navigating using the save button', () => {
// verify that the previously input data exists
cy.getByInputValue(firstTask)
// navigate home
cy.getByTestID('task-save-btn').click()
// click on the second task
cy.getByTestID('task-card--name')
.contains(secondTask)
@ -421,16 +380,16 @@ function createFirstTask(
cy.getByTestID('add-resource-dropdown--new').click()
cy.getByInputName('name').type(name)
cy.getByTestID('task-form-schedule-input').type(interval)
cy.getByTestID('task-form-offset-input').type(offset)
cy.get<Bucket>('@bucket').then(bucket => {
cy.getByTestID('flux-editor').within(() => {
cy.get('.react-monaco-editor-container')
cy.get('textarea.inputarea')
.click()
.focused()
.type(flux(bucket), {force: true, delay: 2})
})
})
cy.getByInputName('name').type(name)
cy.getByTestID('task-form-schedule-input').type(interval)
cy.getByTestID('task-form-offset-input').type(offset)
}

11
ui/global.d.ts vendored
View File

@ -1,5 +1,16 @@
import {MonacoType} from 'src/types'
//
// got some globals here that only exist during compilation
//
declare module '*.png'
declare let monaco: MonacoType
declare global {
interface Window {
monaco: MonacoType
}
}
window.monaco = window.monaco || {}

View File

@ -130,6 +130,7 @@
},
"dependencies": {
"@influxdata/clockface": "1.1.5",
"@influxdata/flux-lsp-browser": "^0.2.2",
"@influxdata/flux-parser": "^0.3.0",
"@influxdata/giraffe": "0.17.4",
"@influxdata/influx": "0.5.5",
@ -154,9 +155,10 @@
"lodash": "^4.3.0",
"memoize-one": "^4.0.2",
"moment": "^2.13.0",
"monaco-editor": "^0.18.1",
"monaco-editor": "^0.19.2",
"monaco-editor-textmate": "^2.2.1",
"monaco-editor-webpack-plugin": "^1.7.0",
"monaco-languageclient": "^0.11.0",
"monaco-editor-webpack-plugin": "^1.8.2",
"monaco-textmate": "^3.0.1",
"normalizr": "^3.4.1",
"onigasm": "^2.2.4",
@ -174,7 +176,7 @@
"react-grid-layout": "^0.16.6",
"react-loadable": "^5.5.0",
"react-markdown": "^4.0.3",
"react-monaco-editor": "^0.32.1",
"react-monaco-editor": "^0.33.0",
"react-redux": "^5.1.2",
"react-router": "^3.0.2",
"react-router-redux": "^4.0.8",

1
ui/src/external/constants.ts vendored Normal file
View File

@ -0,0 +1 @@
export const FLUXLANGID = 'flux' as const

View File

@ -1,20 +1,83 @@
// Types
import {MonacoType} from 'src/types'
// Libraries
import {completion, sendMessage} from 'src/external/monaco.lspMessages'
import {
MonacoToProtocolConverter,
ProtocolToMonacoConverter,
} from 'monaco-languageclient/lib/monaco-converter'
import {get} from 'lodash'
export const addSnippets = (monaco: MonacoType) => {
monaco.languages.registerCompletionItemProvider('flux', {
provideCompletionItems: () => {
const suggestions = [
{
label: 'from',
kind: monaco.languages.CompletionItemKind.Snippet,
insertText: ['from(bucket: ${1})', '\t|>'].join('\n'),
insertTextRules:
monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
documentation: 'From-Statement',
},
] as any[]
return {suggestions: suggestions}
},
})
// Constants
import {FLUXLANGID} from 'src/external/constants'
// Types
import {CompletionItem} from 'monaco-languageclient/lib/services'
import {MonacoType} from 'src/types'
import {IDisposable} from 'monaco-editor'
const m2p = new MonacoToProtocolConverter()
const p2m = new ProtocolToMonacoConverter()
export const registerCompletion = (monaco: MonacoType, server): IDisposable => {
const completionProvider = monaco.languages.registerCompletionItemProvider(
FLUXLANGID,
{
provideCompletionItems: (model, position, context) => {
const wordUntil = model.getWordUntilPosition(position)
const defaultRange = new monaco.Range(
position.lineNumber,
wordUntil.startColumn,
position.lineNumber,
wordUntil.endColumn
)
const response = sendMessage(
completion(
m2p.asPosition(position.lineNumber, position.column),
context
),
server
)
const completionItems = get(
response,
'result.items',
null
) as CompletionItem[]
if (!completionItems) {
return
}
return p2m.asCompletionResult(completionItems, defaultRange)
},
triggerCharacters: [
'.',
'a',
'b',
'c',
'd',
'e',
'f',
'g',
'h',
'i',
'j',
'k',
'l',
'm',
'n',
'o',
'p',
'q',
'r',
's',
't',
'u',
'v',
'w',
'x',
'y',
'z',
],
}
)
return completionProvider
}

View File

@ -1,7 +1,10 @@
export const tokenizeFlux = monaco => {
monaco.languages.register({id: 'flux'})
// Constants
import {FLUXLANGID} from 'src/external/constants'
monaco.languages.setMonarchTokensProvider('flux', {
export const tokenizeFlux = monaco => {
monaco.languages.register({id: FLUXLANGID})
monaco.languages.setMonarchTokensProvider(FLUXLANGID, {
keywords: ['from', 'range', 'filter', 'to'],
tokenizer: {
root: [

View File

@ -2,14 +2,15 @@ import loader from 'src/external/monaco.onigasm'
import {Registry} from 'monaco-textmate' // peer dependency
import {wireTmGrammars} from 'monaco-editor-textmate'
// Constants
import {FLUXLANGID} from 'src/external/constants'
// Types
import {MonacoType} from 'src/types'
export async function addSyntax(monaco: MonacoType) {
await loader()
monaco.languages.register({id: 'flux'})
const registry = new Registry({
// TODO: this is maintained in influxdata/vsflux, which is currently
// a private repo, so we can't use it yet (alex)
@ -25,7 +26,17 @@ export async function addSyntax(monaco: MonacoType) {
// map of monaco "language id's" to TextMate scopeNames
const grammars = new Map()
grammars.set('flux', 'flux')
grammars.set(FLUXLANGID, FLUXLANGID)
monaco.languages.setLanguageConfiguration(FLUXLANGID, {
autoClosingPairs: [
{open: '"', close: '"'},
{open: '[', close: ']'},
{open: "'", close: "'"},
{open: '{', close: '}'},
{open: '(', close: ')'},
],
})
await wireTmGrammars(monaco, registry, grammars)
}

94
ui/src/external/monaco.lspMessages.ts vendored Normal file
View File

@ -0,0 +1,94 @@
import {get} from 'lodash'
// Constants
import {FLUXLANGID} from 'src/external/constants'
// Types
import {ServerResponse} from 'src/types'
type LSPMessage =
| typeof initialize
| ReturnType<typeof didOpen>
| ReturnType<typeof didChange>
| ReturnType<typeof completion>
const URI = 'monacoeditor' as const
const JSONRPC = '2.0' as const
export const initialize = {
jsonrpc: JSONRPC,
id: 1,
method: 'initialize',
params: {},
} as const
export const didOpen = (text: string) => ({
jsonrpc: JSONRPC,
id: 2,
method: 'textDocument/didOpen' as const,
params: {
textDocument: {
uri: URI,
languageId: FLUXLANGID,
version: 1 as const,
text,
},
},
})
export const didChange = (
newText: string,
version: number,
messageID: number
) => ({
jsonrpc: JSONRPC,
id: messageID,
method: 'textDocument/didChange' as const,
params: {
textDocument: {
uri: URI,
version: version,
},
contentChanges: [
{
text: newText,
},
],
},
})
export const completion = (position, context) => ({
jsonrpc: JSONRPC,
id: 100,
method: 'textDocument/completion' as const,
params: {
textDocument: {uri: URI},
position,
context,
},
})
export const parseResponse = (response: ServerResponse): null | object => {
const message = response.get_message()
if (message) {
const split = message.split('\n')
const parsed_msg = get(split, '2', null)
return JSON.parse(parsed_msg)
} else {
const error = response.get_error()
const split = error.split('\n')
const parsed_err = get(split, '2', null)
return JSON.parse(parsed_err)
}
}
export const sendMessage = (message: LSPMessage, server) => {
const stringifiedMessage = JSON.stringify(message)
const size = stringifiedMessage.length
const resp = server.process(
`Content-Length: ${size}\r\n\r\n` + stringifiedMessage
)
return parseResponse(resp)
}

View File

@ -1,58 +1,72 @@
// Libraries
import React, {FC} from 'react'
import React, {FC, useEffect, useRef, useState} from 'react'
import {Server} from '@influxdata/flux-lsp-browser'
// Components
import MonacoEditor from 'react-monaco-editor'
// Utils
import addFluxTheme, {THEME_NAME} from 'src/external/monaco.fluxTheme'
import {addSnippets} from 'src/external/monaco.fluxCompletions'
import {registerCompletion} from 'src/external/monaco.fluxCompletions'
import {addSyntax} from 'src/external/monaco.fluxSyntax'
import {OnChangeScript} from 'src/types/flux'
import {addKeyBindings} from 'src/external/monaco.keyBindings'
import {
sendMessage,
initialize,
didChange,
didOpen,
} from 'src/external/monaco.lspMessages'
// Constants
import {FLUXLANGID} from 'src/external/constants'
// Types
import {OnChangeScript} from 'src/types/flux'
import {MonacoType, EditorType} from 'src/types'
import './FluxMonacoEditor.scss'
interface Position {
line: number
ch: number
}
interface Props {
script: string
onChangeScript: OnChangeScript
onSubmitScript?: () => void
onCursorChange?: (position: Position) => void
setEditorInstance?: (editor: EditorType) => void
}
const FluxEditorMonaco: FC<Props> = props => {
const FluxEditorMonaco: FC<Props> = ({
script,
onChangeScript,
onSubmitScript,
setEditorInstance,
}) => {
let completionProvider = {dispose: () => {}}
const lspServer = useRef(new Server(false))
const [docVersion, setdocVersion] = useState(2)
const [msgID, setmsgID] = useState(3)
useEffect(() => {
sendMessage(initialize, lspServer.current)
sendMessage(didOpen(script), lspServer.current)
return () => {
completionProvider.dispose()
}
}, [])
const editorWillMount = (monaco: MonacoType) => {
monaco.languages.register({id: FLUXLANGID})
addFluxTheme(monaco)
addSnippets(monaco)
addSyntax(monaco)
completionProvider = registerCompletion(monaco, lspServer.current)
}
const editorDidMount = (editor: EditorType, monaco: MonacoType) => {
if (setEditorInstance) {
setEditorInstance(editor)
}
addKeyBindings(editor, monaco)
editor.onDidChangeCursorPosition(evt => {
const {position} = evt
const {onCursorChange} = props
const pos = {
line: position.lineNumber - 1,
ch: position.column,
}
if (onCursorChange) {
onCursorChange(pos)
}
})
editor.focus()
editor.onKeyUp(evt => {
const {ctrlKey, code} = evt
const {onSubmitScript} = props
if (ctrlKey && code === 'Enter') {
if (onSubmitScript) {
onSubmitScript()
@ -60,15 +74,21 @@ const FluxEditorMonaco: FC<Props> = props => {
}
})
}
const {script, onChangeScript} = props
const onChange = (text: string) => {
sendMessage(didChange(text, docVersion, msgID), lspServer.current)
setdocVersion(docVersion + 1)
setmsgID(msgID + 1)
onChangeScript(text)
}
return (
<div className="time-machine-editor" data-testid="flux-editor">
<MonacoEditor
language="flux"
language={FLUXLANGID}
theme={THEME_NAME}
value={script}
onChange={onChangeScript}
onChange={onChange}
options={{
fontSize: 13,
fontFamily: '"RobotoMono", monospace',

View File

@ -1,7 +1,6 @@
// Libraries
import React, {PureComponent} from 'react'
import React, {FC, useState} from 'react'
import {connect} from 'react-redux'
import {Position} from 'codemirror'
// Components
import FluxEditor from 'src/shared/components/FluxMonacoEditor'
@ -16,14 +15,16 @@ import {saveAndExecuteQueries} from 'src/timeMachine/actions/queries'
// Utils
import {getActiveQuery, getActiveTimeMachine} from 'src/timeMachine/selectors'
import {insertFluxFunction} from 'src/timeMachine/utils/insertFunction'
import {insertVariable} from 'src/timeMachine/utils/insertVariable'
import {
formatFunctionForInsert,
generateImport,
} from 'src/timeMachine/utils/insertFunction'
// Constants
import {HANDLE_VERTICAL, HANDLE_NONE} from 'src/shared/constants'
// Types
import {AppState, FluxToolbarFunction} from 'src/types'
import {AppState, FluxToolbarFunction, EditorType} from 'src/types'
interface StateProps {
activeQueryText: string
@ -35,132 +36,122 @@ interface DispatchProps {
onSubmitQueries: typeof saveAndExecuteQueries
}
interface State {
displayFluxFunctions: boolean
}
type Props = StateProps & DispatchProps
class TimeMachineFluxEditor extends PureComponent<Props, State> {
private cursorPosition: Position = {line: 0, ch: 0}
const TimeMachineFluxEditor: FC<Props> = ({
activeQueryText,
onSubmitQueries,
onSetActiveQueryText,
activeTab,
}) => {
const [displayFluxFunctions, setDisplayFluxFunctions] = useState(true)
const [editorInstance, setEditorInstance] = useState<EditorType>(null)
public state: State = {
displayFluxFunctions: true,
const showFluxFunctions = () => {
setDisplayFluxFunctions(true)
}
public render() {
const {
activeQueryText,
onSubmitQueries,
onSetActiveQueryText,
activeTab,
} = this.props
const hideFluxFunctions = () => {
setDisplayFluxFunctions(false)
}
const divisions = [
const handleInsertVariable = (variableName: string): void => {
const p = editorInstance.getPosition()
editorInstance.executeEdits('', [
{
size: 0.75,
handleDisplay: HANDLE_NONE,
render: () => {
return (
<FluxEditor
script={activeQueryText}
onChangeScript={onSetActiveQueryText}
onSubmitScript={onSubmitQueries}
onCursorChange={this.handleCursorPosition}
/>
)
},
range: new window.monaco.Range(
p.lineNumber,
p.column,
p.lineNumber,
p.column
),
text: `v.${variableName}`,
},
])
onSetActiveQueryText(editorInstance.getValue())
}
const handleInsertFluxFunction = (func: FluxToolbarFunction): void => {
const p = editorInstance.getPosition()
const edits = [
{
render: () => {
return (
<>
<div className="toolbar-tab-container">
{activeTab !== 'customCheckQuery' && (
<ToolbarTab
onSetActive={this.hideFluxFunctions}
name="Variables"
active={!this.state.displayFluxFunctions}
/>
)}
<ToolbarTab
onSetActive={this.showFluxFunctions}
name="Functions"
active={this.state.displayFluxFunctions}
testID="functions-toolbar-tab"
/>
</div>
{this.rightDivision}
</>
)
},
handlePixels: 6,
size: 0.25,
range: new window.monaco.Range(
p.lineNumber,
p.column,
p.lineNumber,
p.column
),
text: formatFunctionForInsert(func.name, func.example),
},
]
return (
<div className="time-machine-flux-editor">
<Threesizer orientation={HANDLE_VERTICAL} divisions={divisions} />
</div>
const importStatement = generateImport(
func.package,
editorInstance.getValue()
)
}
private get rightDivision(): JSX.Element {
const {displayFluxFunctions} = this.state
if (displayFluxFunctions) {
return (
<FluxFunctionsToolbar
onInsertFluxFunction={this.handleInsertFluxFunction}
/>
)
if (importStatement) {
edits.unshift({
range: new window.monaco.Range(1, 1, 1, 1),
text: `${importStatement}\n`,
})
}
return <VariableToolbar onClickVariable={this.handleInsertVariable} />
editorInstance.executeEdits('', edits)
onSetActiveQueryText(editorInstance.getValue())
}
private handleCursorPosition = (position: Position): void => {
this.cursorPosition = position
}
const divisions = [
{
size: 0.75,
handleDisplay: HANDLE_NONE,
render: () => {
return (
<FluxEditor
script={activeQueryText}
onChangeScript={onSetActiveQueryText}
onSubmitScript={onSubmitQueries}
setEditorInstance={setEditorInstance}
/>
)
},
},
{
render: () => {
return (
<>
<div className="toolbar-tab-container">
{activeTab !== 'customCheckQuery' && (
<ToolbarTab
onSetActive={hideFluxFunctions}
name="Variables"
active={!displayFluxFunctions}
/>
)}
<ToolbarTab
onSetActive={showFluxFunctions}
name="Functions"
active={displayFluxFunctions}
testID="functions-toolbar-tab"
/>
</div>
{displayFluxFunctions ? (
<FluxFunctionsToolbar
onInsertFluxFunction={handleInsertFluxFunction}
/>
) : (
<VariableToolbar onClickVariable={handleInsertVariable} />
)}
</>
)
},
handlePixels: 6,
size: 0.25,
},
]
private handleInsertVariable = (variableName: string): void => {
const {activeQueryText} = this.props
const {line, ch} = this.cursorPosition
const {updatedScript, cursorPosition} = insertVariable(
line,
ch,
activeQueryText,
variableName
)
this.props.onSetActiveQueryText(updatedScript)
this.handleCursorPosition(cursorPosition)
}
private handleInsertFluxFunction = (func: FluxToolbarFunction): void => {
const {activeQueryText, onSetActiveQueryText} = this.props
const {line} = this.cursorPosition
const {updatedScript, cursorPosition} = insertFluxFunction(
line,
activeQueryText,
func
)
onSetActiveQueryText(updatedScript)
this.handleCursorPosition(cursorPosition)
}
private showFluxFunctions = () => {
this.setState({displayFluxFunctions: true})
}
private hideFluxFunctions = () => {
this.setState({displayFluxFunctions: false})
}
return (
<div className="time-machine-flux-editor">
<Threesizer orientation={HANDLE_VERTICAL} divisions={divisions} />
</div>
)
}
const mstp = (state: AppState) => {

View File

@ -1,44 +1,6 @@
import {Position} from 'codemirror'
// Constants
import {FROM, UNION} from 'src/shared/constants/fluxFunctions'
// Types
import {FluxToolbarFunction} from 'src/types'
const rejoinScript = (scriptLines: string[]): string => {
return scriptLines.join('\n')
}
const insertAtLine = (
lineNumber: number,
scriptLines: string[],
textToInsert: string,
insertOnSameLine?: boolean
): string => {
const front = scriptLines.slice(0, lineNumber)
const backStartIndex = insertOnSameLine ? lineNumber + 1 : lineNumber
const back = scriptLines.slice(backStartIndex)
const updated = [...front, textToInsert, ...back]
return rejoinScript(updated)
}
const getInsertLineNumber = (
currentLineNumber: number,
scriptLines: string[]
): number => {
const currentLine = scriptLines[currentLineNumber]
// Insert on the current line if its an empty line
if (!currentLine.trim()) {
return currentLineNumber
}
return currentLineNumber + 1
}
const functionRequiresNewLine = (funcName: string): boolean => {
switch (funcName) {
case FROM.name:
@ -50,7 +12,10 @@ const functionRequiresNewLine = (funcName: string): boolean => {
}
}
const formatFunctionForInsert = (funcName: string, fluxFunction: string) => {
export const formatFunctionForInsert = (
funcName: string,
fluxFunction: string
) => {
if (functionRequiresNewLine(funcName)) {
return `\n${fluxFunction}`
}
@ -58,63 +23,15 @@ const formatFunctionForInsert = (funcName: string, fluxFunction: string) => {
return ` |> ${fluxFunction}`
}
const getCursorPosition = (
insertLineNumber,
formattedFunction,
funcName
): Position => {
const ch = formattedFunction.length - 1
const line = functionRequiresNewLine(funcName)
? insertLineNumber + 1
: insertLineNumber
return {line, ch}
}
const genImport = (script: string, funcPackage: string) => {
export const generateImport = (
funcPackage: string,
script: string
): false | string => {
const importStatement = `import "${funcPackage}"`
if (!funcPackage || script.includes(importStatement)) {
return ''
return false
}
return importStatement
}
export const insertFluxFunction = (
currentLineNumber: number,
currentScript: string,
func: FluxToolbarFunction
): {updatedScript: string; cursorPosition: Position} => {
const {name, example} = func
const scriptLines = currentScript.split('\n')
let insertLineNumber = getInsertLineNumber(currentLineNumber, scriptLines)
const insertOnSameLine = currentLineNumber === insertLineNumber
const formattedFunction = formatFunctionForInsert(name, example)
let nextScript = insertAtLine(
insertLineNumber,
scriptLines,
formattedFunction,
insertOnSameLine
)
const importStatement = genImport(nextScript, func.package)
if (importStatement) {
nextScript = `${importStatement}\n${nextScript}`
insertLineNumber += 1
}
const nextCursorPos = getCursorPosition(
insertLineNumber,
formattedFunction,
name
)
return {updatedScript: nextScript, cursorPosition: nextCursorPos}
}

View File

@ -1,60 +0,0 @@
import {Position} from 'codemirror'
const rejoinScript = (scriptLines: string[]): string => {
return scriptLines.join('\n')
}
const getCursorPosition = (
currentLineNumber: number,
currentCharacterNumber: number,
variableName: string
) => {
return {
line: currentLineNumber,
ch: currentCharacterNumber + formatVariable(variableName).length,
}
}
const insertAtCharacter = (
lineNumber: number,
characterNumber: number,
scriptLines: string[],
variableName: string
): string => {
const lineToEdit = scriptLines[lineNumber]
const front = lineToEdit.slice(0, characterNumber)
const back = lineToEdit.slice(characterNumber, lineToEdit.length)
const updatedLine = `${front}${formatVariable(variableName)}${back}`
scriptLines[lineNumber] = updatedLine
return rejoinScript(scriptLines)
}
const formatVariable = (variableName: string): string => {
return `v.${variableName}`
}
export const insertVariable = (
currentLineNumber: number,
currentCharacterNumber: number,
currentScript: string,
variableName: string
): {updatedScript: string; cursorPosition: Position} => {
const scriptLines = currentScript.split('\n')
const updatedScript = insertAtCharacter(
currentLineNumber,
currentCharacterNumber,
scriptLines,
variableName
)
const updatedCursorPosition = getCursorPosition(
currentLineNumber,
currentCharacterNumber,
variableName
)
return {updatedScript, cursorPosition: updatedCursorPosition}
}

View File

@ -1,4 +1,7 @@
import * as allMonaco from 'monaco-editor/esm/vs/editor/editor.api'
import * as lsp from '@influxdata/flux-lsp-browser'
export type ServerResponse = lsp.ServerResponse
export type MonacoType = typeof allMonaco
export type EditorType = allMonaco.editor.IStandaloneCodeEditor

View File

@ -40,5 +40,6 @@
"src/**/*.test.tsx",
"src/**/mocks.ts",
"coverage"
]
],
"include": ["global.d.ts"]
}

View File

@ -2,6 +2,7 @@ const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const MonacoWebpackPlugin = require('monaco-editor-webpack-plugin')
const {CleanWebpackPlugin} = require('clean-webpack-plugin')
const webpack = require('webpack')
const {
@ -29,14 +30,29 @@ module.exports = {
},
extensions: ['.tsx', '.ts', '.js', '.wasm'],
},
node: {
fs: 'empty',
global: true,
crypto: 'empty',
tls: 'empty',
net: 'empty',
process: true,
module: false,
clearImmediate: false,
setImmediate: true,
},
module: {
rules: [
{
test: /\flux_parser_bg.wasm$/,
test: /flux_parser_bg.wasm$/,
type: 'webassembly/experimental',
},
{
test: /^((?!flux_parser_bg).)*.wasm$/,
test: /flux-lsp-browser_bg.wasm$/,
type: 'webassembly/experimental',
},
{
test: /^((?!flux_parser_bg|flux-lsp-browser_bg).)*.wasm$/,
loader: 'file-loader',
type: 'javascript/auto',
},
@ -120,6 +136,10 @@ module.exports = {
API_PREFIX: API_BASE_PATH,
STATIC_PREFIX: BASE_PATH,
}),
new MonacoWebpackPlugin({
languages: ['json', 'markdown'],
features: ['!gotoSymbol'],
}),
],
stats: {
colors: true,

View File

@ -20,6 +20,24 @@ module.exports = {
entry: {
vendor,
},
resolve: {
alias: {
vscode: path.resolve(
'./node_modules/monaco-languageclient/lib/vscode-compatibility'
),
},
},
node: {
fs: 'empty',
global: true,
crypto: 'empty',
tls: 'empty',
net: 'empty',
process: true,
module: false,
clearImmediate: false,
setImmediate: true,
},
output: {
path: path.join(__dirname, 'build'),
filename: '[name].bundle.js',
@ -32,6 +50,14 @@ module.exports = {
include: MONACO_DIR,
use: ['style-loader', 'css-loader'],
},
{
test: /\.(woff|woff2|eot|ttf|otf)$/,
use: [
{
loader: 'file-loader',
},
],
},
],
},
plugins: [
@ -40,7 +66,7 @@ module.exports = {
path: path.join(__dirname, 'build', '[name]-manifest.json'),
}),
new MonacoWebpackPlugin({
languages: ['json', 'javascript', 'go', 'markdown'],
languages: ['json', 'markdown'],
}),
],
stats: {

View File

@ -1016,6 +1016,11 @@
resolved "https://registry.yarnpkg.com/@influxdata/clockface/-/clockface-1.1.5.tgz#dfedc4f59788717d5e92bd00935dda35c0c60fc8"
integrity sha512-5+RDswLCiOBX21rey6e1j4vrwQ6obwMv+qcP6ucH7y0vTRnAaMk9lqKqHH3FGUqUEfBNegtj4Ho8430ywfS6ag==
"@influxdata/flux-lsp-browser@^0.2.2":
version "0.2.2"
resolved "https://registry.yarnpkg.com/@influxdata/flux-lsp-browser/-/flux-lsp-browser-0.2.2.tgz#766ef965da25149ac300df6db1a64ad277b5b50b"
integrity sha512-MlFmu7gVdgJ1pkoC1CRaWmjedi6IuSJa2Bg3vDl0l/1cJyAShtoDs8levYvL0gqcKyvq/i/baXrNXF+SCIM7MQ==
"@influxdata/flux-parser@^0.3.0":
version "0.3.0"
resolved "https://registry.yarnpkg.com/@influxdata/flux-parser/-/flux-parser-0.3.0.tgz#b63123ac814ad32c65e46a4097ba3d8b959416a5"
@ -1486,11 +1491,6 @@
resolved "https://registry.yarnpkg.com/@types/sizzle/-/sizzle-2.3.2.tgz#a811b8c18e2babab7d542b3365887ae2e4d9de47"
integrity sha512-7EJYyKTL7tFR8+gDbB6Wwz/arpGa0Mywk1TJbNzKzHtzbwVmY4HR9WqS5VV7dsBUKQmPNr192jHr/VpBluj/hg==
"@types/source-list-map@*":
version "0.1.2"
resolved "https://registry.yarnpkg.com/@types/source-list-map/-/source-list-map-0.1.2.tgz#0078836063ffaf17412349bba364087e0ac02ec9"
integrity sha512-K5K+yml8LTo9bWJI/rECfIPrGgxdpeNbj+d53lwN4QjW1MCwlkhUms+gtdzigTeUyBr09+u8BwOIY3MXvHdcsA==
"@types/stack-utils@^1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-1.0.1.tgz#0a851d3bd96498fa25c33ab7278ed3bd65f06c3e"
@ -1520,27 +1520,6 @@
dependencies:
"@types/node" "*"
"@types/webpack-sources@*":
version "0.1.5"
resolved "https://registry.yarnpkg.com/@types/webpack-sources/-/webpack-sources-0.1.5.tgz#be47c10f783d3d6efe1471ff7f042611bd464a92"
integrity sha512-zfvjpp7jiafSmrzJ2/i3LqOyTYTuJ7u1KOXlKgDlvsj9Rr0x7ZiYu5lZbXwobL7lmsRNtPXlBfmaUD8eU2Hu8w==
dependencies:
"@types/node" "*"
"@types/source-list-map" "*"
source-map "^0.6.1"
"@types/webpack@^4.4.19":
version "4.39.8"
resolved "https://registry.yarnpkg.com/@types/webpack/-/webpack-4.39.8.tgz#8083a4eb850ea02961ef6161465434c9b478851f"
integrity sha512-lkJvwNJQUPW2SbVwAZW9s9whJp02nzLf2yTNwMULa4LloED9MYS1aNnGeoBCifpAI1pEBkTpLhuyRmBnLEOZAA==
dependencies:
"@types/anymatch" "*"
"@types/node" "*"
"@types/tapable" "*"
"@types/uglify-js" "*"
"@types/webpack-sources" "*"
source-map "^0.6.0"
"@types/webpack@^4.4.31", "@types/webpack@^4.4.35":
version "4.4.35"
resolved "https://registry.yarnpkg.com/@types/webpack/-/webpack-4.4.35.tgz#b7088eb2d471d5645e5503d272783cafa753583b"
@ -5380,6 +5359,11 @@ glob-parent@^5.0.0:
dependencies:
is-glob "^4.0.1"
glob-to-regexp@^0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.3.0.tgz#8c5a1494d2066c570cc3bfe4496175acc4d502ab"
integrity sha1-jFoUlNIGbFcMw7/kSWF1rMTVAqs=
glob@7.1.2:
version "7.1.2"
resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15"
@ -7908,17 +7892,27 @@ monaco-editor-textmate@^2.2.1:
resolved "https://registry.yarnpkg.com/monaco-editor-textmate/-/monaco-editor-textmate-2.2.1.tgz#93f3f1932061dd2311b92a42ea1c027cfeb3e439"
integrity sha512-RYTNNfvyjK15M0JA8WIi9UduU10eX5724UGNKnaA8MSetehjThGENctUTuKaxPk/k3pq59QzaQ/C06A44iJd3Q==
monaco-editor-webpack-plugin@^1.7.0:
version "1.7.0"
resolved "https://registry.yarnpkg.com/monaco-editor-webpack-plugin/-/monaco-editor-webpack-plugin-1.7.0.tgz#920cbeecca25f15d70d568a7e11b0ba4daf1ae83"
integrity sha512-oItymcnlL14Sjd7EF7q+CMhucfwR/2BxsqrXIBrWL6LQplFfAfV+grLEQRmVHeGSBZ/Gk9ptzfueXnWcoEcFuA==
monaco-editor-webpack-plugin@^1.8.2:
version "1.8.2"
resolved "https://registry.yarnpkg.com/monaco-editor-webpack-plugin/-/monaco-editor-webpack-plugin-1.8.2.tgz#3721b8d9a3e2e41b154cf2a2955a7d7246c03714"
integrity sha512-g9G7A/lxQtpPsYaZFBqm73dwVkOziGUXExIR6iW7ksZUaiMkpvdTiE9O8edgdJGo+XtCmjycmIKB1Lt8VKbSTQ==
dependencies:
"@types/webpack" "^4.4.19"
loader-utils "^1.2.3"
monaco-editor@^0.18.1:
version "0.18.1"
resolved "https://registry.yarnpkg.com/monaco-editor/-/monaco-editor-0.18.1.tgz#ced7c305a23109875feeaf395a504b91f6358cfc"
integrity sha512-fmL+RFZ2Hrezy+X/5ZczQW51LUmvzfcqOurnkCIRFTyjdVjzR7JvENzI6+VKBJzJdPh6EYL4RoWl92b2Hrk9fw==
monaco-editor@^0.19.2:
version "0.19.3"
resolved "https://registry.yarnpkg.com/monaco-editor/-/monaco-editor-0.19.3.tgz#1c994b3186c00650dbcd034d5370d46bf56c0663"
integrity sha512-2n1vJBVQF2Hhi7+r1mMeYsmlf18hjVb6E0v5SoMZyb4aeOmYPKun+CE3gYpiNA1KEvtSdaDHFBqH9d7Wd9vREg==
monaco-languageclient@^0.11.0:
version "0.11.0"
resolved "https://registry.yarnpkg.com/monaco-languageclient/-/monaco-languageclient-0.11.0.tgz#0968ec143c98bf2c9fa69c2a84ac9ad5448e039d"
integrity sha512-aqvgI+gX5K711eCJ3ggsMIeJ5fv7LnhxgzkMPRxSrkp+5/KLKY80V/m7PzDvWri+zF5cZ6InIPSmAAekn4oRzA==
dependencies:
glob-to-regexp "^0.3.0"
vscode-jsonrpc "^4.1.0-next"
vscode-languageclient "^5.3.0-next"
vscode-uri "^1.0.5"
monaco-textmate@^3.0.1:
version "3.0.1"
@ -9410,7 +9404,7 @@ prop-types@15.x, prop-types@^15.5.10, prop-types@^15.5.6, prop-types@^15.5.8, pr
loose-envify "^1.3.1"
object-assign "^4.1.1"
prop-types@^15.0.0, prop-types@^15.5.0, prop-types@^15.7.2:
prop-types@^15.5.0, prop-types@^15.7.2:
version "15.7.2"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5"
integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==
@ -9772,13 +9766,13 @@ react-markdown@^4.0.3:
unist-util-visit "^1.3.0"
xtend "^4.0.1"
react-monaco-editor@^0.32.1:
version "0.32.1"
resolved "https://registry.yarnpkg.com/react-monaco-editor/-/react-monaco-editor-0.32.1.tgz#fa45d62fd19d5942cba98bd7c59336d21f8750e0"
integrity sha512-gJjU9Rx/QuJr+Y4C0MSidMdkh1hmHGneIU8yI87bc5kd46ZXPNETqiigyUB7pKy4ZSuFHBhjhg2lgESaID43ag==
react-monaco-editor@^0.33.0:
version "0.33.0"
resolved "https://registry.yarnpkg.com/react-monaco-editor/-/react-monaco-editor-0.33.0.tgz#822c331836ec39b1160faf22b0c722266f822460"
integrity sha512-zFo3A55QHCABYm4Xt0HWQMq10GI+oxhhCUGDYgzLksU1iGrdvHudUNXTDZvE43B1gM+cPyJ75jla/464kss8Iw==
dependencies:
"@types/react" "^15.x || ^16.x"
prop-types "^15.0.0"
prop-types "^15.7.2"
react-onclickoutside@^6.7.1:
version "6.7.1"
@ -12028,6 +12022,42 @@ vm-browserify@^1.0.1:
resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-1.1.2.tgz#78641c488b8e6ca91a75f511e7a3b32a86e5dda0"
integrity sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==
vscode-jsonrpc@^4.1.0-next:
version "4.1.0-next.3"
resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-4.1.0-next.3.tgz#05fe742959a2726020d4d0bfbc3d3c97873c7fde"
integrity sha512-Z6oxBiMks2+UADV1QHXVooSakjyhI+eHTnXzDyVvVMmegvSfkXk2w6mPEdSkaNHFBdtWW7n20H1yw2nA3A17mg==
vscode-jsonrpc@^5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-5.0.1.tgz#9bab9c330d89f43fc8c1e8702b5c36e058a01794"
integrity sha512-JvONPptw3GAQGXlVV2utDcHx0BiY34FupW/kI6mZ5x06ER5DdPG/tXWMVHjTNULF5uKPOUUD0SaXg5QaubJL0A==
vscode-languageclient@^5.3.0-next:
version "5.3.0-next.9"
resolved "https://registry.yarnpkg.com/vscode-languageclient/-/vscode-languageclient-5.3.0-next.9.tgz#34f58017647f15cd86015f7af45935dc750611f7"
integrity sha512-BFA3X1y2EI2CfsSBy0KG2Xr5BOYfd/97jTmD+doqL6oj+cY8S7AmRCOwb2f9Hbjq8GWL7YC+OJ0leZEUSPgP0A==
dependencies:
semver "^6.3.0"
vscode-languageserver-protocol "^3.15.0-next.8"
vscode-languageserver-protocol@^3.15.0-next.8:
version "3.15.2"
resolved "https://registry.yarnpkg.com/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.15.2.tgz#e52c62923140b2655ad2472f6f29cfb83bacf5b8"
integrity sha512-GdL05JKOgZ76RDg3suiGCl9enESM7iQgGw4x93ibTh4sldvZmakHmTeZ4iUApPPGKf6O3OVBtrsksBXnHYaxNg==
dependencies:
vscode-jsonrpc "^5.0.1"
vscode-languageserver-types "3.15.1"
vscode-languageserver-types@3.15.1:
version "3.15.1"
resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.15.1.tgz#17be71d78d2f6236d414f0001ce1ef4d23e6b6de"
integrity sha512-+a9MPUQrNGRrGU630OGbYVQ+11iOIovjCkqxajPa9w57Sd5ruK8WQNsslzpa0x/QJqC8kRc2DUxWjIFwoNm4ZQ==
vscode-uri@^1.0.5:
version "1.0.8"
resolved "https://registry.yarnpkg.com/vscode-uri/-/vscode-uri-1.0.8.tgz#9769aaececae4026fb6e22359cb38946580ded59"
integrity sha512-obtSWTlbJ+a+TFRYGaUumtVwb+InIUVI0Lu0VBUAPmj2cU5JutEXg3xUE0c2J5Tcy7h2DEKVJBFi+Y9ZSFzzPQ==
w3c-hr-time@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/w3c-hr-time/-/w3c-hr-time-1.0.1.tgz#82ac2bff63d950ea9e3189a58a65625fedf19045"