diff --git a/Composer/packages/client/src/pages/language-understanding/code-editor.tsx b/Composer/packages/client/src/pages/language-understanding/code-editor.tsx index 72fd35e5cb..4db4524cae 100644 --- a/Composer/packages/client/src/pages/language-understanding/code-editor.tsx +++ b/Composer/packages/client/src/pages/language-understanding/code-editor.tsx @@ -13,8 +13,9 @@ import { CodeEditorSettings } from '@bfc/shared'; import { useRecoilValue } from 'recoil'; import { LuFile } from '@bfc/shared'; -import { localeState, settingsState } from '../../recoilModel/atoms'; +import { dialogState, localeState, settingsState } from '../../recoilModel/atoms'; import { userSettingsState, dispatcherState, luFilesSelectorFamily } from '../../recoilModel'; +import { navigateTo } from '../../utils/navigation'; import TelemetryClient from '../../telemetry/TelemetryClient'; import { DiffCodeEditor } from './diff-editor'; @@ -43,6 +44,7 @@ const CodeEditor: React.FC = (props) => { const luFiles = useRecoilValue(luFilesSelectorFamily(actualProjectId)); const locale = useRecoilValue(localeState(actualProjectId)); const settings = useRecoilValue(settingsState(actualProjectId)); + const currentDialog = useRecoilValue(dialogState({ projectId: actualProjectId, dialogId })); const { languages, defaultLanguage } = settings; @@ -145,6 +147,19 @@ const CodeEditor: React.FC = (props) => { updateUserSettings({ codeEditor: settings }); }; + const navigateToLuPage = useCallback( + (luFileId: string, sectionId?: string) => { + // eslint-disable-next-line security/detect-non-literal-regexp + const pattern = new RegExp(`.${locale}`, 'g'); + const fileId = currentDialog.isFormDialog ? luFileId : luFileId.replace(pattern, ''); + const url = currentDialog.isFormDialog + ? `/bot/${projectId}/language-understanding/${currentDialog.id}/item/${fileId}` + : `/bot/${projectId}/language-understanding/${fileId}${sectionId ? `/edit?t=${sectionId}` : ''}`; + navigateTo(url); + }, + [projectId, locale, currentDialog] + ); + const currentLanguageFileEditor = useMemo(() => { return ( = (props) => { value={content} onChange={onChange} onChangeSettings={handleSettingsChange} + onNavigateToLuPage={navigateToLuPage} /> ); }, [luOption, file]); diff --git a/Composer/packages/lib/code-editor/src/LuEditor.tsx b/Composer/packages/lib/code-editor/src/LuEditor.tsx index cae133ef36..dab0448575 100644 --- a/Composer/packages/lib/code-editor/src/LuEditor.tsx +++ b/Composer/packages/lib/code-editor/src/LuEditor.tsx @@ -120,10 +120,11 @@ const LuEditor: React.FC = (props) => { autoClosingBrackets: 'always' as const, autoIndent: 'full' as const, folding: true, + definitions: true, lightbulb: { enabled: true, }, - contextmenu: false, + contextmenu: true, ...props.options, }; @@ -153,7 +154,24 @@ const LuEditor: React.FC = (props) => { const [labelingMenuVisible, setLabelingMenuVisible] = useState(false); const editorDomRef = useRef(null); + const onLuNavigationMsg = ( + languageClient: MonacoLanguageClient, + onNavigateToLuPage: ((luFileId: string, luSectionId?: string | undefined) => void) | undefined + ) => { + return languageClient.onReady().then(() => + languageClient.onNotification('LuGotoDefinition', (result) => { + if (luOption?.projectId) { + onNavigateToLuPage?.(result.fileId, result.intent); + } + }) + ); + }; + useEffect(() => { + if (props.options?.readOnly) { + return; + } + if (!editor) return; if (!window.monacoServiceInstance) { @@ -169,7 +187,7 @@ const LuEditor: React.FC = (props) => { webSocket, onConnection: (connection: MessageConnection) => { const languageClient = createLanguageClient(formatMessage('LU Language Client'), ['lu'], connection); - + onLuNavigationMsg(languageClient, onNavigateToLuPage); const m = monacoRef.current; if (m) { // this is the correct way to combine key codes in Monaco @@ -190,6 +208,7 @@ const LuEditor: React.FC = (props) => { editor.executeEdits(uri, edits); }) ); + const disposable = languageClient.start(); connection.onClose(() => disposable.dispose()); window.monacoLUEditorInstance = languageClient; @@ -198,6 +217,8 @@ const LuEditor: React.FC = (props) => { } else { const m = monacoRef.current; const languageClient = window.monacoLUEditorInstance; + onLuNavigationMsg(languageClient, onNavigateToLuPage); + if (m) { // this is the correct way to combine keycodes in Monaco // eslint-disable-next-line no-bitwise @@ -208,7 +229,7 @@ const LuEditor: React.FC = (props) => { } sendRequestWithRetry(languageClient, 'initializeDocuments', { luOption, uri }); } - }, [editor]); + }, [editor, onNavigateToLuPage]); const onInit: OnInit = (monaco) => { registerLULanguage(monaco); diff --git a/Composer/packages/tools/language-servers/language-understanding/src/LUServer.ts b/Composer/packages/tools/language-servers/language-understanding/src/LUServer.ts index 3c88ed234b..79893969ab 100644 --- a/Composer/packages/tools/language-servers/language-understanding/src/LUServer.ts +++ b/Composer/packages/tools/language-servers/language-understanding/src/LUServer.ts @@ -1,5 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +import path from 'path'; import URI from 'vscode-uri'; import { FoldingRangeParams, IConnection, TextDocuments } from 'vscode-languageserver'; @@ -36,6 +37,8 @@ export class LUServer { protected readonly pendingValidationRequests = new Map(); protected LUDocuments: LUDocument[] = []; private luParser = new LuParser(); + private _curFileId = ''; + private _curProjectId = ''; constructor( protected readonly connection: IConnection, @@ -66,6 +69,7 @@ export class LUServer { capabilities: { textDocumentSync: this.documents.syncKind, codeActionProvider: false, + definitionProvider: true, completionProvider: { resolveProvider: true, triggerCharacters: ['@', ' ', '{', ':', '[', '('], @@ -79,6 +83,7 @@ export class LUServer { }); this.connection.onCompletion((params) => this.completion(params)); this.connection.onDocumentOnTypeFormatting((docTypingParams) => this.docTypeFormat(docTypingParams)); + this.connection.onDefinition((params: TextDocumentPositionParams) => this.definitionHandler(params)); this.connection.onFoldingRanges((foldingRangeParams: FoldingRangeParams) => this.foldingRangeHandler(foldingRangeParams) ); @@ -101,6 +106,36 @@ export class LUServer { this.connection.listen(); } + protected definitionHandler(params: TextDocumentPositionParams): undefined { + const document = this.documents.get(params.textDocument.uri); + if (!document) { + return; + } + + const curLine = document.getText().split(/\r?\n/g)[params.position.line]; + // eslint-disable-next-line security/detect-unsafe-regex + const importRegex = /^\s*-?\s*\[[^[\]]+\](\(([^()#]+)(#([a-zA-Z0-9_-]*))?(\*([a-zA-Z0-9_-]+)\*)?\))/; + if (importRegex.test(curLine)) { + const importedFile = curLine.match(importRegex); + if (importedFile) { + const target = importedFile[2]; + const intent = importedFile[4]; + const fileId = path.parse(target).name; + const targetFile = this.importResolver?.(this._curFileId, target, this._curProjectId); + if (targetFile) { + this.connection.sendNotification('LuGotoDefinition', { + fileId: fileId, + intent: intent, + }); + } + + return; + } + } + + return; + } + protected async foldingRangeHandler(params: FoldingRangeParams): Promise { const document = this.documents.get(params.textDocument.uri); if (!document) { @@ -191,6 +226,8 @@ export class LUServer { } const id = fileId || uri; + this._curFileId = id; + this._curProjectId = projectId || ''; const { intents: sections, diagnostics } = await this.luParser.parse(content, id, luFeatures); return { sections, diagnostics, content };