• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

streetsidesoftware / cspell / 14853458363

06 May 2025 06:53AM UTC coverage: 93.251%. First build
14853458363

Pull #7295

github

web-flow
Merge 0fbbc5bb7 into 02979efe6
Pull Request #7295: fix: Add support to add words to config and keep comments.

11971 of 13836 branches covered (86.52%)

131 of 182 new or added lines in 5 files covered. (71.98%)

15503 of 16625 relevant lines covered (93.25%)

30989.02 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

97.8
/packages/cspell-config-lib/src/CSpellConfigFile/CSpellConfigFileYaml.ts
1
import assert from 'node:assert';
2

3
import type { CSpellSettings } from '@cspell/cspell-types';
4
import {
5
    type Document as YamlDocument,
6
    isMap,
7
    isScalar,
8
    isSeq,
9
    parseDocument,
10
    Scalar,
11
    stringify,
12
    YAMLMap,
13
    YAMLSeq,
14
} from 'yaml';
15

16
import { CSpellConfigFile } from '../CSpellConfigFile.js';
17
import { detectIndentAsNum } from '../serializers/util.js';
18
import type { TextFile } from '../TextFile.js';
19
import { ParseError } from './Errors.js';
20

21
export class CSpellConfigFileYaml extends CSpellConfigFile {
22
    #settings: CSpellSettings;
23

24
    constructor(
25
        readonly url: URL,
18✔
26
        readonly yamlDoc: YamlDocument,
18✔
27
        readonly indent: number,
18✔
28
    ) {
29
        super(url);
18✔
30
        this.#settings = yamlDoc.toJS();
18✔
31
    }
32

33
    get settings(): CSpellSettings {
34
        return this.#settings;
8✔
35
    }
36

37
    addWords(wordsToAdd: string[]): this {
38
        const cfgWords: YAMLSeq<StringOrScalar> =
39
            (this.yamlDoc.get('words') as YAMLSeq<StringOrScalar>) || new YAMLSeq<StringOrScalar>();
7!
40
        assert(isSeq(cfgWords), 'Expected words to be a YAML sequence');
7✔
41
        const knownWords = new Set(cfgWords.items.map((item) => getScalarValue(item)));
49✔
42
        wordsToAdd.forEach((w) => {
7✔
43
            if (knownWords.has(w)) return;
23✔
44
            cfgWords.add(w);
18✔
45
            knownWords.add(w);
18✔
46
        });
47
        const sorted = sortWords(cfgWords.items);
7✔
48
        sorted.forEach((item, index) => cfgWords.set(index, item));
67✔
49
        cfgWords.items.length = sorted.length;
7✔
50
        this.yamlDoc.set('words', cfgWords);
7✔
51
        this.#settings = this.yamlDoc.toJS();
7✔
52
        return this;
7✔
53
    }
54

55
    serialize() {
56
        return stringify(this.yamlDoc, { indent: this.indent });
11✔
57
    }
58
}
59

60
export function parseCSpellConfigFileYaml(file: TextFile): CSpellConfigFileYaml {
61
    const { url, content } = file;
21✔
62

63
    try {
21✔
64
        const doc = parseDocument<YAMLMap | Scalar<null | string>>(content);
21✔
65
        // Force empty content to be a map.
66
        if (doc.contents === null || (isScalar(doc.contents) && !doc.contents.value)) {
21✔
67
            doc.contents = new YAMLMap();
3✔
68
        }
69
        if (!isMap(doc.contents)) {
21✔
70
            throw new ParseError(url, `Invalid YAML content ${url}`);
3✔
71
        }
72
        const indent = detectIndentAsNum(content);
18✔
73
        return new CSpellConfigFileYaml(url, doc, indent);
18✔
74
    } catch (e) {
75
        if (e instanceof ParseError) {
3!
76
            throw e;
3✔
77
        }
NEW
78
        throw new ParseError(url, undefined, { cause: e });
×
79
    }
80
}
81

82
function getScalarValue<T>(node: T | Scalar<T>): T {
83
    if (isScalar(node)) {
259✔
84
        return node.value;
196✔
85
    }
86
    return node;
63✔
87
}
88

