Skip to content

Commit

Permalink
feat: LG LSP in Composer (#1504)
Browse files Browse the repository at this point in the history
* Add LSP of LG

* change folder name

* change folder location

* seperate server and client demo

* add completion and hover for builtin-functions

* change file names

* change dependency

* refactor the package

* update demo readme

* make some refine

* remove npm lock file

* add demos to workspace and run test in 1 command

* remove declaration files

* change tsc outDir to dist, simplify test command

* add syntax highlight in demo and new API

* change naming of the project and move the demo

* remove package.json in demo

* change the content in readme

* fix lg-lsp package publish problem

* fix build command  and redundent d.ts files

* integrate LG LSP server to composer server

* change api in demo

* change the order of commands in build:prod

* delete redundent files generated from build

* change lg-lsp-server api to attachLSPServer

* remove gitignore in lg-lsp-server demo

* remove attachLSPServer in server

* stash

* freeze vscode-languageserver-protocol@3.15.0-next.8

* stash code-editor lsp demo

* update

* build react sample demo

* version 17

* launch lsp server

* connect server

* calc offset

* concat full content

* clean demo

* update

* do not build lglsp editor

* update demo

* update before build

* clean build

* inline mode

* build

* lsp working demo in composer

* renaming

* eslint and build fix

* add utils

* monaco editor core component

* wrap up editor component

* seperate client and server

* update wrap

* attach language server to composer server

* token rules and suggestion context awareness

* complete demo

* inline template editor in form

* inline in all-up view

* refactor

* check file

* update

* remove monaco-webpack-plugin

* import diectly instead of call register

* create language server url

* add package

* allow suggestion in plaintext state

* update integrity hash

* exist check

* add back monaco-webpack-plugin

* add mixed demo

* code refactor

* merge folder

* add dependency

* resolve alias in jest

* fix type

* resolve typing

* clean up

* clean up

* Update Composer/package.json

Co-Authored-By: Andy Brown <asbrown002@gmail.com>

* Update Composer/packages/tools/language-servers/language-generation/src/utils.ts

Co-Authored-By: Andy Brown <asbrown002@gmail.com>

* Update Composer/packages/tools/language-servers/package.json

Co-Authored-By: Andy Brown <asbrown002@gmail.com>

* refactor lsp server

* refactor lsp client

* resolve eslint

* move static syntax setting to code editor

* clean up

* clean

* use latest @types/vscode replace vscode

* only path map actual vscode module

not including the `$` will make jest replace all modules with *vscode*

* disable lint errors

* resolve code review warning

* clean up

* pass less data

* fix type

* clean up, typo, refine and naming

* clean up

* extends base

* hide source name

* add test

* eslint resolve

* close connection when document close

* update

* add time stamp for every new connection

* pass parameters to inline editor

* match lsp server port with env server port

* rename /lgServer to /lg-language-server

* refactor code

* compatible  LG Parser, prevent crash

* do not close shared connection

* send initial diagnostics with delay

* update demo

* refactor error handling

* refine, review

* error handling

* ignore non-exist references at check template body

* update tokenizer

* close websocket when editor unmount

* all editors  use a shared unique ws connection

* fix structure lg in inline mode and comment token

* fix lint

* update structure name token

* fix fence_block miss recognize template name

* remove plaintext in allowState for LG completion

* control initial diagnostics send

* fix regExp in getWordAtPosition

* optimize data passing

* update test

* eslint fix

* update test
  • Loading branch information
zhixzhan authored and cwhitten committed Nov 29, 2019
1 parent 70e13ef commit ae7fa1d
Show file tree
Hide file tree
Showing 60 changed files with 3,474 additions and 396 deletions.
41 changes: 37 additions & 4 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
@@ -1,14 +1,41 @@
{
"version": "0.2.0",
"configurations": [
{
"type": "chrome",
"request": "attach",
"name": "Attach to Chrome",
"port": 9222,
"webRoot": "${workspaceFolder}"
},
{
"name": "LSP Server",
"type": "node",
"request": "launch",
"args": [
"${workspaceFolder}/Composer/packages/tools/language-servers/language-generation/demo/src/server.ts"
],
"runtimeArgs": [
"--nolazy",
"-r",
"${workspaceFolder}/Composer/node_modules/ts-node/register"
],
"sourceMaps": true,
"cwd": "${workspaceFolder}/Composer/packages/tools/language-servers/language-generation/demo/src",
"protocol": "inspector",
},
{
"type": "node",
"request": "launch",
"name": "Server: Launch",
"args": ["./build/server.js"],
"args": [
"./build/server.js"
],
"preLaunchTask": "server: build",
"restart": true,
"outFiles": ["./build/*"],
"outFiles": [
"./build/*"
],
"envFile": "${workspaceFolder}/Composer/packages/server/.env",
"outputCapture": "std",
"cwd": "${workspaceFolder}/Composer/packages/server"
Expand All @@ -20,8 +47,14 @@
"name": "Jest Debug",
"program": "${workspaceRoot}/Composer/node_modules/jest/bin/jest",
"stopOnEntry": false,
"args": ["--runInBand", "--env=jsdom", "--config=jest.config.js"],
"runtimeArgs": ["--inspect-brk"],
"args": [
"--runInBand",
"--env=jsdom",
"--config=jest.config.js"
],
"runtimeArgs": [
"--inspect-brk"
],
"cwd": "${workspaceRoot}/Composer/packages/server",
"sourceMaps": true,
"console": "integratedTerminal"
Expand Down
1 change: 1 addition & 0 deletions Composer/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,5 +41,6 @@ module.exports = {
'<rootDir>/packages/extensions/visual-designer',
'<rootDir>/packages/lib/code-editor',
'<rootDir>/packages/lib/shared',
'<rootDir>/packages/tools/language-servers/language-generation',
],
};
10 changes: 7 additions & 3 deletions Composer/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,20 @@
"packages/extensions",
"packages/extensions/*",
"packages/lib",
"packages/lib/*"
"packages/lib/*",
"packages/tools",
"packages/tools/language-servers",
"packages/tools/language-servers/*"
],
"scripts": {
"build": "node scripts/begin.js && yarn build:prod",
"build:prod": "yarn build:lib && yarn build:extensions && yarn build:server && yarn build:client",
"build:dev": "yarn build:lib && yarn build:extensions",
"build:prod": "yarn build:dev && yarn build:server && yarn build:client",
"build:dev": "yarn build:tools && yarn build:lib && yarn build:extensions",
"build:lib": "yarn workspace @bfc/libs build:all",
"build:extensions": "yarn workspace @bfc/extensions build:all",
"build:server": "yarn workspace @bfc/server build",
"build:client": "yarn workspace @bfc/client build",
"build:tools": "yarn workspace @bfc/tools build:all",
"start": "cross-env NODE_ENV=production PORT=3000 yarn start:server",
"startall": "concurrently --kill-others-on-fail \"npm:runtime\" \"npm:start\"",
"start:dev": "concurrently \"npm:start:client\" \"npm:start:server:dev\"",
Expand Down
6 changes: 5 additions & 1 deletion Composer/packages/client/config/webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,10 @@ module.exports = function(webpackEnv) {
// `web` extension prefixes have been added for better support
// for React Native Web.
extensions: paths.moduleFileExtensions.map(ext => `.${ext}`),
alias: {
// Support lsp code editor
vscode: require.resolve('monaco-languageclient/lib/vscode-compatibility'),
},
plugins: [
// Adds support for installing with Plug'n'Play, leading to faster installs and adding
// guards against forgotten dependencies and such.
Expand Down Expand Up @@ -402,7 +406,7 @@ module.exports = function(webpackEnv) {
plugins: [
new MonacoWebpackPlugin({
// available options are documented at https://github.com/Microsoft/monaco-editor-webpack-plugin#options
languages: ['markdown', 'botbuilderlg', 'json'],
languages: ['markdown', 'json'],
}),
// Generates an `index.html` file with the <script> injected.
new HtmlWebpackPlugin(
Expand Down
2 changes: 2 additions & 0 deletions Composer/packages/client/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ module.exports = {
// Any imports of .scss / .css files will instead import styleMock.js which is an empty object
'\\.(jpg|jpeg|png|svg|gif)$': '<rootDir>/__tests__/jestMocks/styleMock.js',
'\\.(s)?css$': '<rootDir>/__tests__/jestMocks/styleMock.js',
// lsp code editor
vscode$: 'monaco-languageclient/lib/vscode-compatibility',

// use commonjs modules for test so they do not need to be compiled
'office-ui-fabric-react/lib/(.*)$': 'office-ui-fabric-react/lib-commonjs/$1',
Expand Down
106 changes: 87 additions & 19 deletions Composer/packages/client/src/pages/language-generation/code-editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,45 +2,108 @@
// Licensed under the MIT License.

/* eslint-disable react/display-name */
import React, { useState, useEffect } from 'react';
import { LgEditor } from '@bfc/code-editor';
import React, { useState, useEffect, useMemo, useContext } from 'react';
import { LgEditor, LGOption } from '@bfc/code-editor';
import get from 'lodash/get';
import debounce from 'lodash/debounce';
import isEmpty from 'lodash/isEmpty';
import { CodeRange, LgFile } from '@bfc/shared';
import { Diagnostic } from 'botbuilder-lg';
import { LgFile } from '@bfc/shared';

import { StoreContext } from '../../store';
import * as lgUtil from '../../utils/lgUtil';

interface CodeEditorProps {
file: LgFile;
onChange: (value: string) => void;
codeRange?: Partial<CodeRange> | null;
template: lgUtil.Template | null;
}

// lsp server port should be same with composer/server port.
const lspServerPort = process.env.NODE_ENV === 'production' ? process.env.PORT || 3000 : 5000;
const lspServerPath = '/lg-language-server';

export default function CodeEditor(props: CodeEditorProps) {
const { file, codeRange } = props;
const onChange = debounce(props.onChange, 500);
const { actions } = useContext(StoreContext);
const { file, template } = props;
const [diagnostics, setDiagnostics] = useState(get(file, 'diagnostics', []));
const [content, setContent] = useState(get(file, 'content', ''));

const [content, setContent] = useState('');
const [errorMsg, setErrorMsg] = useState('');
const fileId = file && file.id;
const inlineMode = !!template;
useEffect(() => {
// reset content with file.content's initial state
if (isEmpty(file)) return;
setContent(file.content);
}, [fileId]);
const value = template ? get(template, 'Body', '') : get(file, 'content', '');
setContent(value);
}, [fileId, template]);

useEffect(() => {
const isInvalid = !lgUtil.isValid(diagnostics);
const text = isInvalid ? lgUtil.combineMessage(diagnostics) : '';
setErrorMsg(text);
}, [diagnostics]);

const updateLgTemplate = useMemo(
() =>
debounce((body: string) => {
const templateName = get(template, 'Name');
if (!templateName) return;
const payload = {
file,
templateName,
template: {
Name: templateName,
Parameters: get(template, 'Parameters'),
Body: body,
},
};
actions.updateLgTemplate(payload);
}, 500),
[file, template]
);

const updateLgFile = useMemo(
() =>
debounce((content: string) => {
const payload = {
id: file.id,
content,
};
actions.updateLgFile(payload);
}, 500),
[file]
);

// local content maybe invalid and should always sync real-time
// file.content assume to be load from server
const _onChange = value => {
setContent(value);
onChange(value);
const diagnostics = lgUtil.check(value);

let diagnostics: Diagnostic[] = [];
if (inlineMode) {
const content = get(file, 'content', '');
const templateName = get(template, 'Name', '');
try {
const newContent = lgUtil.updateTemplate(content, templateName, {
Name: templateName,
Parameters: get(template, 'Parameters'),
Body: value,
});
diagnostics = lgUtil.check(newContent);
updateLgTemplate(value);
} catch (error) {
setErrorMsg(error.message);
}
} else {
diagnostics = lgUtil.check(value);
updateLgFile(value);
}
setDiagnostics(diagnostics);
};

const isInvalid = !lgUtil.isValid(diagnostics);
const errorMsg = isInvalid ? lgUtil.combineMessage(diagnostics) : '';
const lgOption: LGOption = {
inline: inlineMode,
content: get(file, 'content', ''),
template: template ? template : undefined,
};

return (
<LgEditor
Expand All @@ -53,9 +116,14 @@ export default function CodeEditor(props: CodeEditorProps) {
lineDecorationsWidth: undefined,
lineNumbersMinChars: false,
}}
codeRange={codeRange || -1}
errorMsg={errorMsg}
hidePlaceholder={inlineMode}
value={content}
errorMsg={errorMsg}
lgOption={lgOption}
languageServer={{
port: Number(lspServerPort),
path: lspServerPath,
}}
onChange={_onChange}
/>
);
Expand Down
43 changes: 12 additions & 31 deletions Composer/packages/client/src/pages/language-generation/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,11 @@ import React, { useContext, Fragment, useEffect, useState, useMemo, Suspense } f
import formatMessage from 'format-message';
import { Toggle } from 'office-ui-fabric-react/lib/Toggle';
import { Nav, INavLinkGroup, INavLink } from 'office-ui-fabric-react/lib/Nav';
import get from 'lodash/get';
import { LGTemplate } from 'botbuilder-lg';
import { RouteComponentProps } from '@reach/router';
import { CodeRange } from '@bfc/shared';
import get from 'lodash/get';

import { LoadingSpinner } from '../../components/LoadingSpinner';
import { OpenAlertModal, DialogStyle } from '../../components/Modal';
import { StoreContext } from '../../store';
import {
ContentHeaderStyle,
Expand All @@ -33,11 +32,11 @@ import { TestController } from './../../TestController';
const CodeEditor = React.lazy(() => import('./code-editor'));

const LGPage: React.FC<RouteComponentProps> = props => {
const { state, actions } = useContext(StoreContext);
const { state } = useContext(StoreContext);
const { lgFiles, dialogs } = state;
const [editMode, setEditMode] = useState(false);
const [fileValid, setFileValid] = useState(true);
const [codeRange, setCodeRange] = useState<CodeRange | null>(null);
const [inlineTemplate, setInlineTemplate] = useState<null | lgUtil.Template>(null);

const subPath = props['*'];
const isRoot = subPath === '';
Expand Down Expand Up @@ -121,34 +120,16 @@ const LGPage: React.FC<RouteComponentProps> = props => {

function onToggleEditMode() {
setEditMode(!editMode);
setCodeRange(null);
}

async function onChange(newContent) {
if (!lgFile) {
return;
}

const payload = {
id: lgFile.id,
content: newContent,
};

try {
await actions.updateLgFile(payload);
} catch (error) {
OpenAlertModal('Save Failed', error.message, {
style: DialogStyle.Console,
});
}
setInlineTemplate(null);
}

// #TODO: get line number from lg parser, then deep link to code editor this
// Line
function onTableViewClickEdit(template) {
setCodeRange({
startLineNumber: get(template, 'ParseTree._start._line', 0),
endLineNumber: get(template, 'ParseTree._stop._line', 0),
function onTableViewClickEdit(template: LGTemplate) {
setInlineTemplate({
Name: get(template, 'Name', ''),
Parameters: get(template, 'Parameters'),
Body: get(template, 'Body', ''),
});
navigateTo(`/language-generation`);
setEditMode(true);
Expand All @@ -174,7 +155,7 @@ const LGPage: React.FC<RouteComponentProps> = props => {
onText={formatMessage('Edit mode')}
offText={formatMessage('Edit mode')}
checked={editMode}
disabled={(!isRoot && editMode === false) || (codeRange === null && fileValid === false)}
disabled={(!isRoot && editMode === false) || (fileValid === false && editMode === true)}
onChange={onToggleEditMode}
/>
</div>
Expand Down Expand Up @@ -215,7 +196,7 @@ const LGPage: React.FC<RouteComponentProps> = props => {
<div css={contentEditor}>
{editMode ? (
<Suspense fallback={<LoadingSpinner />}>
<CodeEditor file={lgFile} codeRange={codeRange} onChange={onChange} />
<CodeEditor file={lgFile} template={inlineTemplate} />
</Suspense>
) : (
<TableView file={lgFile} activeDialog={activeDialog} onClickEdit={onTableViewClickEdit} />
Expand Down
12 changes: 8 additions & 4 deletions Composer/packages/client/src/utils/lgUtil.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ const lgStaticChecker = new StaticChecker();

const lgImportResolver = ImportResolver.fileResolver;

interface Template {
export interface Template {
Name: string;
Parameters: string[];
Parameters?: string[];
Body: string;
}

Expand Down Expand Up @@ -69,7 +69,11 @@ export function increaseNameUtilNotExist(templates: LGTemplate[], name: string):
return newName;
}

export function updateTemplate(content: string, templateName: string, { Name, Parameters, Body }: Template): string {
export function updateTemplate(
content: string,
templateName: string,
{ Name, Parameters = [], Body }: Template
): string {
const resource = LGParser.parse(content);
// add if not exist
if (resource.Templates.findIndex(t => t.Name === templateName) === -1) {
Expand All @@ -80,7 +84,7 @@ export function updateTemplate(content: string, templateName: string, { Name, Pa
}

// if Name exist, throw error.
export function addTemplate(content: string, { Name, Parameters, Body }: Template): string {
export function addTemplate(content: string, { Name, Parameters = [], Body }: Template): string {
const resource = LGParser.parse(content);
return resource.addTemplate(Name, Parameters, Body).toString();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,9 @@ module.exports = {
resolve: {
extensions: ['.tsx', '.ts', '.js'],
plugins: [new TsconfigPathsPlugin({ configFile: path.resolve(__dirname, './tsconfig.json') })],
alias: {
vscode: require.resolve('monaco-languageclient/lib/vscode-compatibility'),
},
},

output: {
Expand Down
Loading

0 comments on commit ae7fa1d

Please sign in to comment.