diff --git a/package-lock.json b/package-lock.json index 5f6a310..21d0fe5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7868,7 +7868,7 @@ "version": "5.5.2", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.2.tgz", "integrity": "sha512-NcRtPEOsPFFWjobJEtfihkLCZCXZt/os3zf8nTxjVH3RvTSxjrCamJpbExGvYOF+tFHc3pA65qpdwPbzjohhew==", - "devOptional": true, + "dev": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/src/common/monaco/completion.ts b/src/common/monaco/completion.ts new file mode 100644 index 0000000..f4899eb --- /dev/null +++ b/src/common/monaco/completion.ts @@ -0,0 +1,64 @@ +import * as monaco from 'monaco-editor'; +import { keywords } from './lexerRules.ts'; + +const provideMethodCompletionItems = (lineContent: string) => { + const methods = new Map([ + [/^ge?t?$/gi, 'GET '], + [/^put?$/gi, 'PUT '], + [/^pos?t?$/gi, 'POST '], + [/^de?l?e?t?e?$/gi, 'DELETE '], + ]); + const matchedMethodKey = Array.from(methods.keys()).find(regex => regex.test(lineContent)); + console.log('matchedMethodKey', { matchedMethodKey, lineContent }); + if (!matchedMethodKey) { + return null; + } + + const method = methods.get(matchedMethodKey); + + return { + suggestions: [ + { + label: method, + kind: monaco.languages.CompletionItemKind.Constant, + insertText: method, + }, + ], + }; +}; + +const provideKeywordCompletionItems = (lineContent: string) => { + const word = lineContent.split(/[ /]+/).pop() || ''; + const suggestions = keywords + .filter(keyword => { + return keyword.startsWith(word); + }) + .map(keyword => ({ + label: keyword, + kind: monaco.languages.CompletionItemKind.Keyword, + insertText: keyword, + })); + return { suggestions }; +}; + +export const searchCompletionProvider = ( + model: monaco.editor.ITextModel, + position: monaco.Position, +) => { + const textUntilPosition = model.getValueInRange({ + startLineNumber: position.lineNumber, + startColumn: 1, + endLineNumber: position.lineNumber, + endColumn: position.column, + }); + + const methodCompletions = provideMethodCompletionItems(textUntilPosition); + if (methodCompletions) { + return methodCompletions; + } + const keywordCompletions = provideKeywordCompletionItems(textUntilPosition); + + if (keywordCompletions) { + return keywordCompletions; + } +}; diff --git a/src/common/monaco/index.ts b/src/common/monaco/index.ts index 3728c67..072034f 100644 --- a/src/common/monaco/index.ts +++ b/src/common/monaco/index.ts @@ -1,8 +1,9 @@ import * as monaco from 'monaco-editor'; -import { search, executeActions } from './lexerRules.ts'; +import { executeActions, search } from './lexerRules.ts'; import { monacoEnvironment } from './environment.ts'; import { buildSearchToken } from './tokenlizer.ts'; +import { searchCompletionProvider } from './completion.ts'; self.MonacoEnvironment = monacoEnvironment; @@ -19,47 +20,9 @@ monaco.languages.setLanguageConfiguration( monaco.languages.registerCompletionItemProvider(search.id, { triggerCharacters: ['g', 'p', 'd'], // @ts-ignore - provideCompletionItems: function (model, position) { - const textUntilPosition = model.getValueInRange({ - startLineNumber: position.lineNumber, - startColumn: 1, - endLineNumber: position.lineNumber, - endColumn: position.column, - }); - - const methods = new Map([ - [/^ge?t?$/gi, 'GET '], - [/^put?$/gi, 'PUT '], - [/^pos?t?$/gi, 'POST '], - [/^de?l?e?t?e?$/gi, 'DELETE '], - ]); - const matchedMethodKey = Array.from(methods.keys()).find(regex => - regex.test(textUntilPosition), - ); - if (!matchedMethodKey) { - return; - } - - const method = methods.get(matchedMethodKey); - const range = { - startLineNumber: position.lineNumber, - endLineNumber: position.lineNumber, - startColumn: position.column - method!.length, - endColumn: position.column, - }; - - return { - suggestions: [ - { - label: method, - kind: monaco.languages.CompletionItemKind.Constant, - insertText: method, - range: range, - }, - ], - }; - }, + provideCompletionItems: searchCompletionProvider, }); + export * from './type.ts'; export { monaco, executeActions, buildSearchToken }; export * from './referDoc.ts'; diff --git a/src/common/monaco/lexerRules.ts b/src/common/monaco/lexerRules.ts index 10d1240..cecfb5c 100644 --- a/src/common/monaco/lexerRules.ts +++ b/src/common/monaco/lexerRules.ts @@ -1,106 +1,9 @@ -export const xJson = { - id: 'xjson', - rules: { - defaultToken: 'invalid', - tokenPostfix: '', - escapes: /\\(?:[abfnrtv\\"']|x[0-9A-Fa-f]{1,4}|u[0-9A-Fa-f]{4}|U[0-9A-Fa-f]{8})/, - tokenizer: { - root: [ - [ - /("(?:[^"]*_)?script"|"inline"|"source")(\s*?)(:)(\s*?)(""")/, - [ - 'variable', - 'whitespace', - 'ace.punctuation.colon', - 'whitespace', - { - token: 'punctuation.start_triple_quote', - nextEmbedded: 'painless', - next: 'my_painless', - }, - ], - ], - [ - /(:)(\s*?)(""")(sql)/, - [ - 'ace.punctuation.colon', - 'whitespace', - 'punctuation.start_triple_quote', - { - token: 'punctuation.start_triple_quote.lang_marker', - nextEmbedded: 'opensearchql', - next: 'my_sql', - }, - ], - ], - [/{/, { token: 'paren.lparen', next: '@push' }], - [/}/, { token: 'paren.rparen', next: '@pop' }], - [/[[(]/, { token: 'paren.lparen' }], - [/[\])]/, { token: 'paren.rparen' }], - [/,/, { token: 'punctuation.comma' }], - [/:/, { token: 'punctuation.colon' }], - [/\s+/, { token: 'whitespace' }], - [/["](?:(?:\\.)|(?:[^"\\]))*?["]\s*(?=:)/, { token: 'variable' }], - [/"""/, { token: 'string_literal', next: 'string_literal' }], - [/0[xX][0-9a-fA-F]+\b/, { token: 'constant.numeric' }], - [/[+-]?\d+(?:(?:\.\d*)?(?:[eE][+-]?\d+)?)?\b/, { token: 'constant.numeric' }], - [/(?:true|false)\b/, { token: 'constant.language.boolean' }], - // strings - [/"([^"\\]|\\.)*$/, 'string.invalid'], // non-teminated string - [ - /"/, - { - token: 'string.quote', - bracket: '@open', - next: '@string', - }, - ], - [/['](?:(?:\\.)|(?:[^'\\]))*?[']/, { token: 'invalid' }], - [/.+?/, { token: 'text' }], - [/\/\/.*$/, { token: 'invalid' }], - ], - - my_painless: [ - [ - /"""/, - { - token: 'punctuation.end_triple_quote', - nextEmbedded: '@pop', - next: '@pop', - }, - ], - ], - - my_sql: [ - [ - /"""/, - { - token: 'punctuation.end_triple_quote', - nextEmbedded: '@pop', - next: '@pop', - }, - ], - ], - - string: [ - [/[^\\"]+/, 'string'], - [/@escapes/, 'string.escape'], - [/\\./, 'string.escape.invalid'], - [/"/, { token: 'string.quote', bracket: '@close', next: '@pop' }], - ], - - string_literal: [ - [/"""/, { token: 'punctuation.end_triple_quote', next: '@pop' }], - [/./, { token: 'multi_string' }], - ], - }, - }, -}; export const executeActions = { regexp: /^(GET|DELETE|POST|PUT)\s\w+/, decorationClassName: 'action-execute-decoration', }; -const keywords = [ + +export const keywords = [ 'GET', 'POST', 'PUT', @@ -117,6 +20,12 @@ const keywords = [ 'size', 'explain', 'analyze', + 'query', + 'filter', + 'aggs', + 'sort', + 'match', + 'match_all', 'default_operator', 'df', 'analyzer', @@ -198,6 +107,7 @@ const keywords = [ 'version', 'version_type', '_search', + '_cat', '_count', '_mapping', '_cluster', @@ -230,16 +140,16 @@ export const search = { // The main tokenizer for our languages tokenizer: { root: [ + [/^(GET|POST|PUT|DELETE)(\s+[a-zA-Z0-9_\/-?\-&,]*)/, ['type', 'regexp']], { regex: '{', action: { token: 'paren.lparen', - next: 'json', + next: 'xjson', }, }, { include: 'common' }, ], - constant: [[executeActions.regexp, executeActions.decorationClassName]], common: [ // identifiers and keywords [ @@ -256,30 +166,95 @@ export const search = { // whitespace { include: '@whitespace' }, // json block - { include: '@json' }, + { include: '@xjson' }, + ], + xjson: [ + [ + /("(?:[^"]*_)?script"|"inline"|"source")(\s*?)(:)(\s*?)(""")/, + [ + 'variable', + 'whitespace', + 'delimiter', + 'whitespace', + { + token: 'punctuation.start_triple_quote', + nextEmbedded: 'painless', + next: 'search_painless', + }, + ], + ], + [ + /(:)(\s*?)(""")(sql)/, + [ + 'delimiter', + 'whitespace', + 'punctuation.start_triple_quote', + { + token: 'punctuation.start_triple_quote.lang_marker', + nextEmbedded: 'opensearchql', + next: 'search_sql', + }, + ], + ], + [/{/, { token: 'paren.lparen', next: '@push' }], + [/}/, { token: 'paren.rparen', next: '@pop' }], + [/[[(]/, { token: 'paren.lparen' }], + [/[\])]/, { token: 'paren.rparen' }], + [/,/, { token: 'delimiter' }], + [/:/, { token: 'delimiter' }], + [/\s+/, { token: 'whitespace' }], + [/["](?:(?:\\.)|(?:[^"\\]))*?["]\s*(?=:)/, { token: 'variable' }], + [/"""/, { token: 'string_literal', next: 'string_literal' }], + [/0[xX][0-9a-fA-F]+\b/, { token: 'constant.numeric' }], + [/[+-]?\d+(?:(?:\.\d*)?(?:[eE][+-]?\d+)?)?\b/, { token: 'constant.numeric' }], + [/(?:true|false)\b/, { token: 'constant.language.boolean' }], + // strings + [/"([^"\\]|\\.)*$/, 'string.invalid'], // non-teminated string + [ + /"/, + { + token: 'string.quote', + bracket: '@open', + next: '@string', + }, + ], + [/['](?:(?:\\.)|(?:[^'\\]))*?[']/, { token: 'invalid' }], + [/.+?/, { token: 'text' }], + [/\/\/.*$/, { token: 'invalid' }], ], - json: [ - // JSON strings - [/"(?:\\.|[^\\"])*"/, 'string'], - - // JSON numbers - [/-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?/, 'number'], - - // JSON booleans - [/\b(?:true|false)\b/, 'keyword'], - - // JSON null - [/\bnull\b/, 'keyword'], + search_painless: [ + [ + /"""/, + { + token: 'punctuation.end_triple_quote', + nextEmbedded: '@pop', + next: '@pop', + }, + ], + ], - // JSON property names - [/"(?:\\.|[^\\"])*"(?=\s*:)/, 'key'], + search_sql: [ + [ + /"""/, + { + token: 'punctuation.end_triple_quote', + nextEmbedded: '@pop', + next: '@pop', + }, + ], + ], - // JSON punctuation - [/[{}[\],:]/, 'delimiter'], + string: [ + [/[^\\"]+/, 'string'], + [/@escapes/, 'string.escape'], + [/\\./, 'string.escape.invalid'], + [/"/, { token: 'string.quote', bracket: '@close', next: '@pop' }], + ], - // JSON whitespace - { include: '@whitespace' }, + string_literal: [ + [/"""/, { token: 'punctuation.end_triple_quote', next: '@pop' }], + [/./, { token: 'multi_string' }], ], whitespace: [ diff --git a/src/common/monaco/type.ts b/src/common/monaco/type.ts index cfb8501..dd6f994 100644 --- a/src/common/monaco/type.ts +++ b/src/common/monaco/type.ts @@ -62,3 +62,4 @@ export enum EngineType { } export type Monaco = typeof monaco.editor.create; +export type Editor = ReturnType; diff --git a/src/views/editor/display-editor.vue b/src/views/editor/display-editor.vue new file mode 100644 index 0000000..a7b0a53 --- /dev/null +++ b/src/views/editor/display-editor.vue @@ -0,0 +1,46 @@ + + + + + diff --git a/src/views/editor/index.vue b/src/views/editor/index.vue index bc80608..b031679 100644 --- a/src/views/editor/index.vue +++ b/src/views/editor/index.vue @@ -1,10 +1,10 @@ @@ -17,19 +17,19 @@ import { useMessage } from 'naive-ui'; import { CustomError } from '../../common'; import { useAppStore, useChatStore, useConnectionStore, useSourceFileStore } from '../../store'; import { useLang } from '../../lang'; +import DisplayEditor from './display-editor.vue'; + import { buildSearchToken, Decoration, defaultCodeSnippet, + Editor, EngineType, getActionApiDoc, - Monaco, monaco, SearchAction, } from '../../common/monaco'; -type Editor = ReturnType; - const appStore = useAppStore(); const message = useMessage(); const lang = useLang(); @@ -47,7 +47,6 @@ const { themeType } = storeToRefs(appStore); const chatStore = useChatStore(); const { insertBoard } = storeToRefs(chatStore); // https://github.com/tjx666/adobe-devtools/commit/8055d8415ed3ec5996880b3a4ee2db2413a71c61 -let displayEditor: Editor | null = null; let queryEditor: Editor | null = null; let autoIndentCmdId: string | null = null; // DOM @@ -114,9 +113,7 @@ let executeDecorations: Array = []; watch(themeType, () => { const vsTheme = getEditorTheme(); - queryEditor?.updateOptions({ theme: vsTheme }); - displayEditor?.updateOptions({ theme: vsTheme }); }); watch(insertBoard, () => { if (queryEditor) { @@ -143,10 +140,7 @@ watch(insertBoard, () => { } }); -const executeQueryAction = async ( - displayEditor: Editor, - position: { column: number; lineNumber: number }, -) => { +const executeQueryAction = async (position: { column: number; lineNumber: number }) => { const action = searchTokens.find( ({ actionPosition }) => actionPosition.startLineNumber === position.lineNumber, ); @@ -173,7 +167,7 @@ const executeQueryAction = async ( index: action.index || established.value?.activeIndex?.index, }); - displayJsonEditor(displayEditor, JSON.stringify(data, null, ' ')); + displayJsonEditor(JSON.stringify(data, null, ' ')); } catch (err) { const { status, details } = err as CustomError; message.error(`status: ${status}, details: ${details}`, { @@ -267,10 +261,9 @@ const setupQueryEditor = (code: string) => { event.leftButton && target.type === 4 && Object.values(target!.element!.classList).includes(executionGutterClass) && - queryEditor && - displayEditor + queryEditor ) { - executeQueryAction(displayEditor, target.position); + executeQueryAction(target.position); } }); @@ -289,7 +282,7 @@ const setupQueryEditor = (code: string) => { queryEditor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.Enter, () => { const { actionPosition } = getPointerAction(queryEditor!, searchTokens) || {}; if (actionPosition) { - executeQueryAction(displayEditor!, { + executeQueryAction({ column: actionPosition.startColumn, lineNumber: actionPosition.startLineNumber, }); @@ -366,30 +359,21 @@ const setupQueryEditor = (code: string) => { const queryEditorSize = ref(1); -const displayJsonEditor = (displayEditor: Editor, content: string) => { +const displayJsonEditor = (content: string) => { queryEditorSize.value = queryEditorSize.value === 1 ? 0.5 : queryEditorSize.value; - displayEditor?.getModel()?.setValue(content); -}; - -const setupJsonEditor = () => { - displayEditor = monaco.editor.create(displayEditorRef.value, { - automaticLayout: true, - theme: getEditorTheme(), - value: '', - language: 'json', - minimap: { enabled: false }, - }); + displayEditorRef.value.display(content); }; onMounted(async () => { await readSourceFromFile(); const code = defaultFile.value; setupQueryEditor(code); - setupJsonEditor(); }); onUnmounted(() => { codeLensProvider.dispose(); + queryEditor?.dispose(); + displayEditorRef.value.dispose(); }); // @ts-ignore listen('saveFile', async event => { @@ -400,7 +384,7 @@ listen('saveFile', async event => { }); -