Commit 7789a6b9 authored by Johannes Kapfhammer's avatar Johannes Kapfhammer

implement stoml language support

parent c75d7fd6
Pipeline #4885 failed with stages
in 32 seconds
This diff is collapsed.
......@@ -168,27 +168,30 @@
},
"languages": [
{
"id": "soitask",
"id": "stoml",
"aliases": [
"soitask"
"soitask",
"stoml",
"STOML"
],
"extensions": [
".soitask"
".soitask",
".stoml"
],
"mimetypes": [
"text/x-soitask"
"text/x-stoml"
],
"configuration": ".stoml-language/language-configuration.json"
"configuration": "./stoml-language/language-configuration.json"
}
]
],
"grammars": [
{
"language": "stoml",
"scopeName": "source.stoml",
"path": "./stoml-language/STOML.tmLanguage"
}
]
},
"grammars": [
{
"language": "soitask",
"scopeName": "source.stoml",
"path": "./stoml-language/STOML.tmLanguage"
}
],
"extensionDependencies-deactivated": [
"ms-vscode.cpptools"
],
......@@ -216,6 +219,13 @@
"vscode-test": "^1.2.0"
},
"dependencies": {
"fs-extra": "^7.0.0"
"fs-extra": "^7.0.0",
"@sgarciac/bombadil": "0.0.7",
"node-fetch": "^2.1.2",
"request-light": "^0.2.2",
"vscode-json-languageservice": "^3.1.0",
"vscode-languageclient": "^4.1.3",
"vscode-languageserver": "^4.1.2",
"vscode-nls": "^3.2.2"
}
}
{
"properties": {
"foo": { "type": "string" },
"bar": { "type": "number", "maximum": 3 }
}
}
\ No newline at end of file
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { MarkedString, CompletionItemKind, CompletionItem } from 'vscode-languageserver';
import Strings = require('../utils/strings');
import { JSONWorkerContribution, JSONPath, CompletionsCollector } from 'vscode-json-languageservice';
import * as nls from 'vscode-nls';
const localize = nls.loadMessageBundle();
let globProperties: CompletionItem[] = [
{ kind: CompletionItemKind.Value, label: localize('assocLabelFile', "Files with Extension"), insertText: '"*.{{extension}}": "{{language}}"', documentation: localize('assocDescriptionFile', "Map all files matching the glob pattern in their filename to the language with the given identifier.") },
{ kind: CompletionItemKind.Value, label: localize('assocLabelPath', "Files with Path"), insertText: '"/{{path to file}}/*.{{extension}}": "{{language}}"', documentation: localize('assocDescriptionPath', "Map all files matching the absolute path glob pattern in their path to the language with the given identifier.") }
];
export class FileAssociationContribution implements JSONWorkerContribution {
private languageIds: string[] = [];
constructor() {
}
public setLanguageIds(ids: string[]): void {
this.languageIds = ids;
}
private isSettingsFile(resource: string): boolean {
return Strings.endsWith(resource, '/settings.json');
}
public async collectDefaultCompletions(resource: string, result: CompletionsCollector): Promise<any> {
return null;
}
public async collectPropertyCompletions(resource: string, location: JSONPath, currentWord: string, addValue: boolean, isLast: boolean, result: CompletionsCollector): Promise<any> {
if (this.isSettingsFile(resource) && location.length === 1 && location[0] === 'files.associations') {
globProperties.forEach(e => {
e.filterText = e.insertText;
result.add(e);
});
}
return null;
}
public async collectValueCompletions(resource: string, location: JSONPath, currentKey: string, result: CompletionsCollector): Promise<any> {
if (this.isSettingsFile(resource) && location.length === 1 && location[0] === 'files.associations') {
this.languageIds.forEach(l => {
result.add({
kind: CompletionItemKind.Value,
label: l,
insertText: JSON.stringify('{{' + l + '}}'),
filterText: JSON.stringify(l)
});
});
}
return null;
}
public async getInfoContribution(resource: string, location: JSONPath): Promise<MarkedString[]> {
return [];
}
}
\ No newline at end of file
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { MarkedString, CompletionItemKind, CompletionItem } from 'vscode-languageserver';
import Strings = require('../utils/strings');
import { JSONWorkerContribution, JSONPath, CompletionsCollector } from 'vscode-json-languageservice';
import * as nls from 'vscode-nls';
const localize = nls.loadMessageBundle();
let globProperties: CompletionItem[] = [
{ kind: CompletionItemKind.Value, label: localize('fileLabel', "Files by Extension"), insertText: '"**/*.{{extension}}": true', documentation: localize('fileDescription', "Match all files of a specific file extension.") },
{ kind: CompletionItemKind.Value, label: localize('filesLabel', "Files with Multiple Extensions"), insertText: '"**/*.{ext1,ext2,ext3}": true', documentation: localize('filesDescription', "Match all files with any of the file extensions.") },
{ kind: CompletionItemKind.Value, label: localize('derivedLabel', "Files with Siblings by Name"), insertText: '"**/*.{{source-extension}}": { "when": "$(basename).{{target-extension}}" }', documentation: localize('derivedDescription', "Match files that have siblings with the same name but a different extension.") },
{ kind: CompletionItemKind.Value, label: localize('topFolderLabel', "Folder by Name (Top Level)"), insertText: '"{{name}}": true', documentation: localize('topFolderDescription', "Match a top level folder with a specific name.") },
{ kind: CompletionItemKind.Value, label: localize('topFoldersLabel', "Folders with Multiple Names (Top Level)"), insertText: '"{folder1,folder2,folder3}": true', documentation: localize('topFoldersDescription', "Match multiple top level folders.") },
{ kind: CompletionItemKind.Value, label: localize('folderLabel', "Folder by Name (Any Location)"), insertText: '"**/{{name}}": true', documentation: localize('folderDescription', "Match a folder with a specific name in any location.") },
];
let globValues: CompletionItem[] = [
{ kind: CompletionItemKind.Value, label: localize('trueLabel', "true"), insertText: 'true', documentation: localize('trueDescription', "Enable the pattern.") },
{ kind: CompletionItemKind.Value, label: localize('falseLabel', "false"), insertText: 'false', documentation: localize('falseDescription', "Disable the pattern.") },
{ kind: CompletionItemKind.Value, label: localize('derivedLabel', "Files with Siblings by Name"), insertText: '{ "when": "$(basename).{{extension}}" }', documentation: localize('siblingsDescription', "Match files that have siblings with the same name but a different extension.") }
];
export class GlobPatternContribution implements JSONWorkerContribution {
constructor() {
}
private isSettingsFile(resource: string): boolean {
return Strings.endsWith(resource, '/settings.json');
}
public async collectDefaultCompletions(resource: string, result: CompletionsCollector): Promise<any> {
return null;
}
public async collectPropertyCompletions(resource: string, location: JSONPath, currentWord: string, addValue: boolean, isLast: boolean, result: CompletionsCollector): Promise<any> {
if (this.isSettingsFile(resource) && location.length === 1 && ((location[0] === 'files.exclude') || (location[0] === 'search.exclude'))) {
globProperties.forEach(e => {
e.filterText = e.insertText;
result.add(e);
});
}
return null;
}
public async collectValueCompletions(resource: string, location: JSONPath, currentKey: string, result: CompletionsCollector): Promise<any> {
if (this.isSettingsFile(resource) && location.length === 1 && ((location[0] === 'files.exclude') || (location[0] === 'search.exclude'))) {
globValues.forEach(e => {
e.filterText = e.insertText;
result.add(e);
});
}
return null;
}
public async getInfoContribution(resource: string, location: JSONPath): Promise<MarkedString[]> {
return [];
}
}
\ No newline at end of file
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { TextDocument } from 'vscode-languageserver';
export interface LanguageModelCache<T> {
get(document: TextDocument): T;
onDocumentRemoved(document: TextDocument): void;
dispose(): void;
}
export function getLanguageModelCache<T>(maxEntries: number, cleanupIntervalTimeInSec: number, parse: (document: TextDocument) => T): LanguageModelCache<T> {
let languageModels: { [uri: string]: { version: number, languageId: string, cTime: number, languageModel: T } } = {};
let nModels = 0;
let cleanupInterval: NodeJS.Timeout | undefined = void 0;
if (cleanupIntervalTimeInSec > 0) {
cleanupInterval = setInterval(() => {
let cutoffTime = Date.now() - cleanupIntervalTimeInSec * 1000;
let uris = Object.keys(languageModels);
for (let uri of uris) {
let languageModelInfo = languageModels[uri];
if (languageModelInfo.cTime < cutoffTime) {
delete languageModels[uri];
nModels--;
}
}
}, cleanupIntervalTimeInSec * 1000);
}
return {
get(document: TextDocument): T {
let version = document.version;
let languageId = document.languageId;
let languageModelInfo = languageModels[document.uri];
if (languageModelInfo && languageModelInfo.version === version && languageModelInfo.languageId === languageId) {
languageModelInfo.cTime = Date.now();
return languageModelInfo.languageModel;
}
let languageModel = parse(document);
languageModels[document.uri] = { languageModel, version, languageId, cTime: Date.now() };
if (!languageModelInfo) {
nModels++;
}
if (nModels === maxEntries) {
let oldestTime = Number.MAX_VALUE;
let oldestUri = null;
for (let uri in languageModels) {
let languageModelInfo = languageModels[uri];
if (languageModelInfo.cTime < oldestTime) {
oldestUri = uri;
oldestTime = languageModelInfo.cTime;
}
}
if (oldestUri) {
delete languageModels[oldestUri];
nModels--;
}
}
return languageModel;
},
onDocumentRemoved(document: TextDocument) {
let uri = document.uri;
if (languageModels[uri]) {
delete languageModels[uri];
nModels--;
}
},
dispose() {
if (typeof cleanupInterval !== 'undefined') {
clearInterval(cleanupInterval);
cleanupInterval = void 0;
languageModels = {};
nModels = 0;
}
}
};
}
\ No newline at end of file
This diff is collapsed.
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
export function startsWith(haystack: string, needle: string): boolean {
if (haystack.length < needle.length) {
return false;
}
for (let i = 0; i < needle.length; i++) {
if (haystack[i] !== needle[i]) {
return false;
}
}
return true;
}
/**
* Determines if haystack ends with needle.
*/
export function endsWith(haystack: string, needle: string): boolean {
let diff = haystack.length - needle.length;
if (diff > 0) {
return haystack.lastIndexOf(needle) === diff;
} else if (diff === 0) {
return haystack === needle;
} else {
return false;
}
}
export function convertSimple2RegExpPattern(pattern: string): string {
return pattern.replace(/[\-\\\{\}\+\?\|\^\$\.\,\[\]\(\)\#\s]/g, '\\$&').replace(/[\*]/g, '.*');
}
This diff is collapsed.
'use strict';
// The module 'vscode' contains the VS Code extensibility API
// Import the module and reference it with the alias vscode in your code below
import * as path from 'path';
import * as vscode from 'vscode';
import { LanguageClient, LanguageClientOptions, ServerOptions, TransportKind } from 'vscode-languageclient';
// this method is called when your extension is activated
// your extension is activated the very first time the command is executed
export function activate(context: vscode.ExtensionContext) {
// The server is implemented in node
let serverModule = context.asAbsolutePath(path.join('out', 'better-stoml', 'server', 'tomlServerMain.js'));
// The debug options for the server
let debugOptions = { execArgv: ["--nolazy", "--inspect=6004"] };
// If the extension is launched in debug mode then the debug server options are used
// Otherwise the run options are used
let serverOptions: ServerOptions = {
run : { module: serverModule, transport: TransportKind.ipc },
debug: { module: serverModule, transport: TransportKind.ipc, options: debugOptions }
};
// Options to control the language client
let clientOptions: LanguageClientOptions = {
// Register the server for plain text documents
documentSelector: ['stoml'],
synchronize: {
// Synchronize the setting section 'toml' and 'http' to the server
configurationSection: ['stoml.schemas', 'http.proxy', 'http.proxyStrictSSL'],
// Notify the server about file changes to '.toml files contain in the workspace
fileEvents: vscode.workspace.createFileSystemWatcher('**/*.soitask')
}
};
// Create the language client and start the client.
let disposable = new LanguageClient('STOML Language Server', serverOptions, clientOptions).start();
// Push the disposable to the context's subscriptions so that the
// client can be deactivated on extension deactivation
context.subscriptions.push(disposable);
}
// this method is called when your extension is deactivated
export function deactivate() {
}
\ No newline at end of file
declare module "ct" {
export interface IRecognizerContext {
/**
* A copy of the parser's rule stack at the "time" the RecognitionException occurred.
* This can be used to help debug parsing errors (How did we get here?).
*/
ruleStack: string[]
/**
* A copy of the parser's rule occurrence stack at the "time" the RecognitionException occurred.
* This can be used to help debug parsing errors (How did we get here?).
*/
ruleOccurrenceStack: number[]
}
export interface IRecognitionException {
name: string
message: string
/**
* The token which caused the parser error.
*/
token: Token
/**
* Additional tokens which have been re-synced in error recovery due to the original error.
* This information can be used the calculate the whole text area which has been skipped due to an error.
* For example for displaying with a red underline in a text editor.
*/
resyncedTokens: Token[]
context: IRecognizerContext
}
export interface ILexingError {
offset: number
line: number
column: number
length: number
message: string
}
export interface Token {
}
}
\ No newline at end of file
......@@ -11,6 +11,8 @@ import { Runner } from "./runner";
import { Store } from "./store";
import { downloadHelperBinary } from "./helperBinary";
import { activate as activate_stoml } from './better-stoml/src/extension';
export async function activate(context: vscode.ExtensionContext) {
let taskTreeViewProvider = new TaskTreeViewProvider(context);
let samplesTreeViewProvider = new SamplesTreeViewProvider(context, []);
......@@ -132,6 +134,8 @@ export async function activate(context: vscode.ExtensionContext) {
runner.startServer(16314);
await downloadHelperBinary(store);
activate_stoml(context);
console.log("SOICode has been loaded");
}
......
......@@ -8,7 +8,7 @@ interface Span {
endPos: number;
}
class StomlError extends Error {
export class StomlError extends Error {
constructor(public span: Span, message: string) {
super(message);
Object.setPrototypeOf(this, new.target.prototype); // restore prototype chain
......
......@@ -5,6 +5,7 @@
<key>fileTypes</key>
<array>
<string>stoml</string>
<string>soitask</string>
</array>
<key>name</key>
<string>STOML</string>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment