Initial LSP implementation/vscode support
Some checks failed
Publish Playground / build (push) Has been cancelled
Publish Playground / deploy (push) Has been cancelled

This commit is contained in:
2026-02-12 20:14:14 -08:00
parent 01a05ba186
commit a9718621e3
36 changed files with 6909 additions and 76 deletions

View File

@@ -0,0 +1,30 @@
{
"root": true,
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": 6,
"sourceType": "module"
},
"plugins": [
"@typescript-eslint"
],
"rules": {
"@typescript-eslint/naming-convention": [
"warn",
{
"selector": "import",
"format": [ "camelCase", "PascalCase" ]
}
],
"@typescript-eslint/semi": "warn",
"curly": "off",
"eqeqeq": "warn",
"no-throw-literal": "warn",
"semi": "off"
},
"ignorePatterns": [
"out",
"dist",
"**/*.d.ts"
]
}

4
newt-vscode-lsp/.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
dist
node_modules
*.vsix

View File

@@ -0,0 +1,5 @@
.vscode/**
.vscode-test/**
.gitignore
vsc-extension-quickstart.md
node_modules

View File

@@ -0,0 +1,9 @@
# Change Log
All notable changes to the "newt-vscode" extension will be documented in this file.
Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how to structure this file.
## [Unreleased]
- Initial release

24
newt-vscode-lsp/LICENSE Normal file
View File

@@ -0,0 +1,24 @@
This is free and unencumbered software released into the public domain.
Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.
In jurisdictions that recognize copyright laws, the author or authors
of this software dedicate any and all copyright interest in the
software to the public domain. We make this dedication for the benefit
of the public at large and to the detriment of our heirs and
successors. We intend this dedication to be an overt act of
relinquishment in perpetuity of all present and future rights to this
software under copyright law.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
For more information, please refer to <https://unlicense.org>

View File

@@ -0,0 +1,3 @@
# newt-vscode README
newt extension for vscode

View File

@@ -0,0 +1,58 @@
const esbuild = require("esbuild");
const production = process.argv.includes('--production');
const watch = process.argv.includes('--watch');
/**
* @type {import('esbuild').Plugin}
*/
const esbuildProblemMatcherPlugin = {
name: 'esbuild-problem-matcher',
setup(build) {
build.onStart(() => {
console.log('[watch] build started');
});
build.onEnd((result) => {
result.errors.forEach(({ text, location }) => {
console.error(`✘ [ERROR] ${text}`);
console.error(` ${location.file}:${location.line}:${location.column}:`);
});
console.log('[watch] build finished');
});
},
};
async function main() {
const ctx = await esbuild.context({
entryPoints: [
'src/extension.ts',
'src/lsp.ts'
],
bundle: true,
format: 'cjs',
minify: production,
sourcemap: !production,
sourcesContent: false,
platform: 'node',
// outfile: 'dist/extension.js',
outdir: 'dist',
external: ['vscode'],
logLevel: 'silent',
plugins: [
/* add to the end of plugins array */
esbuildProblemMatcherPlugin,
],
});
if (watch) {
await ctx.watch();
} else {
await ctx.rebuild();
await ctx.dispose();
}
}
main().catch(e => {
console.error(e);
process.exit(1);
});

View File

@@ -0,0 +1,53 @@
{
// see singleton in Tokenizer.idr
"wordPattern": "[^()\\{}\\[\\],.@\\s]+",
"comments": {
// symbol used for single line comment. Remove this entry if your language does not support line comments
"lineComment": "--",
// symbols used for start and end a block comment. Remove this entry if your language does not support block comments
"blockComment": ["/-", "-/"]
},
// symbols used as brackets
"brackets": [
["{", "}"],
["{{", "}}"],
["[", "]"],
["(", ")"]
],
// symbols that are auto closed when typing
"autoClosingPairs": [
["{", "}"],
["[", "]"],
["(", ")"],
["\"", "\""],
// ["'", "'"], causes problems with foo''
["/-", "-/"]
],
// symbols that can be used to surround a selection
"surroundingPairs": [
["{", "}"],
["[", "]"],
["(", ")"],
["\"", "\""],
["'", "'"]
],
"onEnterRules": [
{
"beforeText": "\\b(where|of|do)\\s*$",
"action": { "indent": "indent" }
},
{
"beforeText": "/-",
"afterText": "-/",
"action": {
"indent": "indentOutdent"
}
},
{
"beforeText": "^\\s+$",
"action": {
"indent": "outdent"
}
}
]
}

