|
@@ -0,0 +1,219 @@
|
|
|
|
|
+import * as monaco from 'monaco-editor'
|
|
|
|
|
+
|
|
|
|
|
+export type CodeEditorCompletionKind =
|
|
|
|
|
+ | 'method'
|
|
|
|
|
+ | 'function'
|
|
|
|
|
+ | 'property'
|
|
|
|
|
+ | 'field'
|
|
|
|
|
+ | 'variable'
|
|
|
|
|
+ | 'class'
|
|
|
|
|
+ | 'keyword'
|
|
|
|
|
+
|
|
|
|
|
+export interface CodeEditorCompletionSuggestion {
|
|
|
|
|
+ label: string
|
|
|
|
|
+ insertText?: string
|
|
|
|
|
+ detail?: string
|
|
|
|
|
+ documentation?: string
|
|
|
|
|
+ kind?: CodeEditorCompletionKind
|
|
|
|
|
+ snippet?: boolean
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+export interface CodeEditorCompletionTarget {
|
|
|
|
|
+ object: string
|
|
|
|
|
+ methods?: CodeEditorCompletionSuggestion[]
|
|
|
|
|
+ properties?: CodeEditorCompletionSuggestion[]
|
|
|
|
|
+ triggerCharacters?: string[]
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+export type CodeEditorCompletionConfig = Partial<Record<string, CodeEditorCompletionTarget[]>>
|
|
|
|
|
+
|
|
|
|
|
+interface LanguageProviderState {
|
|
|
|
|
+ disposable: monaco.IDisposable | null
|
|
|
|
|
+ models: Map<string, CodeEditorCompletionTarget[]>
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const completionKindMap: Record<CodeEditorCompletionKind, monaco.languages.CompletionItemKind> = {
|
|
|
|
|
+ method: monaco.languages.CompletionItemKind.Method,
|
|
|
|
|
+ function: monaco.languages.CompletionItemKind.Function,
|
|
|
|
|
+ property: monaco.languages.CompletionItemKind.Property,
|
|
|
|
|
+ field: monaco.languages.CompletionItemKind.Field,
|
|
|
|
|
+ variable: monaco.languages.CompletionItemKind.Variable,
|
|
|
|
|
+ class: monaco.languages.CompletionItemKind.Class,
|
|
|
|
|
+ keyword: monaco.languages.CompletionItemKind.Keyword
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const languageProviderStates = new Map<string, LanguageProviderState>()
|
|
|
|
|
+const modelLanguageState = new Map<string, string>()
|
|
|
|
|
+
|
|
|
|
|
+const getLanguageProviderState = (language: string): LanguageProviderState => {
|
|
|
|
|
+ const current = languageProviderStates.get(language)
|
|
|
|
|
+ if (current) {
|
|
|
|
|
+ return current
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const nextState: LanguageProviderState = {
|
|
|
|
|
+ disposable: null,
|
|
|
|
|
+ models: new Map()
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ languageProviderStates.set(language, nextState)
|
|
|
|
|
+ return nextState
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const escapeRegExp = (value: string) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
|
|
|
+
|
|
|
|
|
+const isObjectContextMatch = (textBeforeCursor: string, objectName: string) => {
|
|
|
|
|
+ return new RegExp(`(?:^|[^\\w$])${escapeRegExp(objectName)}\\.\\w*$`).test(textBeforeCursor)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const getTriggerCharacters = (models: Map<string, CodeEditorCompletionTarget[]>) => {
|
|
|
|
|
+ const triggerCharacters = new Set<string>()
|
|
|
|
|
+
|
|
|
|
|
+ for (const rules of models.values()) {
|
|
|
|
|
+ for (const rule of rules) {
|
|
|
|
|
+ const nextTriggerCharacters =
|
|
|
|
|
+ rule.triggerCharacters && rule.triggerCharacters.length ? rule.triggerCharacters : ['.']
|
|
|
|
|
+
|
|
|
|
|
+ nextTriggerCharacters.forEach((char) => triggerCharacters.add(char))
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return Array.from(triggerCharacters)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const toCompletionItem = (
|
|
|
|
|
+ suggestion: CodeEditorCompletionSuggestion,
|
|
|
|
|
+ range: monaco.IRange,
|
|
|
|
|
+ fallbackKind: monaco.languages.CompletionItemKind
|
|
|
|
|
+): monaco.languages.CompletionItem => {
|
|
|
|
|
+ return {
|
|
|
|
|
+ label: suggestion.label,
|
|
|
|
|
+ kind: suggestion.kind ? completionKindMap[suggestion.kind] : fallbackKind,
|
|
|
|
|
+ insertText: suggestion.insertText || suggestion.label,
|
|
|
|
|
+ insertTextRules: suggestion.snippet
|
|
|
|
|
+ ? monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet
|
|
|
|
|
+ : undefined,
|
|
|
|
|
+ detail: suggestion.detail,
|
|
|
|
|
+ documentation: suggestion.documentation,
|
|
|
|
|
+ range
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const buildSuggestions = (
|
|
|
|
|
+ rule: CodeEditorCompletionTarget,
|
|
|
|
|
+ range: monaco.IRange
|
|
|
|
|
+): monaco.languages.CompletionItem[] => {
|
|
|
|
|
+ const methodSuggestions = (rule.methods || []).map((item) =>
|
|
|
|
|
+ toCompletionItem(item, range, monaco.languages.CompletionItemKind.Method)
|
|
|
|
|
+ )
|
|
|
|
|
+ const propertySuggestions = (rule.properties || []).map((item) =>
|
|
|
|
|
+ toCompletionItem(item, range, monaco.languages.CompletionItemKind.Property)
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ return [...methodSuggestions, ...propertySuggestions]
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const registerLanguageProvider = (language: string) => {
|
|
|
|
|
+ const state = getLanguageProviderState(language)
|
|
|
|
|
+
|
|
|
|
|
+ state.disposable?.dispose()
|
|
|
|
|
+
|
|
|
|
|
+ state.disposable = monaco.languages.registerCompletionItemProvider(language, {
|
|
|
|
|
+ triggerCharacters: getTriggerCharacters(state.models),
|
|
|
|
|
+ provideCompletionItems(model, position) {
|
|
|
|
|
+ const rules = state.models.get(model.uri.toString())
|
|
|
|
|
+ if (!rules?.length) {
|
|
|
|
|
+ return { suggestions: [] }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const word = model.getWordUntilPosition(position)
|
|
|
|
|
+ const range = {
|
|
|
|
|
+ startLineNumber: position.lineNumber,
|
|
|
|
|
+ endLineNumber: position.lineNumber,
|
|
|
|
|
+ startColumn: word.startColumn,
|
|
|
|
|
+ endColumn: word.endColumn
|
|
|
|
|
+ }
|
|
|
|
|
+ const textBeforeCursor = model.getValueInRange({
|
|
|
|
|
+ startLineNumber: position.lineNumber,
|
|
|
|
|
+ startColumn: 1,
|
|
|
|
|
+ endLineNumber: position.lineNumber,
|
|
|
|
|
+ endColumn: position.column
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ const suggestions = rules.flatMap((rule) =>
|
|
|
|
|
+ isObjectContextMatch(textBeforeCursor, rule.object) ? buildSuggestions(rule, range) : []
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ return { suggestions }
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+export const clearCodeEditorCompletionConfig = (
|
|
|
|
|
+ modelOrUri: monaco.editor.ITextModel | string
|
|
|
|
|
+) => {
|
|
|
|
|
+ const modelUri = typeof modelOrUri === 'string' ? modelOrUri : modelOrUri.uri.toString()
|
|
|
|
|
+ const language = modelLanguageState.get(modelUri)
|
|
|
|
|
+
|
|
|
|
|
+ if (!language) {
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const state = languageProviderStates.get(language)
|
|
|
|
|
+ modelLanguageState.delete(modelUri)
|
|
|
|
|
+
|
|
|
|
|
+ if (!state) {
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ state.models.delete(modelUri)
|
|
|
|
|
+
|
|
|
|
|
+ if (!state.models.size) {
|
|
|
|
|
+ state.disposable?.dispose()
|
|
|
|
|
+ languageProviderStates.delete(language)
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ registerLanguageProvider(language)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+export const syncCodeEditorCompletionConfig = (
|
|
|
|
|
+ model: monaco.editor.ITextModel,
|
|
|
|
|
+ language: string,
|
|
|
|
|
+ config?: CodeEditorCompletionConfig
|
|
|
|
|
+) => {
|
|
|
|
|
+ const modelUri = model.uri.toString()
|
|
|
|
|
+
|
|
|
|
|
+ clearCodeEditorCompletionConfig(modelUri)
|
|
|
|
|
+
|
|
|
|
|
+ const rules = config?.[language] || []
|
|
|
|
|
+ if (!rules.length) {
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const state = getLanguageProviderState(language)
|
|
|
|
|
+ state.models.set(modelUri, rules)
|
|
|
|
|
+ modelLanguageState.set(modelUri, language)
|
|
|
|
|
+ registerLanguageProvider(language)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+export const shouldTriggerCodeEditorCompletion = (
|
|
|
|
|
+ model: monaco.editor.ITextModel,
|
|
|
|
|
+ position: monaco.Position,
|
|
|
|
|
+ language: string,
|
|
|
|
|
+ config?: CodeEditorCompletionConfig
|
|
|
|
|
+) => {
|
|
|
|
|
+ const rules = config?.[language] || []
|
|
|
|
|
+ if (!rules.length) {
|
|
|
|
|
+ return false
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const textBeforeCursor = model.getValueInRange({
|
|
|
|
|
+ startLineNumber: position.lineNumber,
|
|
|
|
|
+ startColumn: 1,
|
|
|
|
|
+ endLineNumber: position.lineNumber,
|
|
|
|
|
+ endColumn: position.column
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ return rules.some((rule) => isObjectContextMatch(textBeforeCursor, rule.object))
|
|
|
|
|
+}
|