@codemirror/view#PluginValue TypeScript Examples

The following examples show how to use @codemirror/view#PluginValue. You can vote up the ones you like or vote down the ones you don't like, and go to the original project or source file by following the links above each example. You may check out the related API usage on the sidebar.
Example #1
Source File: index.ts    From obsidian-banners with MIT License 5 votes vote down vote up
getViewPlugin = (plugin: BannersPlugin) => ViewPlugin.fromClass(class BannerPV implements PluginValue {
  decor: DecorationSet;

  constructor(view: EditorView) {
    this.decor = this.decorate(view.state);
  }

  update(_update: ViewUpdate) {
    const { docChanged, view, state, startState } = _update;
    if (docChanged || state.facet(bannerDecorFacet) !== startState.facet(bannerDecorFacet) || state.facet(iconDecorFacet) !== startState.facet(iconDecorFacet)) {
      this.decor = this.decorate(view.state);
    }
  }

  decorate(state: EditorState): DecorationSet {
    // If there's no YAML, stop here
    const cursor =  syntaxTree(state).cursor();
    cursor.firstChild();
    if (cursor.name !== YAML_SEPARATOR_TOKEN) { return Decoration.none }

    // Get all frontmatter fields to later process
    const frontmatter: {[key: string]: string} = {};
    let key;
    while (cursor.nextSibling() && cursor.name !== YAML_SEPARATOR_TOKEN) {
      const { from, to, name } = cursor;
      if (name === YAML_DEF_NAME_TOKEN) {
        key = state.sliceDoc(from, to);
      } else if (YAML_DEF_VAL_TOKENS.includes(name) && !frontmatter[key]) {
        const isStr = name === YAML_DEF_STR_TOKEN;
        const val = state.sliceDoc(from + (isStr ? 1 : 0), to - (isStr ? 1 : 0));
        frontmatter[key] = val;
      }
    };

    const bannerData = plugin.metaManager.getBannerData(frontmatter);
    const { src, icon } = bannerData;
    const { contentEl, file } = state.field(editorViewField);
    const widgets: Decoration[] = [];

    // Add banner widgets if applicable
    if (src) {
      const settingsFacet = state.facet(bannerDecorFacet);
      widgets.push(
        Decoration.widget({ widget: new BannerWidget(plugin, bannerData, file.path, contentEl, settingsFacet) }),
        Decoration.widget({ widget: new SpacerWidget() }),
        Decoration.line({ class: 'has-banner' })
      );
    }

    // Add icon widget if applicable
    if (icon) {
      const settingsFacet = state.facet(iconDecorFacet);
      widgets.push(
        Decoration.widget({ widget: new IconWidget(plugin, icon, file, settingsFacet) }),
        Decoration.line({ class: "has-banner-icon", attributes: { "data-icon-v": settingsFacet.iconVerticalAlignment }})
      );
    }

    return Decoration.set(widgets.map(w => w.range(0)), true);
  }
}, {
  decorations: v => v.decor
})
Example #2
Source File: index.ts    From codemirror-languageserver with BSD 3-Clause "New" or "Revised" License 4 votes vote down vote up
class LanguageServerPlugin implements PluginValue {
    public client: LanguageServerClient;

    private documentUri: string;
    private languageId: string;
    private documentVersion: number;
    
    private changesTimeout: number;

    constructor(private view: EditorView) {
        this.client = this.view.state.facet(client);
        this.documentUri = this.view.state.facet(documentUri);
        this.languageId = this.view.state.facet(languageId);
        this.documentVersion = 0;
        this.changesTimeout = 0;

        this.client.attachPlugin(this);
        
        this.initialize({
            documentText: this.view.state.doc.toString(),
        });
    }

    update({ docChanged }: ViewUpdate) {
        if (!docChanged) return;
        if (this.changesTimeout) clearTimeout(this.changesTimeout);
        this.changesTimeout = self.setTimeout(() => {
            this.sendChange({
                documentText: this.view.state.doc.toString(),
            });
        }, changesDelay);
    }

    destroy() {
        this.client.detachPlugin(this);
    }

    async initialize({ documentText }: { documentText: string }) {
        this.client.textDocumentDidOpen({
            textDocument: {
                uri: this.documentUri,
                languageId: this.languageId,
                text: documentText,
                version: this.documentVersion,
            }
        });
    }