6056
newt-vscode-lsp/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,105 @@
{
"name": "newt-vscode",
"publisher": "dunhamsteve",
"displayName": "newt-vscode",
"description": "newt language support with LSP",
"version": "0.0.1",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/dunhamsteve/newt"
},
"engines": {
"vscode": "^1.91.0"
},
"categories": [
"Programming Languages"
],
"activationEvents": [],
"main": "./dist/extension.js",
"contributes": {
"languages": [
{
"id": "newt",
"aliases": [
"newt"
],
"extensions": [
"newt"
],
"configuration": "./language-configuration.json"
}
],
"grammars": [
{
"language": "newt",
"scopeName": "source.newt",
"path": "./syntaxes/newt.tmLanguage.json"
},
{
"path": "./syntaxes/inject.json",
"scopeName": "newt.injection",
"injectTo": [
"text.html.markdown"
],
"embeddedLanguages": {
"meta.embedded.block.idris": "newt"
}
}
],
"commands": [
{
"command": "newt-vscode.check",
"title": "Check newt file"
}
],
"configuration": {
"type": "object",
"title": "Newt Configuration",
"properties": {
"newt.path": {
"type": "string",
"default": "node bootstrap/newt.js",
"description": "Command to run newt"
},
"newt.lspPath": {
"type": "string",
"default": null,
"description": "path to LSP script (run in node)"
}
}
}
},
"scripts": {
"vscode:prepublish": "npm run package",
"compile": "npm run check-types && npm run lint && node esbuild.js",
"watch": "npm-run-all -p watch:*",
"watch:esbuild": "node esbuild.js --watch",
"esbuild": "node esbuild.js",
"package": "echo npm run check-types && npm run lint && node esbuild.js --production",
"compile-tests": "tsc -p . --outDir out",
"watch-tests": "tsc -p . -w --outDir out",
"pretest": "npm run compile-tests && npm run compile && npm run lint",
"check-types": "tsc --noEmit",
"lint": "eslint src --ext ts",
"test": "vscode-test"
},
"devDependencies": {
"@types/mocha": "^10.0.7",
"@types/node": "25.x",
"@types/vscode": "^1.90.0",
"@typescript-eslint/eslint-plugin": "^7.14.1",
"@typescript-eslint/parser": "^7.11.0",
"@vscode/test-cli": "^0.0.9",
"@vscode/test-electron": "^2.4.0",
"esbuild": "^0.25.0",
"eslint": "^8.57.0",
"npm-run-all": "^4.1.5",
"typescript": "^5.4.5"
},
"dependencies": {
"vscode-languageclient": "^9.0.1",
"vscode-languageserver": "^9.0.1",
"vscode-languageserver-textdocument": "^1.0.12"
}
}

View File

@@ -0,0 +1,36 @@
// This needs to be fleshed out a lot, I've been adding them as needed
// There is a mix of agda, lean, and my own shortcuts here
export const ABBREV: Record<string, string> = {
"\\x": "×",
"\\r": "→",
"\\all": "∀",
"\\\\": "\\",
"\\==": "≡",
"\\circ": "∘",
"\\oplus": "⊕",
"\\otimes": "⊗",
// lean
"\\1": "₁",
"\\2": "₂",
"\\<": "⟨",
"\\>": "⟩",
// agda
"\\_0": "₀",
"\\_1": "₁",
"\\_2": "₂",
"\\_3": "₃",
// lean has \n here, which is a royal pain
"\\neg": "¬",
"\\bN": "",
"\\bZ": "",
"\\GG": "Γ",
"\\Gi": "ι",
"\\Gl": "λ",
"\\Gs": "σ",
"\\Gt": "τ",
"\\GD": "Δ",
"\\GS": "Σ",
"\\GP": "∏",
"\\[[": "⟦",
"\\]]": "⟧",
};

