В этом репозитории находятся решение тестового задания «Найдите ошибки» для 16-й Школы разработки интерфейсов (зима 2020, Москва).
- VS Code
- VS Code debugger
- console.log
- Документация https://code.visualstudio.com/api/language-extensions/language-server-extension-guide
В данном коде ошибка, textDocumentSync не может быть always
:
conn.onInitialize((params: InitializeParams) => {
return {
capabilities: {
textDocumentSync: 'always'
}
};
});
Смотрим возможные параметры и меняем на наиболее подходящее по смыслу:
conn.onInitialize((params: InitializeParams) => {
return {
capabilities: {
textDocumentSync: TextDocumentSyncKind.Full
}
};
});
В данном коде ошибка, поле loc не существует у property.key
const validateProperty = (
property: jsonToAst.AstProperty
): LinterProblem<RuleKeys>[] =>
/^[A-Z]+$/.test(property.key.value)
? [
{
key: RuleKeys.UppercaseNamesIsForbidden,
loc: property.key.loc
}
]
: [];
Идем в json-to-ast.d.ts, находим AstIdentifier и добавляем loc:
export interface AstIdentifier {
type: 'Identifier';
value: string;
raw: string;
loc: AstLocation;
}
Запускаем дебаг расширения - ничего не работает при открытии json файла. Идем в настройки, включаем линтер - все еще не работает.
В принципе, это не ошибка, но для удобства дебага влючаем линтер сразу после запуска дебага расширения.
Идем в package.json, находим данную настройку:
"example.enable": {
"type": "boolean",
"default": false,
"description": "Enable/disable example linter."
}
Меняем default на true:
"example.enable": {
"type": "boolean",
"default": true,
"description": "Enable/disable example linter."
}
Так как ничего не работает, то смотрим в Output для Language Server Example.
Находим там кучу стек трейсов вида:
For help, see: https://nodejs.org/en/docs/inspector
(node:19212) UnhandledPromiseRejectionWarning: SyntaxError: Unexpected symbol <f> at 1:1
1 | file://shri-2020-task-1/stub/pages/product.json
^
at Object.<anonymous> (\shri-2020-task-3\node_modules\json-to-ast\build.js:5:2)
at Module._compile (internal/modules/cjs/loader.js:786:30)
at Object..js (internal/modules/cjs/loader.js:798:10)
at Module.load (internal/modules/cjs/loader.js:645:32)
at Function._load (internal/modules/cjs/loader.js:560:12)
at Module.require (internal/modules/cjs/loader.js:685:19)
at require (internal/modules/cjs/helpers.js:16:16)
at Object.<anonymous> (\shri-2020-task-3\out\linter.js:3:19)
(node:19212) UnhandledPromiseRejectionWarning: SyntaxError: Unexpected symbol <f> at 1:1
Открываем файл linter.js
на 3 строке, видим, что это вызов функции makeLint
. Смотрим, что передается в коде в эту функцию, находим в файле server.ts
следующее:
const source = basename(textDocument.uri);
const json = textDocument.uri;
Передается путь до файла, вместо его содержимого, правим на следующее:
const source = basename(textDocument.uri);
const json = textDocument.getText();
Перезапускаем дебаг расширения, ошибки в консоли пропали, но все еще не отображаются во вкладке Problems
. Продолжаем искать проблему. После долгих поисков, находим следующий подозрительный кусок кода:
const errors: LinterProblem<TProblemKey>[] = [];
const ast: JsonAST = parseJson(json);
if (ast) {
walk(ast,
(property: jsonToAst.AstProperty) => errors.concat(...validateProperty(property)),
(obj: jsonToAst.AstObject) => errors.concat(...validateObject(obj)));
}
Здесь используется contat
, который возвращает новый массив, нам же надо добавлять в текущий массив, поэтому меняем на push
.
const errors: LinterProblem<TProblemKey>[] = [];
const ast: JsonAST = parseJson(json);
if (ast) {
walk(ast,
(property: jsonToAst.AstProperty) => errors.push(...validateProperty(property)),
(obj: jsonToAst.AstObject) => errors.push(...validateObject(obj)));
}
Перезапускаем дебаг, линтер начинает успешно показывать ошибки (правда не всегда верно, к примеру есть много ошибок, что нужно поле block
в объектах mods
и elemMods
).
Идем в настройки, пытаемся менять для каждого правила Severity
.
Замечаем, что при установке уровня Error
ставится уровень Information
. Находим в коде место с ошибкой:
function GetSeverity(key: RuleKeys): DiagnosticSeverity | undefined {
if (!conf || !conf.severity) {
return undefined;
}
const severity: Severity = conf.severity[key];
switch (severity) {
case Severity.Error:
return DiagnosticSeverity.Information;
Правим кусок со switch
на
switch (severity) {
case Severity.Error:
return DiagnosticSeverity.Error;
Замечаем, что если поправить ошибку, то сообщение о ней не исчезает. Пролема в этом куске кода, который посылает новый массив ошибок, только если он не пустой.
if (diagnostics.length) {
conn.sendDiagnostics({ uri: textDocument.uri, diagnostics });
}
Меняем данный код на
conn.sendDiagnostics({ uri: textDocument.uri, diagnostics });
Так как будем подключать свой линтер, то не правим текущую реализацию правил. Считаем, что все ошибки, мешающие работе линтера, исправлены.
Открываем превью, вместо него наблюдаем {{content}}
. Ищем по слову content
в коде, обнаруживаем файл preview\index.html
и код, который по всей видимости должен подключать наш html:
panel.webview.html = previewHtml
.replace(/{{\s+(\w+)\s+}}/g, (str, key) => {
switch (key) {
case 'content':
return html;
case 'mediaPath':
return getMediaPath(context);
default:
return str;
}
});
Решаем, что хочется, чтобы код одинаково обрабатывал случаи {{content}}
и {{ content }}
, поэтому меняем regexp на:
panel.webview.html = previewHtml
.replace(/{{\s*(\w+)\s*}}/g, (str, key) => {
Перезапускаем дебаг, {{content}}
пропадает, остается пустое поле. Пытаемся посмотреть, через VS Code Developer Tools
что внутри, но это не дает дополнительной информации.
Возникает предположение, что проблема в стилях. Открываем файл preview\style.css
. Видим следующее:
.div {
...
}
.div::before {
...
}
Вместо селекторов по тегам, используется селектор по именам классов, правим код на следующий:
div {
...
}
div::before {
...
}
Перезапускаем дебаг расширения - все равно ничего не появляется. Появляется предположение, что проблема в подключении стилей.
Вставляем стили напрямую в index.html
в тег style
.
Перезапускаем дебаг расширения, пытаемся посмотреть превью, и обнаруживаем, что все заработало.
Копаем дальше, в чем проблема.
Обнаруживаем в Output > Log (Extension Host)
следующую ошибку:
[2020-01-12 19:33:09.479] [exthost] [warning] undefined_publisher.shri-ext created a webview without a content security policy: https://aka.ms/vscode-webview-missing-csp
Идем по ссылке, и обнаруживаем, что для подключения своих стилей \ скриптов надо вставить в html следующую строку.
<meta
http-equiv="Content-Security-Policy"
content="default-src 'none'; script-src ${webview.cspSource}; style-src ${webview.cspSource};"
/>
Подключаем ее, но это не помогает (но ошибка из консоли исчезает).
Продолжаем попытки поключить наши стили. Находим официальный пример расширения с webview https://github.com/microsoft/vscode-extension-samples/blob/master/webview-sample/src/extension.ts#L164 . Понимаем, что для пути нужно использовать функцию webview.asWebviewUri
.
Меняем функцию для отрисовки webview
:
const mediaPath = panel.webview.asWebviewUri(getMediaPath(context)).toString();
panel.webview.html = previewHtml
.replace(/{{\s*(\w+)\s*}}/g, (str, key) => {
switch (key) {
case 'cspSource':
return panel.webview.cspSource;
case 'content':
return html;
case 'mediaPath':
return mediaPath;
default:
return str;
}
});
и меняем фунцию getMediaPath
на:
const getMediaPath = (context: vscode.ExtensionContext) => vscode.Uri
.file(context.asAbsolutePath("/"));
Перезапускаем дебаг расширения, в результате все работает.
Добавляем на будущее возможно подключения script.js
в index.html
:
<body>
{{content}}
<script src="./preview/script.js"></script>
</body>
Замечаем, что команда Example: Show preview из палитры команд запускается на любых файлах (не только json). Меняем настройки, чтобы ограничиться json:
"commands": [
{
"command": "example.showPreviewToSide",
"title": "Show preview",
"category": "Example",
"enablement": "editorLangId == json",
"icon": {
"light": "./media/PreviewIcon16x.svg",
"dark": "./media/PreviewIcon16x_dark.svg"
}
}
],
Для превью просто заменяем файлы в папке preview.
Для линтера были скопированы файлы из 2 задания. В идеале нужно опубликовать линтер как отдельный пакет и использовать уже пакет.
Для линтера была добавлена поддержка настроек по каждому правилу. Так как добавление вручную настроек в package.json
чревато случайными ошибками, то была создана дополнительная утилита для автоматической генерации секции contributes.configuration
файла package.json
.
Для запуска утилиты используется команда npm run update-package-json-config
.
Файлы jsonMain.ts
и hash.ts
не выполняют какие-то полезные нужные действия. Поэтому лучше эти файлы удалить.
Не всегда нужно писать свои .d.ts файлы для js библиотек. Для многих уже есть готовые типы в пакетах вида @types/*
. К примеру для библиотеки json-to-ast
есть пакет с типами @types/json-to-ast
.
В коде используются разные виды импорта:
import * as vscode from 'vscode';
import {
LanguageClient,
LanguageClientOptions,
ServerOptions,
TransportKind,
SettingMonitor,
DocumentColorRequest
} from 'vscode-languageclient';
Лучше придерживаться одного стиля (если это конечно не реакт, там импорт React для jsx оправдан).
Есть как функции с именами в стиле PascalCase, так и в стиле camelCase. Лучше придерживаться единого стиля.
Есть как обычные функции (к примеру activate), так и стрелочные функции (openPreview). Лучше придерживаться единого стиля.