Skip to content
This repository was archived by the owner on Jul 9, 2025. It is now read-only.
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ const IntentField: React.FC<FieldProps> = (props) => {
const { currentRecognizer } = useRecognizerConfig();

const Editor = currentRecognizer?.intentEditor;
const label = formatMessage('Trigger phrases (intent: #{intentName})', { intentName: value });
const label = formatMessage('Trigger phrases');

const handleChange = () => {
onChange(value);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ describe('<IntentField />', () => {
});

const { getByLabelText } = renderSubject({ value: 'MyIntent' });
expect(getByLabelText('Trigger phrases (intent: #MyIntent)')).toBeInTheDocument();
expect(getByLabelText('Trigger phrases')).toBeInTheDocument();
});

it('invokes change handler with intent name', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ const state = {
],
locale: 'en-us',
luFiles: [
{ id: 'a.en-us', content: initialContent, templates: initialIntents, diagnostics: [] },
{ id: 'a.fr-fr', content: initialContent, templates: initialIntents, diagnostics: [] },
{ id: 'a.en-us', content: initialContent, templates: initialIntents, diagnostics: [], intents: [] },
{ id: 'a.fr-fr', content: initialContent, templates: initialIntents, diagnostics: [], intents: [] },
],
settings: {
defaultLanguage: 'en-us',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -153,18 +153,20 @@ const CodeEditor: React.FC<CodeEditorProps> = (props) => {
languageServer={{
path: lspServerPath,
}}
luFile={file}
luOption={luOption}
value={content}
onChange={onChange}
onChangeSettings={handleSettingsChange}
/>
);
}, [luOption]);
}, [luOption, file]);