View File

@@ -0,0 +1,95 @@
import * as vscode from "vscode";
import { ABBREV } from "./abbrev";
import {
LanguageClient,
LanguageClientOptions,
ServerOptions,
TransportKind
} from 'vscode-languageclient/node';
// They put this at module level for deactivate below.
let client: LanguageClient
export function activate(context: vscode.ExtensionContext) {
const serverModule = context.asAbsolutePath('./dist/lsp.js')
console.log('*** servervModule', serverModule)
const config = vscode.workspace.getConfiguration("newt");
const cmd = config.get<string | null>("lspPath");
let serverOptions: ServerOptions
if (cmd) {
serverOptions = {
run: { command: "node", args: [cmd], transport: TransportKind.pipe },
debug: { command: "node", args: [cmd], transport: TransportKind.pipe },
}
} else {
serverOptions = {
run: { module: serverModule, transport: TransportKind.ipc },
debug: { module: serverModule, transport: TransportKind.ipc },
}
}
const clientOptions: LanguageClientOptions = {
documentSelector: [{ scheme: 'file', language: 'newt' }],
synchronize: {
fileEvents: vscode.workspace.createFileSystemWatcher('*.newt')
}
}
client = new LanguageClient(
'NewtLanguageServer',
'Newt Language Server',
serverOptions,
clientOptions
)
client.start();
console.log('CLIENT started')
vscode.workspace.onDidChangeTextDocument((event) => {
const editor = vscode.window.activeTextEditor;
if (!editor || event.document !== editor.document) return;
const changes = event.contentChanges;
if (changes.length === 0) return;
// TODO - agda input mode does the replacement as soon as possible
// but if the sequence is a prefix, it will change for subsequent characters
// The latter would require keeping state, but if we don't allow sequences to prefix
// each other, we could activate quicker.
const lastChange = changes[changes.length - 1];
const text = lastChange.text;
// Check if the last change is a potential shortcut trigger
if (!text || !(" ')\\_".includes(text) || text.startsWith('\n'))) return;
const document = editor.document;
const position = lastChange.range.end;
const lineText = document.lineAt(position.line).text;
const start = Math.max(0, position.character - 10);
const snippet = lineText.slice(start, position.character);
const m = snippet.match(/(\\[^ ]+)$/);
if (m) {
const cand = m[0];
console.log("cand", cand);
const replacement = ABBREV[cand];
console.log("repl", replacement);
if (replacement) {
const range = new vscode.Range(
position.line,
position.character - cand.length,
position.line,
position.character
);
editor.edit((editBuilder) => {
editBuilder.replace(range, replacement);
});
}
}
});
return;
}
export function deactivate() {
if (client) client.stop();
}

View File