89
function toScalar<T>(node: T | Scalar<T>): Scalar<T> {
90
    if (isScalar(node)) {
78✔
91
        return node;
60✔
92
    }
93
    return new Scalar(node);
18✔
94
}
95

96
type StringOrScalar = string | Scalar<string>;
97

98
function groupWords(words: StringOrScalar[]): StringOrScalar[][] {
99
    const groups: StringOrScalar[][] = [];
7✔
100
    if (words.length === 0) {
7!
NEW
101
        return groups;
×
102
    }
103
    let currentGroup: StringOrScalar[] = [];
7✔
104
    groups.push(currentGroup);
7✔
105
    for (const word of words) {
7✔
106
        if (isSectionHeader(word)) {
67✔
107
            currentGroup = [];
8✔
108
            groups.push(currentGroup);
8✔
109
        }
110
        currentGroup.push(cloneWord(word));
67✔
111
    }
112
    return groups;
7✔
113
}
114

115
function isSectionHeader(word: StringOrScalar): boolean {
116
    if (!isScalar(word) || (!word.commentBefore && !word.spaceBefore)) return false;
67✔
117
    if (word.spaceBefore) return true;
16✔
118
    if (!word.commentBefore) return false;
8!
119
    return word.commentBefore.includes('\n\n');
8✔
120
}
121

122
function adjustSectionHeader(word: Scalar<string>, prev: StringOrScalar, isFirstSection: boolean): void {
123
    // console.log('adjustSectionHeader %o', { word, prev, isFirstSection });
124
    if (!isScalar(prev)) return;
11!
125
    let captureComment = isFirstSection;
11✔
126
    if (prev.spaceBefore) {
11✔
127
        word.spaceBefore = true;
6✔
128
        captureComment = true;
6✔
129
        delete prev.spaceBefore;
6✔
130
    }
131
    if (!prev.commentBefore) return;
11✔
132

133
    const originalComment = prev.commentBefore;
5✔
134
    const lines = originalComment.split(/^\n/gm);
5✔
135
    const lastLine = lines[lines.length - 1];
5✔
136
    // console.log('adjustSectionHeader lines %o', { lines, isFirstSection, lastLine, originalComment });
137
    captureComment = (captureComment && originalComment.trim() === lastLine.trim()) || originalComment.endsWith('\n');
5!
138
    let header = originalComment;
5✔
139
    if (captureComment) {
5✔
140
        delete prev.commentBefore;
3✔
141
    } else {
142
        prev.commentBefore = lastLine;
2✔
143
        lines.pop();
2✔
144
        header = lines.join('\n');
2✔
145
    }
146
    if (word.commentBefore) {
5✔
147
        header += header.endsWith('\n\n') ? '' : '\n';
2!
148
        header += header.endsWith('\n\n') ? '' : '\n';
2✔
149
        header += word.commentBefore;
2✔
150
    }
151
    word.commentBefore = header;
5✔
152
    // console.log('adjustSectionHeader after %o', { word, prev, isFirstSection, originalComment, lastLine, lines });
153
}
154

155
function sortWords(words: StringOrScalar[]): StringOrScalar[] {
156
    const compare = new Intl.Collator().compare;
7✔
157

158
    const groups = groupWords(words);
7✔
159
    let firstGroup = true;
7✔
160
    for (const group of groups) {
7✔
161
        const head = group[0];
15✔
162
        group.sort((a, b) => {
15✔
163
            return compare(getScalarValue(a), getScalarValue(b));
105✔
164
        });
165
        if (group[0] !== head && isScalar(head)) {
15✔
166
            const first = (group[0] = toScalar(group[0]));
11✔
167
            adjustSectionHeader(first, head, firstGroup);
11✔
168
        }
169
        firstGroup = false;
15✔
170
    }
171

172
    const result = groups.flat();
7✔
173
    return result.map((w) => toScalar(w));
67✔
174
}
175

176
function cloneWord(word: StringOrScalar): StringOrScalar {
177
    if (isScalar(word)) {
67✔
178
        return word.clone() as Scalar<string>;
49✔
179
    }
180
    return word;
18✔
181
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc