summaryrefslogtreecommitdiff
path: root/tools/language-server/src/plugins/css/CSSPlugin.ts
blob: 26c90ac66b0becc0b2ccfceeb7d0f178212addb0 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
import type { CompletionsProvider } from '../interfaces';
import type { Document, DocumentManager } from '../../core/documents';
import type { ConfigManager } from '../../core/config';
import { getEmmetCompletionParticipants, doComplete as doEmmetComplete } from 'vscode-emmet-helper';
import { CompletionContext, CompletionList, CompletionTriggerKind, Position } from 'vscode-languageserver';
import { isInsideFrontmatter } from '../../core/documents/utils';
import { CSSDocument, CSSDocumentBase } from './CSSDocument';
import { getLanguage, getLanguageService } from './service';
import { StyleAttributeDocument } from './StyleAttributeDocument';
import { mapCompletionItemToOriginal } from '../../core/documents';
import { AttributeContext, getAttributeContextAtPosition } from '../../core/documents/parseHtml';
import { getIdClassCompletion } from './features/getIdClassCompletion';

export class CSSPlugin implements CompletionsProvider {
  private docManager: DocumentManager;
  private configManager: ConfigManager;
  private documents = new WeakMap<Document, CSSDocument>();
  private triggerCharacters = new Set(['.', ':', '-', '/']);

  constructor(docManager: DocumentManager, configManager: ConfigManager) {
    this.docManager = docManager;
    this.configManager = configManager;

    this.docManager.on('documentChange', (document) => {
      this.documents.set(document, new CSSDocument(document));
    });
  }

  getCompletions(document: Document, position: Position, completionContext?: CompletionContext): CompletionList | null {
    const triggerCharacter = completionContext?.triggerCharacter;
    const triggerKind = completionContext?.triggerKind;
    const isCustomTriggerCharacter = triggerKind === CompletionTriggerKind.TriggerCharacter;

    if (isCustomTriggerCharacter && triggerCharacter && !this.triggerCharacters.has(triggerCharacter)) {
      return null;
    }

    if (this.isInsideFrontmatter(document, position)) {
      return null;
    }

    const cssDocument = this.getCSSDoc(document);

    if (cssDocument.isInGenerated(position)) {
      return this.getCompletionsInternal(document, position, cssDocument);
    }

    const attributeContext = getAttributeContextAtPosition(document, position);
    if (!attributeContext) {
      return null;
    }

    if (this.inStyleAttributeWithoutInterpolation(attributeContext, document.getText())) {
      const [start, end] = attributeContext.valueRange;
      return this.getCompletionsInternal(document, position, new StyleAttributeDocument(document, start, end));
    } else {
      return getIdClassCompletion(cssDocument, attributeContext);
    }
  }

  private getCompletionsInternal(document: Document, position: Position, cssDocument: CSSDocumentBase) {
    if (isSASS(cssDocument)) {
      // the css language service does not support sass, still we can use
      // the emmet helper directly to at least get emmet completions
      return doEmmetComplete(document, position, 'sass', this.configManager.getEmmetConfig());
    }

    const type = extractLanguage(cssDocument);

    const lang = getLanguageService(type);
    const emmetResults: CompletionList = {
      isIncomplete: true,
      items: [],
    };
    if (false /* this.configManager.getConfig().css.completions.emmet */) {
      lang.setCompletionParticipants([
        getEmmetCompletionParticipants(cssDocument, cssDocument.getGeneratedPosition(position), getLanguage(type), this.configManager.getEmmetConfig(), emmetResults),
      ]);
    }
    const results = lang.doComplete(cssDocument, cssDocument.getGeneratedPosition(position), cssDocument.stylesheet);
    return CompletionList.create(
      [...(results ? results.items : []), ...emmetResults.items].map((completionItem) => mapCompletionItemToOriginal(cssDocument, completionItem)),
      // Emmet completions change on every keystroke, so they are never complete
      emmetResults.items.length > 0
    );
  }

  private inStyleAttributeWithoutInterpolation(attrContext: AttributeContext, text: string): attrContext is Required<AttributeContext> {
    return attrContext.name === 'style' && !!attrContext.valueRange && !text.substring(attrContext.valueRange[0], attrContext.valueRange[1]).includes('{');
  }

  private getCSSDoc(document: Document) {
    let cssDoc = this.documents.get(document);
    if (!cssDoc || cssDoc.version < document.version) {
      cssDoc = new CSSDocument(document);
      this.documents.set(document, cssDoc);
    }
    return cssDoc;
  }

  private isInsideFrontmatter(document: Document, position: Position) {
    return isInsideFrontmatter(document.getText(), document.offsetAt(position));
  }
}

function isSASS(document: CSSDocumentBase) {
  switch (extractLanguage(document)) {
    case 'sass':
      return true;
    default:
      return false;
  }
}

function extractLanguage(document: CSSDocumentBase): string {
  const lang = document.languageId;
  return lang.replace(/^text\//, '');
}