    async sendChange({ documentText }: { documentText: string }) {
        if (!this.client.ready) return;
        try {
            await this.client.textDocumentDidChange({
                textDocument: {
                    uri: this.documentUri,
                    version: this.documentVersion++,
                },
                contentChanges: [{ text: documentText }],
            });
        } catch (e) {
            console.error(e);
        }
    }

    requestDiagnostics(view: EditorView) {
        this.sendChange({ documentText: view.state.doc.toString() });
    }

    async requestHoverTooltip(
        view: EditorView,
        { line, character }: { line: number; character: number }
    ): Promise<Tooltip | null> {
        if (!this.client.ready || !this.client.capabilities!.hoverProvider) return null;

        this.sendChange({ documentText: view.state.doc.toString() });
        const result = await this.client.textDocumentHover({
            textDocument: { uri: this.documentUri },
            position: { line, character },
        });
        if (!result) return null;
        const { contents, range } = result;
        let pos = posToOffset(view.state.doc, { line, character })!;
        let end: number;
        if (range) {
            pos = posToOffset(view.state.doc, range.start)!;
            end = posToOffset(view.state.doc, range.end);
        }
        if (pos === null) return null;
        const dom = document.createElement('div');
        dom.classList.add('documentation');
        dom.textContent = formatContents(contents);
        return { pos, end, create: (view) => ({ dom }), above: true };
    }

    async requestCompletion(
        context: CompletionContext,
        { line, character }: { line: number; character: number },
        {
            triggerKind,
            triggerCharacter,
        }: {
            triggerKind: CompletionTriggerKind;
            triggerCharacter: string | undefined;
        }
    ): Promise<CompletionResult | null> {
        if (!this.client.ready || !this.client.capabilities!.completionProvider) return null;
        this.sendChange({
            documentText: context.state.doc.toString(),
        });

        const result = await this.client.textDocumentCompletion({
            textDocument: { uri: this.documentUri },
            position: { line, character },
            context: {
                triggerKind,
                triggerCharacter,
            }
        });

        if (!result) return null;

        const items = 'items' in result ? result.items : result;

        let options = items.map(
            ({
                detail,
                label,
                kind,
                textEdit,
                documentation,
                sortText,
                filterText,
            }) => {
                const completion: Completion & {
                    filterText: string;
                    sortText?: string;
                    apply: string;
                } = {
                    label,
                    detail,
                    apply: textEdit?.newText ?? label,
                    type: kind && CompletionItemKindMap[kind].toLowerCase(),
                    sortText: sortText ?? label,
                    filterText: filterText ?? label,
                };
                if (documentation) {
                    completion.info = formatContents(documentation);
                }
                return completion;
            }
        );

        const [span, match] = prefixMatch(options);
        const token = context.matchBefore(match);
        let { pos } = context;

        if (token) {
            pos = token.from;
            const word = token.text.toLowerCase();
            if (/^\w+$/.test(word)) {
                options = options
                    .filter(({ filterText }) =>
                        filterText.toLowerCase().startsWith(word)
                    )
                    .sort(({ apply: a }, { apply: b }) => {
                        switch (true) {
                            case a.startsWith(token.text) &&
                                !b.startsWith(token.text):
                                return -1;
                            case !a.startsWith(token.text) &&
                                b.startsWith(token.text):
                                return 1;
                        }
                        return 0;
                    });
            }
        }
        return {
            from: pos,
            options,
        };
    }

    processNotification(notification: Notification) {
        try {
            switch (notification.method) {
                case 'textDocument/publishDiagnostics':
                    this.processDiagnostics(notification.params);
            }
        } catch (error) {
            console.error(error);
        }
    }

    processDiagnostics(params: PublishDiagnosticsParams) {
        if (params.uri !== this.documentUri) return;

        const diagnostics = params.diagnostics
            .map(({ range, message, severity }) => ({
                from: posToOffset(this.view.state.doc, range.start)!,
                to: posToOffset(this.view.state.doc, range.end)!,
                severity: ({
                    [DiagnosticSeverity.Error]: 'error',
                    [DiagnosticSeverity.Warning]: 'warning',
                    [DiagnosticSeverity.Information]: 'info',
                    [DiagnosticSeverity.Hint]: 'info',
                } as const)[severity!],
                message,
            }))
            .filter(({ from, to }) => from !== null && to !== null && from !== undefined && to !== undefined)
            .sort((a, b) => {
                switch (true) {
                    case a.from < b.from:
                        return -1;
                    case a.from > b.from:
                        return 1;
                }
                return 0;
            });

        this.view.dispatch(setDiagnostics(this.view.state, diagnostics));
    }
}