@@ -0,0 +1,53 @@
/**
* WIP
*
* Wraps newt.js (compiled from src/LSP.newt with some tweaks to `export`) with the
* vscode LSP server module.
*/
import { LSP_checkFile, LSP_updateFile, LSP_hoverInfo } from './newt.js'
import {
createConnection,
TextDocuments,
ProposedFeatures,
Hover,
InitializeParams,
InitializeResult,
TextDocumentSyncKind,
} from "vscode-languageserver/node";
import { TextDocument } from "vscode-languageserver-textdocument";
const connection = createConnection(ProposedFeatures.all);
const documents = new TextDocuments(TextDocument);
documents.onDidChangeContent(async (change) => {
console.log('DIDCHANGE', change.document.uri)
const uri = change.document.uri;
const text = change.document.getText();
LSP_updateFile(uri, text);
const diagnostics = LSP_checkFile(uri);
console.log(`Got ${JSON.stringify(diagnostics, null, ' ')}`)
connection.sendDiagnostics({ uri, diagnostics })
});
connection.onHover((params): Hover | null => {
const uri = params.textDocument.uri;
const pos = params.position;
console.log('HOVER', uri, pos)
let value = LSP_hoverInfo(uri, pos.line, pos.character)
if (!value) return null
console.log('HOVER is ', value)
return { contents: { kind: "plaintext", value } };
});
connection.onInitialize((_params: InitializeParams): InitializeResult => ({
capabilities: {
textDocumentSync: TextDocumentSyncKind.Incremental,
hoverProvider: true,
},
}));
documents.listen(connection);
connection.listen();
console.log('STARTED')

5
newt-vscode-lsp/src/newt.d.ts vendored Normal file
View File

@@ -0,0 +1,5 @@
import { Diagnostic } from "vscode-languageserver";
export function LSP_updateFile(name: string, content: string): (eta: any) => any;
export function LSP_checkFile(name: string): Diagnostic[];
export function LSP_hoverInfo(name: string, row: number, col: number): string|null;

View File

@@ -0,0 +1,44 @@
{
"scopeName": "newt.injection",
"injectionSelector": "L:text.html.markdown",
"patterns": [
{
"include": "#fenced_code_block_newt"
}
],
"repository": {
"fenced_code_block_newt": {
"begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(newt)((\\s+|:|,|\\{|\\?)[^`]*)?$)",
"name": "markup.fenced_code.block.markdown",
"end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$",
"beginCaptures": {
"3": {
"name": "punctuation.definition.markdown"
},
"4": {
"name": "fenced_code.block.language.markdown"
},
"5": {
"name": "fenced_code.block.language.attributes.markdown"
}
},
"endCaptures": {
"3": {
"name": "punctuation.definition.markdown"
}
},
"patterns": [
{
"begin": "(^|\\G)(\\s*)(.*)",
"while": "(^|\\G)(?!\\s*([`~]{3,})\\s*$)",
"contentName": "meta.embedded.block.newt",
"patterns": [
{
"include": "source.newt"
}
]
}
]
}
}
}

View File

@@ -0,0 +1,47 @@
{
"$schema": "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json",
"name": "newt",
"scopeName": "source.newt",
"patterns": [
{
"name": "invalid.illegal.trace",
"match": "\\b(trace|strace|fatalError)\\b"
},
{
"name": "comment.block.newt",
"begin": "/-",
"end": "-/",
"contentName": "comment.block.newt"
},
{
"name": "comment.line.newt",
"begin": "--",
"end": "\\n"
},
{
"name": "keyword.newt",
"match": "\\b(λ|=>|<-|->|→|:=|\\$|data|record|constructor|where|do|class|uses|instance|case|of|let|if|then|else|forall|∀|in|U|module|import|ptype|pfunc|infix|infixl|infixr)\\b"
},
{
"name": "string.js",
"begin": "`",
"end": "`",
"patterns": [{ "include": "source.js" }]
},
{
"name": "character",
"match": "'\\\\?.'"
},
{
"name": "string.double.newt",
"begin": "\"",
"end": "\"",
"patterns": [
{
"name": "constant.character.escape.newt",
"match": "\\\\[^{]"
}
]
}
]
}

View File

@@ -0,0 +1,18 @@
{
"compilerOptions": {
"module": "Node16",
"target": "ES2022",
"lib": [ "ES2022" ],
"sourceMap": true,
// so node can run this stuff
"allowImportingTsExtensions": true,
// required by previous, but we use esbuild anyway
"noEmit": true,
"rootDir": "src",
"strict": true /* enable all strict type-checking options */
/* Additional Checks */
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
// "noUnusedParameters": true, /* Report errors on unused parameters. */
}
}