const defaultLanguageFileEditor = useMemo(() => {
return (
<LuEditor
editorSettings={userSettings.codeEditor}
luFile={defaultLangFile}
luOption={{
fileId: dialogId,
luFeatures,
Expand All @@ -176,7 +178,7 @@ const CodeEditor: React.FC<CodeEditorProps> = (props) => {
onChange={() => {}}
/>
);
}, [dialogId]);
}, [defaultLangFile, dialogId]);

return (
<Fragment>
Expand Down
6 changes: 3 additions & 3 deletions Composer/packages/lib/code-editor/src/BaseEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ export interface BaseEditorProps extends EditorProps {
helpURL?: string;
hidePlaceholder?: boolean;
id?: string;
onChange: (newValue: string) => void;
onChange: (newValue: string, isFlush?: boolean) => void;
onInit?: OnInit;
placeholder?: string;
value?: string;
Expand Down Expand Up @@ -192,10 +192,10 @@ const BaseEditor: React.FC<BaseEditorProps> = (props) => {

useEffect(() => {
if (editorRef.current) {
const disposable = editorRef.current.onDidChangeModelContent(() => {
const disposable = editorRef.current.onDidChangeModelContent((e) => {
if (editorRef.current) {
const newValue = editorRef.current.getValue();
setTimeout(() => onChange(newValue), 0);
setTimeout(() => onChange(newValue, e.isFlush), 0);
}
});

Expand Down
170 changes: 151 additions & 19 deletions Composer/packages/lib/code-editor/src/LuEditor.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,72 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

import React, { useRef, useState, useEffect } from 'react';
import { listen, MessageConnection } from 'vscode-ws-jsonrpc';
import get from 'lodash/get';
import { MonacoServices, MonacoLanguageClient } from 'monaco-languageclient';
import { LuFile } from '@botframework-composer/types';
import styled from '@emotion/styled';
import { EditorDidMount, Monaco } from '@monaco-editor/react';
import { FluentTheme, NeutralColors } from '@uifabric/fluent-theme';
import formatMessage from 'format-message';
import get from 'lodash/get';
import { MonacoLanguageClient, MonacoServices } from 'monaco-languageclient';
import { Icon } from 'office-ui-fabric-react/lib/Icon';
import { Link } from 'office-ui-fabric-react/lib/Link';
import { Stack } from 'office-ui-fabric-react/lib/Stack';
import { Text } from 'office-ui-fabric-react/lib/Text';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { listen, MessageConnection } from 'vscode-ws-jsonrpc';

import { registerLULanguage } from './languages';
import { createUrl, createWebSocket, createLanguageClient, sendRequestWithRetry } from './utils/lspUtil';
import { BaseEditor, BaseEditorProps, OnInit } from './BaseEditor';
import { defaultPlaceholder, LU_HELP } from './constants';
import { registerLULanguage } from './languages';
import { getDefaultMlEntityName } from './lu/constants';
import { useLuEntities } from './lu/hooks/useLuEntities';
import { LuEditorToolbar as DefaultLuEditorToolbar } from './lu/LuEditorToolbar';
import { LuLabelingMenu } from './lu/LuLabelingMenu';
import { ToolbarLuEntityType } from './lu/types';
import { LUOption } from './utils';
import { createLanguageClient, createUrl, createWebSocket, sendRequestWithRetry } from './utils/lspUtil';
import { computeDefineLuEntityEdits, computeInsertLuEntityEdits } from './utils/luUtils';
import { withTooltip } from './utils/withTooltip';

const LuEditorToolbar = styled(DefaultLuEditorToolbar)({
border: `1px solid ${NeutralColors.gray120}`,
borderBottom: 'none',
});

const linkStyles = {
root: {
fontSize: FluentTheme.fonts.small.fontSize,
':hover': { textDecoration: 'none' },
':active': { textDecoration: 'none' },
},
};

const botIconStyles = { root: { padding: '0 4px', fontSize: FluentTheme.fonts.small.fontSize } };
const grayTextStyle = { root: { color: NeutralColors.gray80, fontSize: FluentTheme.fonts.small.fontSize } };

const LuSectionLink = withTooltip(
{
content: (
<Text variant="small">
{formatMessage.rich('Edit this intent in<a>User Input view</a>', {
a: ({ children }) => (
<Text key="pageLink" variant="small">
<Icon iconName="People" styles={botIconStyles} />
{children}
</Text>
),
})}
</Text>
),
},
Link
);

const sectionLinkTokens = { childrenGap: 4 };
export interface LULSPEditorProps extends BaseEditorProps {
luOption?: LUOption;
helpURL?: string;
luFile?: LuFile;
languageServer?:
| {
host?: string;
Expand All @@ -25,11 +75,14 @@ export interface LULSPEditorProps extends BaseEditorProps {
path: string;
}
| string;
toolbarHidden?: boolean;
onNavigateToLuPage?: (luFileId: string, luSectionId?: string) => void;
}

const defaultLUServer = {
path: '/lu-language-server',
};

declare global {
interface Window {
monacoServiceInstance: MonacoServices;
Expand All @@ -43,7 +96,7 @@ type ServerEdit = {
};

/*
convert the edits results from the server to an exectable object in manoco editor
convert the edits results from the server to an executable object in monaco editor
*/
function convertEdit(serverEdit: ServerEdit) {
return {
Expand All @@ -69,11 +122,15 @@ const LuEditor: React.FC<LULSPEditorProps> = (props) => {
lightbulb: {
enabled: true,
},
contextmenu: false,
...props.options,
};

const {
toolbarHidden,
onNavigateToLuPage,
luOption,
luFile,
languageServer,
onInit: onInitProp,
placeholder = defaultPlaceholder,
Expand All @@ -89,6 +146,10 @@ const LuEditor: React.FC<LULSPEditorProps> = (props) => {
}

const [editor, setEditor] = useState<any>();
const entities = useLuEntities(luFile);

const [labelingMenuVisible, setLabelingMenuVisible] = useState(false);
const editorDomRef = useRef<HTMLElement | null>(null);

useEffect(() => {
if (!editor) return;
Expand All @@ -109,7 +170,7 @@ const LuEditor: React.FC<LULSPEditorProps> = (props) => {

const m = monacoRef.current;
if (m) {
// this is the correct way to combine keycodes in Monaco
// this is the correct way to combine key codes in Monaco
// eslint-disable-next-line no-bitwise
editor.addCommand(m.KeyMod.Shift | m.KeyCode.Enter, () => {
const position = editor.getPosition();
Expand Down Expand Up @@ -146,6 +207,7 @@ const LuEditor: React.FC<LULSPEditorProps> = (props) => {
sendRequestWithRetry(languageClient, 'initializeDocuments', { luOption, uri });
}
}, [editor]);

const onInit: OnInit = (monaco) => {
registerLULanguage(monaco);
monacoRef.current = monaco;
Expand All @@ -157,23 +219,93 @@ const LuEditor: React.FC<LULSPEditorProps> = (props) => {

const editorDidMount: EditorDidMount = (_getValue, editor) => {
setEditor(editor);
editorDomRef.current = editor.getDomNode();
if (typeof props.editorDidMount === 'function') {
return props.editorDidMount(_getValue, editor);
}
};

const defineEntity = useCallback(
(entityType: ToolbarLuEntityType, entityName?: string) => {
entityName = entityName || getDefaultMlEntityName(entityType);
if (editor) {
const luEdits = computeDefineLuEntityEdits(entityType, entityName, editor, entities);
if (luEdits?.edits?.length) {
editor.executeEdits('toolbarMenu', luEdits.edits);
if (luEdits.selection) {
editor.setSelection(luEdits.selection);
}

if (luEdits?.scrollLine) {
editor.revealLineInCenter(luEdits?.scrollLine);
}

editor.focus();
}
}
},
[editor, entities]
);

const insertEntity = useCallback(
(entityName: string) => {
if (editor) {
const edits = computeInsertLuEntityEdits(entityName, editor);
if (edits) {
editor.executeEdits('toolbarMenu', edits);
editor.focus();
}
}
},
[editor]
);

const navigateToLuPage = React.useCallback(() => {
onNavigateToLuPage?.(luOption?.fileId ?? 'common', luOption?.sectionId);
}, [onNavigateToLuPage, luOption]);

const onLabelingMenuToggled = React.useCallback((visible: boolean) => setLabelingMenuVisible(visible), []);

return (
<BaseEditor
helpURL={LU_HELP}
id={editorId}
placeholder={placeholder}
{...restProps}
editorDidMount={editorDidMount}
language="lu"
options={options}
theme="lu"
onInit={onInit}
/>
<>
<Stack verticalFill>
{!toolbarHidden && (
<LuEditorToolbar
editor={editor}
labelingMenuVisible={labelingMenuVisible}
luFile={luFile}
onDefineEntity={defineEntity}
onInsertEntity={insertEntity}
/>
)}

<BaseEditor
helpURL={LU_HELP}
id={editorId}
placeholder={placeholder}
{...restProps}
editorDidMount={editorDidMount}
language="lu"
options={options}
theme="lu"
onInit={onInit}
/>
{onNavigateToLuPage && luOption && (
<Stack horizontal tokens={sectionLinkTokens} verticalAlign="center">
<Text styles={grayTextStyle}>{formatMessage('Intent name: ')}</Text>
<LuSectionLink as="button" styles={linkStyles} onClick={navigateToLuPage}>
#{luOption.sectionId}
</LuSectionLink>
</Stack>
)}
</Stack>
<LuLabelingMenu
editor={editor}
luFile={luFile}
onInsertEntity={insertEntity}
onMenuToggled={onLabelingMenuToggled}
/>
</>
);
};

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

import { useState, useCallback } from 'react';

import { useDebounce } from './useDebounce';

/**
* Debounced query and callbacks for FluentUI SearchBox.
*/
export const useDebouncedSearchCallbacks = () => {
const [query, setQuery] = useState<string | undefined>();
const debouncedQuery = useDebounce<string | undefined>(query, 300);

const onSearchAbort = useCallback(() => {
setQuery('');
}, []);

const onSearchQueryChange = useCallback((_?: React.ChangeEvent<HTMLInputElement>, newValue?: string) => {
setQuery(newValue);
}, []);

return { onSearchAbort, onSearchQueryChange, query: debouncedQuery, setQuery };
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

import formatMessage from 'format-message';
import { IContextualMenuItem } from 'office-ui-fabric-react/lib/ContextualMenu';
import { Icon } from 'office-ui-fabric-react/lib/Icon';
import { Stack } from 'office-ui-fabric-react/lib/Stack';
import { Text } from 'office-ui-fabric-react/lib/Text';
import * as React from 'react';

const searchEmptyMessageStyles = { root: { height: 32 } };
const searchEmptyMessageTokens = { childrenGap: 8 };

/**
* Search empty view for contextual menu with search capability.
*/
export const useNoSearchResultMenuItem = (message?: string): IContextualMenuItem => {
message = message ?? formatMessage('no items found');
return React.useMemo(
() => ({
key: 'no_results',
onRender: () => (
<Stack
key="no_results"
horizontal
horizontalAlign="center"
styles={searchEmptyMessageStyles}
tokens={searchEmptyMessageTokens}
verticalAlign="center"
>
<Icon iconName="SearchIssue" title={message} />
<Text variant="small">{message}</Text>
</Stack>
),
}),
[message]
);
};
Loading