Stub out more completion providers (#28)

This commit is contained in:
Brandon Waterloo [MSFT] 2021-09-13 09:42:23 -04:00 коммит произвёл GitHub
Родитель e857abf477
Коммит 554ab9b05c
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
15 изменённых файлов: 197 добавлений и 46 удалений

Просмотреть файл

@ -28,7 +28,7 @@ steps:
- task: CopyFiles@2
displayName: 'Copy Package'
inputs:
Contents: 'compose-language-service*.tgz'
Contents: 'microsoft-compose-language-service*.tgz'
TargetFolder: '$(build.artifactstagingdirectory)'
condition: and(eq(variables['Agent.OS'], 'Linux'), ne(variables['System.PullRequest.IsFork'], 'True'))
@ -36,7 +36,7 @@ steps:
displayName: 'Publish Package'
inputs:
PathtoPublish: '$(build.artifactstagingdirectory)'
ArtifactName: 'compose-language-service'
ArtifactName: 'microsoft-compose-language-service'
condition: and(eq(variables['Agent.OS'], 'Linux'), ne(variables['System.PullRequest.IsFork'], 'True'))
- template: after-all.yml

2
.github/workflows/node.js.yml поставляемый
Просмотреть файл

@ -22,7 +22,7 @@ jobs:
steps:
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
uses: actions/setup-node@v2
with:
node-version: ${{ matrix.node-version }}
- run: npm ci

Просмотреть файл

@ -8,6 +8,15 @@ import { TextDocument } from 'vscode-languageserver-textdocument';
import { CST, Document as YamlDocument, Parser, Composer, isDocument } from 'yaml';
import { Lazy } from './utils/Lazy';
const EmptyDocumentCST: CST.Document = {
type: 'document',
offset: 0,
start: [],
};
// The stated behavior of character number in the `Position` class is to roll back to line length if it exceeds the line length. So, this will work for any line <1m characters. That should cover most of them.
const MaximumLineLength = 1000 * 1000;
export class ComposeDocument {
public readonly fullCst = new Lazy(() => this.buildFullCst());
public readonly documentCst = new Lazy(() => this.buildDocumentCst());
@ -19,14 +28,14 @@ export class ComposeDocument {
public lineAt(line: Position | number): string {
// Flatten to a position at the start of the line
const start = (typeof line === 'number') ? Position.create(line, 0) : Position.create(line.line, 0);
const end = Position.create(start.line, 1000 * 1000); // The stated behavior of character position is to roll back to line length if it exceeds the line length. This will work for any line <1m characters. That should cover most of them.
const startOfLine = (typeof line === 'number') ? Position.create(line, 0) : Position.create(line.line, 0);
const endOfLine = Position.create(startOfLine.line, MaximumLineLength);
if (start.line > this.textDocument.lineCount) {
throw new Error(`Requested line ${start.line} is out of bounds.`);
if (startOfLine.line > this.textDocument.lineCount) {
throw new Error(`Requested line ${startOfLine.line} is out of bounds.`);
}
return this.textDocument.getText(Range.create(start, end));
return this.textDocument.getText(Range.create(startOfLine, endOfLine));
}
public static DocumentManagerConfig: TextDocumentsConfiguration<ComposeDocument> = {
@ -41,14 +50,8 @@ export class ComposeDocument {
private buildDocumentCst(): CST.Document {
// The CST can consist of more than just the document
// Get the first `type === 'document'` item out of the list; this is the actual document
const documentCst: CST.Document | undefined = this.fullCst.value.find(t => t.type === 'document') as CST.Document;
if (!documentCst) {
// TODO: empty documents are a normal thing but will not have a Document token, that should be handled differently than erroring
throw new ResponseError(ErrorCodes.ParseError, 'Malformed YAML document');
}
return documentCst;
// If there isn't one, return `EmptyDocumentCST`
return this.fullCst.value.find(t => t.type === 'document') as CST.Document || EmptyDocumentCST;
}
private buildYamlDocument(): YamlDocument {

Просмотреть файл

@ -32,6 +32,8 @@ export class ComposeLanguageService implements Disposable {
private readonly documentManager: TextDocuments<ComposeDocument> = new TextDocuments(ComposeDocument.DocumentManagerConfig);
private readonly subscriptions: Disposable[] = [];
// TODO: telemetry! Aggregation!
public constructor(public readonly connection: Connection, private readonly clientParams: InitializeParams) {
// Hook up the document listeners, which create a Disposable which will be added to this.subscriptions
this.createDocumentManagerHandler(this.documentManager.onDidChangeContent, new DiagnosticProvider().on);

Просмотреть файл

@ -11,7 +11,7 @@ type ItemType = 'start' | 'key' | 'sep' | 'value';
export class ExtendedPosition {
private constructor(
public readonly item: CST.CollectionItem,
public readonly parent: CST.CollectionItem,
public readonly itemType: ItemType,
public readonly logicalPath: string,
) { }
@ -85,6 +85,7 @@ export class ExtendedPosition {
}
// TODO Potentially a faster but less accurate way to get the path would be to walk backwards up the document, watching the indentation
// TODO make sure it's actually faster
private static loadLogicalPath(cst: CST.Document, item: CST.CollectionItem, path: CST.VisitPath, itemType: ItemType): string {
const resultParts: string[] = [];

Просмотреть файл

@ -10,13 +10,17 @@ import { debounce } from '../utils/debounce';
import { yamlRangeToLspRange } from '../utils/yamlRangeToLspRange';
import { ProviderBase } from './ProviderBase';
// The time between when typing stops and when diagnostics will be sent (milliseconds)
const DiagnosticDelay = 1000;
export class DiagnosticProvider extends ProviderBase<TextDocumentChangeEvent<ComposeDocument> & ExtendedParams, void, never, never> {
public on(params: TextDocumentChangeEvent<ComposeDocument> & ExtendedParams): void {
if (!params.clientCapabilities.textDocument?.publishDiagnostics) {
return;
}
debounce(500, { uri: params.document.textDocument.uri, callId: 'diagnostics' }, () => {
debounce(DiagnosticDelay, { uri: params.document.textDocument.uri, callId: 'diagnostics' }, () => {
const diagnostics: Diagnostic[] = [];
for (const error of [...params.document.yamlDocument.value.errors, ...params.document.yamlDocument.value.warnings]) {

Просмотреть файл

@ -21,7 +21,7 @@ export class KeyHoverProvider extends ProviderBase<HoverParams & ExtendedParams,
const extendedPosition = ExtendedPosition.build(params.document, params.position);
if (extendedPosition.itemType === 'key' && CST.isScalar(extendedPosition.item.key)) {
if (extendedPosition.itemType === 'key' && CST.isScalar(extendedPosition.parent.key)) {
const keyInfo = ComposeKeyInfo.find((k) => k.pathRegex.test(extendedPosition.logicalPath));
if (keyInfo) {
@ -30,7 +30,7 @@ export class KeyHoverProvider extends ProviderBase<HoverParams & ExtendedParams,
kind: preferMarkdown ? MarkupKind.Markdown : MarkupKind.PlainText, // If Markdown is preferred, even plaintext will be treated as Markdown--it renders better, has line wrapping, etc.
value: (preferMarkdown && keyInfo.markdownContents) || keyInfo.plaintextContents,
},
range: yamlRangeToLspRange(params.document.textDocument, [extendedPosition.item.key.offset, extendedPosition.item.key.offset + extendedPosition.item.key.source.length]),
range: yamlRangeToLspRange(params.document.textDocument, [extendedPosition.parent.key.offset, extendedPosition.parent.key.offset + extendedPosition.parent.key.source.length]),
};
}
}

Просмотреть файл

@ -0,0 +1,46 @@
/*!--------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See LICENSE in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { CompletionItem, CompletionParams, InsertTextFormat, TextEdit } from 'vscode-languageserver';
import { ExtendedParams } from '../../ExtendedParams';
interface ExtendedCompletionItem extends CompletionItem {
/**
* The matching expression
*/
matcher?: RegExp;
/**
* The insertion text does not need to be the same as the label
*/
insertionText: string;
}
export class CompletionCollection extends Array<ExtendedCompletionItem> {
public getActiveCompletionItems(params: CompletionParams & ExtendedParams): CompletionItem[] {
const results: CompletionItem[] = [];
for (const m of this) {
const match = m.matcher?.exec(params.document.lineAt(params.position));
if (match || !m.matcher) {
const ci = CompletionItem.create(m.label);
ci.insertTextFormat = m.insertTextFormat ?? InsertTextFormat.Snippet;
ci.textEdit = TextEdit.insert(params.position, m.insertionText);
// Copy additional properties
// TODO: this doesn't copy everything; what else should be added?
ci.detail = m.detail;
ci.documentation = m.documentation;
ci.commitCharacters = m.commitCharacters;
ci.filterText = m.filterText;
results.push(ci);
}
}
return results;
}
}

Просмотреть файл

@ -0,0 +1,22 @@
/*!--------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See LICENSE in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { CancellationToken, CompletionItem, CompletionParams } from 'vscode-languageserver';
import { ExtendedPositionParams } from '../../ExtendedParams';
import { SubproviderBase } from '../MultiProviderBase';
import { CompletionCollection } from './CompletionCollection';
const PortsCompletions = new CompletionCollection(...[
]);
export class PortsCompletionProvider implements SubproviderBase<CompletionParams & ExtendedPositionParams, CompletionItem[] | undefined, never> {
public on(params: CompletionParams & ExtendedPositionParams, token: CancellationToken): CompletionItem[] | undefined {
if (!/^\/services\/\w+\/ports\/.+$/i.test(params.extendedPosition.value.logicalPath)) {
return undefined;
}
return PortsCompletions.getActiveCompletionItems(params);
}
}

Просмотреть файл

@ -0,0 +1,22 @@
/*!--------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See LICENSE in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { CancellationToken, CompletionItem, CompletionParams } from 'vscode-languageserver';
import { ExtendedPositionParams } from '../../ExtendedParams';
import { SubproviderBase } from '../MultiProviderBase';
import { CompletionCollection } from './CompletionCollection';
const RootCompletions = new CompletionCollection(...[
]);
export class RootCompletionProvider implements SubproviderBase<CompletionParams & ExtendedPositionParams, CompletionItem[] | undefined, never> {
public on(params: CompletionParams & ExtendedPositionParams, token: CancellationToken): CompletionItem[] | undefined {
if (!/^\/[^/]*$/i.test(params.extendedPosition.value.logicalPath)) {
return undefined;
}
return RootCompletions.getActiveCompletionItems(params);
}
}

Просмотреть файл

@ -0,0 +1,59 @@
/*!--------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See LICENSE in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { CancellationToken, CompletionItem, CompletionParams, InsertTextMode } from 'vscode-languageserver';
import { ExtendedPositionParams } from '../../ExtendedParams';
import { SubproviderBase } from '../MultiProviderBase';
import { CompletionCollection } from './CompletionCollection';
const ServiceCompletions = new CompletionCollection(...[
{
label: 'build:',
insertionText: 'build: ${1:path}$0',
detail: 'Short form',
documentation: 'build: <path>',
filterText: 'build',
},
{
label: 'build:',
insertionText: 'build:\n context: ${1:contextPath}\n dockerfile: ${2:Dockerfile}',
insertTextMode: InsertTextMode.adjustIndentation,
detail: 'Long form',
documentation: 'build:\n context: <contextPath>\n dockerfile: <Dockerfile>',
},
{
label: 'image:',
insertionText: 'image: ${1:imageName}$0',
},
]);
/**
* The position given when the cursor is inbetween the service key and first properties, i.e. at the | below:
services:
foo:
|
a: b
*/
const PositionAfterServiceNamePathRegex = /^\/services\/<sep>$/i; // e.g. /services/<sep>
/**
* The position given when the cursor is inbetween properties in a service, i.e. at the | below:
services:
foo:
a: b
|
*/
const PositionInServiceConfigPathRegex = /^\/services\/[\w -]+\/<start>$/i; // e.g. /services/foo/<start>
export class ServiceCompletionProvider implements SubproviderBase<CompletionParams & ExtendedPositionParams, CompletionItem[] | undefined, never> {
public on(params: CompletionParams & ExtendedPositionParams, token: CancellationToken): CompletionItem[] | undefined {
if (!PositionAfterServiceNamePathRegex.test(params.extendedPosition.value.logicalPath) &&
!PositionInServiceConfigPathRegex.test(params.extendedPosition.value.logicalPath)) {
return undefined;
}
return ServiceCompletions.getActiveCompletionItems(params);
}
}

Просмотреть файл

@ -3,43 +3,43 @@
* Licensed under the MIT License. See LICENSE in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { CancellationToken, CompletionItem, CompletionParams, InsertTextFormat, TextEdit } from 'vscode-languageserver';
import { CancellationToken, CompletionItem, CompletionParams } from 'vscode-languageserver';
import { ExtendedPositionParams } from '../../ExtendedParams';
import { SubproviderBase } from '../MultiProviderBase';
import { CompletionCollection } from './CompletionCollection';
interface CompletionMatcher {
matcher: RegExp;
label: string;
insertionText: string;
}
const VolumeMatchers: CompletionMatcher[] = [
const VolumesCompletions = new CompletionCollection(...[
{
// Matches ` - ""` or ` -`, with allowances for other amounts of whitespace
matcher: /(\s*-\s*)(?<leadingQuote>")?\2\s*$/i,
label: 'hostPath:containerPath:mode',
insertionText: '${1:hostPath}:${2:containerPath}:${3|ro,rw|}$0',
},
{
// Matches ` - ""` or ` -`, with allowances for other amounts of whitespace
matcher: /(\s*-\s*)(?<leadingQuote>")?\2\s*$/i,
label: 'volumeName:containerPath:mode',
insertionText: '${1:volumeName}:${2:containerPath}:${3|ro,rw|}$0',
},
{
// Matches ` - "C:\some\path:"` or ` - /some/path:`, with allowances for other amounts of whitespace/quoting
matcher: /(\s*-\s*)(?<leadingQuote>")?(([a-z]:\\)?[^:"]+):\2\s*$/i,
label: ':containerPath:mode',
insertionText: '${2:containerPath}:${3|ro,rw|}$0',
},
{
// Matches ` - "C:\some\path:/another/path:"` or ` - /some/path:/another/path:`, with allowances for other amounts of whitespace/quoting
matcher: /(\s*-\s*)(?<leadingQuote>")?(([a-z]:\\)?[^:"]+):(([a-z]:\\)?[^:"]+):\2\s*$/i,
label: ':ro',
insertionText: 'ro',
},
{
// Matches ` - "C:\some\path:/another/path:"` or ` - /some/path:/another/path:`, with allowances for other amounts of whitespace/quoting
matcher: /(\s*-\s*)(?<leadingQuote>")?(([a-z]:\\)?[^:"]+):(([a-z]:\\)?[^:"]+):\2\s*$/i,
label: ':rw',
insertionText: 'rw',
},
];
]);
export class VolumesCompletionProvider implements SubproviderBase<CompletionParams & ExtendedPositionParams, CompletionItem[] | undefined, never> {
public on(params: CompletionParams & ExtendedPositionParams, token: CancellationToken): CompletionItem[] | undefined {
@ -47,19 +47,6 @@ export class VolumesCompletionProvider implements SubproviderBase<CompletionPara
return undefined;
}
const results: CompletionItem[] = [];
for (const m of VolumeMatchers) {
const match = m.matcher.exec(params.document.lineAt(params.position));
if (match) {
const ci = CompletionItem.create(m.label);
ci.insertTextFormat = InsertTextFormat.Snippet;
ci.textEdit = TextEdit.insert(params.position, m.insertionText);
results.push(ci);
}
}
return results;
return VolumesCompletions.getActiveCompletionItems(params);
}
}

Просмотреть файл

@ -16,7 +16,7 @@ interface ExtendedSignatureInformation extends SignatureInformation {
matcher: RegExp;
}
export class SignatureCollection extends Array<ExtendedSignatureInformation>{
export class SignatureCollection extends Array<ExtendedSignatureInformation> {
public getActiveSignature(params: SignatureHelpParams & ExtendedParams): { activeSignature: number | null, activeParameter: number | null } {
let activeSignature: number | null = null;
let activeParameter: number | null = null;

Просмотреть файл

@ -10,10 +10,14 @@ export class Lazy<T> {
}
public get value(): T {
if (!this.#value) {
if (this.#value === undefined) {
this.#value = this.valueFactory();
}
return this.#value;
}
public hasValue(): boolean {
return (this.#value !== undefined);
}
}

Просмотреть файл

@ -6,7 +6,8 @@
"vscode": "^1.52.0"
},
"activationEvents": [
"*"
"onLanguage:dockercompose",
"onStartupFinished"
],
"main": "./extension.js",
"dependencies": {