Stub out more completion providers (#28)
This commit is contained in:
Родитель
e857abf477
Коммит
554ab9b05c
|
@ -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
|
||||
|
|
|
@ -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": {
|
||||
|
|
Загрузка…
Ссылка в новой задаче